Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

mannequin/src/server.rs

use axum::{
    body::Bytes,
    extract::{Path, Query, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{get, post},
    Json, Router,
};
use serde::Deserialize;
use std::sync::Arc;

use crate::auth::AuthService;
use crate::config::MannequinConfig;
use crate::event_log::{Direction, EventLog, SystemKind};
use crate::nexus::MockNexusService;
use crate::{OidcService, PkiService};

pub struct MannequinServer {
    pub pki: Arc<PkiService>,
    pub oidc: Arc<OidcService>,
    pub auth: Arc<AuthService>,
    pub nexus: Arc<MockNexusService>,
    pub event_log: EventLog,
    pub port: u16,
}

#[derive(Clone)]
struct AppState {
    pki: Arc<PkiService>,
    oidc: Arc<OidcService>,
    auth: Arc<AuthService>,
    nexus: Arc<MockNexusService>,
    event_log: EventLog,
}

impl MannequinServer {
    pub fn new(port: u16) -> anyhow::Result<Self> {
        Self::from_config(MannequinConfig {
            port,
            ..Default::default()
        })
    }

    pub fn from_config(config: MannequinConfig) -> anyhow::Result<Self> {
        let port = config.port;
        let pki = Arc::new(PkiService::from_config(&config)?);
        let oidc = Arc::new(OidcService::new(format!("http://127.0.0.1:{}", port)));
        let auth = Arc::new(AuthService::new());
        let base_url = format!("http://127.0.0.1:{}", port);
        let nexus = Arc::new(MockNexusService::new(&base_url));
        let event_log = EventLog::new();

        Ok(Self {
            pki,
            oidc,
            auth,
            nexus,
            event_log,
            port,
        })
    }

    pub fn router(&self) -> Router {
        let state = AppState {
            pki: self.pki.clone(),
            oidc: self.oidc.clone(),
            auth: self.auth.clone(),
            nexus: self.nexus.clone(),
            event_log: self.event_log.clone(),
        };

        Router::new()
            // PKI endpoints
            .route("/pki/ca.pem", get(pki_ca_pem))
            .route("/pki/issue/server", post(pki_issue_server))
            .route("/pki/issue/client", post(pki_issue_client))
            .route("/pki/list", get(pki_list))
            // OIDC endpoints
            .route("/.well-known/openid-configuration", get(oidc_discovery))
            .route("/token", post(oidc_token))
            .route("/userinfo", get(oidc_userinfo))
            // Auth endpoints (user management + credential issuance)
            .route("/auth/users", get(auth_list_users))
            .route("/auth/credentials/{username}", post(auth_issue_credentials))
            .route("/auth/validate", get(auth_validate))
            // Mock Nexus endpoints
            .route("/service/rest/v1/repositories", get(nexus_list_repos))
            .route(
                "/service/rest/v1/repositories/raw/hosted",
                post(nexus_create_repo),
            )
            .route("/service/rest/v1/assets", get(nexus_list_assets))
            .route(
                "/repository/{repo}/{*path}",
                get(nexus_download).put(nexus_upload),
            )
            // Health
            .route("/health", get(health))
            .with_state(state)
    }

    pub async fn serve(self) -> anyhow::Result<()> {
        let addr = format!("0.0.0.0:{}", self.port);
        let listener = tokio::net::TcpListener::bind(&addr).await?;
        tracing::info!("Mannequin listening on {}", addr);
        // TCP_NODELAY on every connection (HOL-PERF-3): axum 0.8 leaves Nagle on.
        let listener =
            axum::serve::ListenerExt::tap_io(listener, |tcp: &mut tokio::net::TcpStream| {
                let _ = tcp.set_nodelay(true);
            });
        axum::serve(listener, self.router()).await?;
        Ok(())
    }
}

// === Health ===

async fn health() -> &'static str {
    "OK"
}

// === PKI handlers ===

