Private
Public Access
1
0
Files
linux_patch_manager/crates/pm-web/src/main.rs
Draco-Lunaris-Echo e6dd1b8489
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 4s
CI Pipeline / Clippy Lints (push) Successful in 51s
CI Pipeline / Rust Unit Tests (push) Successful in 1m56s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
test: add authz gate integration tests (closes #15)
* test: add authz gate integration tests (closes #15)

* fix: separate authz gate 403 tests from DB-dependent tests

---------

Co-authored-by: Draco Lunaris <331325+Draco-Lunaris@users.noreply.github.com>
2026-06-06 11:18:11 -05:00

202 lines
7.3 KiB
Rust

//! pm-web — Linux Patch Manager web server (binary entry-point).
use dashmap::DashMap;
use pm_auth::{jwt, rbac::AuthConfig};
use pm_core::{config::AppConfig, db, models::ApprovedEntry};
use pm_web::routes::sso::{OidcCache, SsoHandoff, SsoSession};
use pm_web::routes::ws::WsTicket;
use pm_web::{bootstrap_admin_password, build_router, AppState};
use std::{net::SocketAddr, sync::Arc, time::Duration};
use tokio::sync::Mutex;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Install the default crypto provider for rustls (required since 0.23)
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install rustls crypto provider");
let config_path = std::env::var("PATCH_MANAGER_CONFIG")
.unwrap_or_else(|_| "/etc/patch-manager/config.toml".to_string());
let config = AppConfig::load(&config_path).unwrap_or_else(|_| {
eprintln!("Config file not found or invalid, using defaults");
AppConfig::default()
});
pm_core::logging::init(&config.logging);
tracing::info!(
version = env!("CARGO_PKG_VERSION"),
"patch-manager-web starting"
);
let signing_key_pem = jwt::load_signing_key(&config.security.jwt_signing_key_path)
.unwrap_or_else(|e| {
tracing::warn!(error = %e, "JWT signing key not found (dev mode)");
String::new()
});
let verify_key_pem =
jwt::load_verify_key(&config.security.jwt_verify_key_path).unwrap_or_else(|e| {
tracing::warn!(error = %e, "JWT verify key not found (dev mode)");
String::new()
});
let auth_config = Arc::new(AuthConfig::new(
verify_key_pem,
&config.security.ip_whitelist,
&config.security.trusted_proxies,
));
let pool = db::init_pool(&config.database).await?;
db::run_migrations(&pool).await?;
// Bootstrap admin password if the seed admin still has the placeholder hash.
bootstrap_admin_password(&pool).await;
// Initialise the internal CA using the configured certificate paths.
let ca_base = std::path::Path::new(&config.security.ca_cert_path)
.parent()
.expect("CA certificate path must have a parent directory");
let ca = pm_ca::CertAuthority::init(ca_base, &pool)
.await
.unwrap_or_else(|e| {
tracing::warn!(error = %e, "CA init failed (dev mode)");
panic!("CA initialization failed: {}", e);
});
let ws_tickets: Arc<DashMap<String, WsTicket>> = Arc::new(DashMap::new());
let sso_sessions: Arc<DashMap<String, SsoSession>> = Arc::new(DashMap::new());
let sso_handoffs: Arc<DashMap<String, SsoHandoff>> = Arc::new(DashMap::new());
let oidc_cache: Arc<Mutex<OidcCache>> = Arc::new(Mutex::new(OidcCache::default()));
let approved_enrollments: Arc<DashMap<String, ApprovedEntry>> = Arc::new(DashMap::new());
// Background task: purge expired WS tickets every 30 seconds.
{
let tickets = ws_tickets.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
let now = chrono::Utc::now();
let before = tickets.len();
tickets.retain(|_, v| v.expires_at > now);
let removed = before.saturating_sub(tickets.len());
if removed > 0 {
tracing::debug!(removed, "Purged expired WS tickets");
}
}
});
}
// Background task: purge expired SSO sessions every 60 seconds.
{
let sessions = sso_sessions.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60));
loop {
interval.tick().await;
let now = chrono::Utc::now();
let cutoff = now - chrono::Duration::minutes(10);
let before = sessions.len();
sessions.retain(|_, v| v.created_at > cutoff);
let removed = before.saturating_sub(sessions.len());
if removed > 0 {
tracing::debug!(removed, "Purged expired SSO sessions");
}
}
});
}
// Background task: purge expired approved enrollment PKI bundles.
{
let approved = approved_enrollments.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60));
loop {
interval.tick().await;
let before = approved.len();
approved.retain(|_, entry| !entry.is_expired());
let removed = before.saturating_sub(approved.len());
if removed > 0 {
tracing::debug!(removed, "Purged expired enrollment PKI bundles");
}
}
});
}
// Background task: purge expired SSO handoff codes every 60 seconds.
{
let handoffs = sso_handoffs.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60));
loop {
interval.tick().await;
let now = std::time::Instant::now();
let before = handoffs.len();
handoffs.retain(|_, v| v.expires_at > now);
let removed = before.saturating_sub(handoffs.len());
if removed > 0 {
tracing::debug!(removed, "Purged expired SSO handoff codes");
}
}
});
}
let state = AppState {
db: pool,
config: Arc::new(config.clone()),
signing_key_pem,
auth_config,
ws_tickets,
sso_sessions,
sso_handoffs,
ca: Arc::new(ca),
approved_enrollments,
oidc_cache,
};
let app = build_router(state);
let addr: SocketAddr = format!("{}:{}", config.server.host, config.server.port)
.parse()
.expect("Invalid bind address");
// Try to load TLS certificate and key; fall back to plain HTTP if missing.
let tls_cert = std::path::Path::new(&config.security.web_tls_cert_path);
let tls_key = std::path::Path::new(&config.security.web_tls_key_path);
if tls_cert.exists() && tls_key.exists() {
let tls_config = axum_server::tls_rustls::RustlsConfig::from_pem_file(
&config.security.web_tls_cert_path,
&config.security.web_tls_key_path,
)
.await
.map_err(|e| {
tracing::error!(error = %e, "Failed to load TLS certificates");
e
})?;
tracing::info!(%addr, "Listening (HTTPS)");
axum_server::bind_rustls(addr, tls_config)
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
.await?;
} else {
tracing::warn!(
cert_path = %config.security.web_tls_cert_path,
key_path = %config.security.web_tls_key_path,
"TLS certificates not found — falling back to plain HTTP. \
This is insecure for production!"
);
tracing::info!(%addr, "Listening (HTTP — no TLS)");
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
}
Ok(())
}