async fn pki_ca_pem(State(state): State<AppState>) -> String {
    state
        .event_log
        .log(SystemKind::Pki, Direction::Inbound, "GET /pki/ca.pem", None);
    state.pki.ca_cert_pem()
}

#[derive(Deserialize)]
struct IssueServerReq {
    cn: String,
    sans: Option<Vec<String>>,
}

#[derive(serde::Serialize)]
struct CertResponse {
    cert_pem: String,
    key_pem: String,
}

async fn pki_issue_server(
    State(state): State<AppState>,
    Json(req): Json<IssueServerReq>,
) -> Result<Json<CertResponse>, StatusCode> {
    state.event_log.log(
        SystemKind::Pki,
        Direction::Inbound,
        format!("POST /pki/issue/server cn={}", req.cn),
        None,
    );
    let sans: Vec<&str> = req
        .sans
        .as_ref()
        .map(|v| v.iter().map(|s| s.as_str()).collect())
        .unwrap_or_default();
    let cert = state
        .pki
        .issue_server_cert(&req.cn, sans)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    state.event_log.log(
        SystemKind::Pki,
        Direction::Outbound,
        format!("Issued server cert: {}", req.cn),
        None,
    );
    Ok(Json(CertResponse {
        cert_pem: cert.cert_pem,
        key_pem: cert.key_pem,
    }))
}

#[derive(Deserialize)]
struct IssueClientReq {
    cn: String,
}

async fn pki_issue_client(
    State(state): State<AppState>,
    Json(req): Json<IssueClientReq>,
) -> Result<Json<CertResponse>, StatusCode> {
    let cert = state
        .pki
        .issue_client_cert(&req.cn)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    Ok(Json(CertResponse {
        cert_pem: cert.cert_pem,
        key_pem: cert.key_pem,
    }))
}

async fn pki_list(State(state): State<AppState>) -> Json<Vec<String>> {
    Json(state.pki.list_issued())
}

// === OIDC handlers ===

async fn oidc_discovery(State(state): State<AppState>) -> impl IntoResponse {
    Json(state.oidc.discovery())
}

#[derive(Deserialize)]
struct OidcTokenForm {
    username: Option<String>,
    client_id: Option<String>,
}

async fn oidc_token(
    State(state): State<AppState>,
    Json(form): Json<OidcTokenForm>,
) -> impl IntoResponse {
    let subject = form
        .username
        .or(form.client_id)
        .unwrap_or_else(|| "anonymous".to_string());
    state.event_log.log(
        SystemKind::Oidc,
        Direction::Inbound,
        format!("POST /token sub={}", subject),
        None,
    );
    let resp = state.oidc.issue_token(&subject);
    state.event_log.log(
        SystemKind::Oidc,
        Direction::Outbound,
        format!("Token issued: {}", &resp.access_token[..8]),
        None,
    );
    Json(resp)
}

async fn oidc_userinfo(
    State(state): State<AppState>,
    headers: axum::http::HeaderMap,
) -> Result<Json<serde_json::Value>, StatusCode> {
    let token = headers
        .get("Authorization")
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.strip_prefix("Bearer "))
        .ok_or(StatusCode::UNAUTHORIZED)?;

    let subject = state
        .oidc
        .validate_token(token)
        .ok_or(StatusCode::UNAUTHORIZED)?;
    Ok(Json(serde_json::json!({ "sub": subject })))
}

// === Mock Nexus handlers ===

async fn nexus_list_repos(
    State(state): State<AppState>,
) -> Json<Vec<crate::nexus::NexusRepository>> {
    Json(state.nexus.list_repositories())
}

#[derive(Deserialize)]
struct CreateRepoReq {
    #[allow(dead_code)]
    name: String,
}

async fn nexus_create_repo(
    State(_state): State<AppState>,
    Json(_req): Json<CreateRepoReq>,
) -> StatusCode {
    // Repos are fixed (proxy-only, prod, upload) — acknowledge but no-op
    StatusCode::CREATED
}

#[derive(Deserialize)]
struct AssetQuery {
    repository: String,
}

async fn nexus_list_assets(
    State(state): State<AppState>,
    Query(q): Query<AssetQuery>,
) -> Json<crate::nexus::AssetPage> {
    let items = state.nexus.list_assets(&q.repository);
    Json(crate::nexus::AssetPage {
        items,
        continuation_token: None,
    })
}

async fn nexus_upload(
    State(state): State<AppState>,
    Path((repo, path)): Path<(String, String)>,
    body: Bytes,
) -> StatusCode {
    state.event_log.log(
        SystemKind::Nexus,
        Direction::Inbound,
        format!("PUT /{}/{} ({}B)", repo, path, body.len()),
        None,
    );
    if state.nexus.upload(&repo, &path, body.to_vec()) {
        state.event_log.log(
            SystemKind::Nexus,
            Direction::Outbound,
            format!("201 stored {}", path),
            None,
        );
        StatusCode::CREATED
    } else {
        StatusCode::FORBIDDEN
    }
}

async fn nexus_download(
    State(state): State<AppState>,
    Path((repo, path)): Path<(String, String)>,
) -> Result<Bytes, StatusCode> {
    state.event_log.log(
        SystemKind::Nexus,
        Direction::Inbound,
        format!("GET /{}/{}", repo, path),
        None,
    );
    state
        .nexus
        .download(&repo, &path)
        .await
        .map(|data| {
            state.event_log.log(
                SystemKind::Nexus,
                Direction::Outbound,
                format!("200 {} ({}B)", path, data.len()),
                None,
            );
            Bytes::from(data)
        })
        .ok_or(StatusCode::NOT_FOUND)
}

// === Auth handlers ===

async fn auth_list_users(State(state): State<AppState>) -> Json<Vec<String>> {
    Json(state.auth.list_users())
}

#[derive(serde::Serialize)]
struct CredentialsResponse {
    username: String,
    access_token: String,
    client_cert_pem: String,
    client_key_pem: String,
}

async fn auth_issue_credentials(
    State(state): State<AppState>,
    Path(username): Path<String>,
) -> Result<Json<CredentialsResponse>, StatusCode> {
    state.event_log.log(
        SystemKind::Oidc,
        Direction::Inbound,
        format!("POST /auth/credentials/{}", username),
        None,
    );
    let creds = state
        .auth
        .issue_credentials(&username, &state.oidc, &state.pki)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    state.event_log.log(
        SystemKind::Oidc,
        Direction::Outbound,
        format!("Issued creds for {}", username),
        None,
    );
    Ok(Json(CredentialsResponse {
        username: creds.username,
        access_token: creds.access_token,
        client_cert_pem: creds.client_cert_pem,
        client_key_pem: creds.client_key_pem,
    }))
}

async fn auth_validate(
    State(state): State<AppState>,
    headers: axum::http::HeaderMap,
) -> Result<Json<serde_json::Value>, StatusCode> {
    // Try Bearer token first, then check X-Client-CN header (simulating mTLS CN extraction)
    if let Some(auth_header) = headers.get("Authorization") {
        if let Some(token) = auth_header
            .to_str()
            .ok()
            .and_then(|v| v.strip_prefix("Bearer "))
        {
            if let Some(identity) = state.auth.validate_oidc_token(token, &state.oidc) {
                return Ok(Json(serde_json::json!({
                    "subject": identity.subject,
                    "method": "oidc",
                    "roles": identity.roles,
                })));
            }
        }
    }

    if let Some(cn_header) = headers.get("X-Client-CN") {
        if let Ok(cn) = cn_header.to_str() {
            if let Some(identity) = state.auth.validate_client_cert(cn) {
                return Ok(Json(serde_json::json!({
                    "subject": identity.subject,
                    "method": "mtls",
                    "roles": identity.roles,
                })));
            }
        }
    }

    Err(StatusCode::UNAUTHORIZED)
}