aegis-api 0.3.5

Independent Aegis fleet control plane HTTP service.
Documentation
use anyhow::Context;
mod aegis_store;
mod config;
mod endpoint;
mod firestore;
mod identity;
mod path;
use axum::{Router, routing::get};
use std::sync::Arc;
#[derive(Default)]
pub struct ApplicationOptions {
    pub database: arche_firestore::DatabaseOptions,
    pub oidc_client_secret: Option<String>,
}

pub struct ApplicationConfig {
    database: arche_firestore::DatabaseConfig,
    oidc_client_secret: Option<String>,
}

impl ApplicationOptions {
    pub fn from_env() -> anyhow::Result<Self> {
        Ok(Self {
            database: arche_firestore::DatabaseOptions::from_env(),
            oidc_client_secret: match std::env::var(aegis_dto::identity::OIDC_SECRET_ENV) {
                Ok(secret) => Some(secret),
                Err(std::env::VarError::NotPresent) => None,
                Err(error) => return Err(error.into()),
            },
        })
    }

    pub fn validate(self) -> anyhow::Result<ApplicationConfig> {
        anyhow::ensure!(
            self.oidc_client_secret
                .as_ref()
                .is_none_or(|secret| !secret.trim().is_empty()),
            "OIDC client secret must not be empty"
        );
        Ok(ApplicationConfig {
            database: self.database.validate()?,
            oidc_client_secret: self.oidc_client_secret,
        })
    }
}

impl ApplicationConfig {
    pub async fn connect(self) -> anyhow::Result<Router> {
        let db = self.database.connect().await?;
        application(db, self.oidc_client_secret).await
    }
}

async fn application(
    db: arche_firestore::Db,
    oidc_client_secret: Option<String>,
) -> anyhow::Result<Router> {
    let identity = identity::store(&db)?.load_config().await?;
    let identities = identity::store(&db)?;
    let authentication = identity::load_authentication(&db).await?;
    let user_auth = identities.auth_store(&identity, authentication.login_session_ttl_seconds())?;
    let jwt_issuer = Arc::new(phylax_core::JwtIssuer::from_config(
        &identity.api_token.jwt_config(),
    )?);
    let revoke = endpoint::oauth::UserRevokeTokenGrant {
        auth: user_auth.clone(),
    };
    let info = serde_json::json!({"issuer":identity.api_token.iss,"protocol":2,"version":env!("CARGO_PKG_VERSION"),
        "authentication":{"credentials":true,"oauth":authentication.oauth.is_some()}});
    let mut namespace_routes = Router::new();
    for namespace in firestore::list_aegis_namespaces(&db).await? {
        let store = firestore::AegisDb::new(db.clone(), namespace.clone());
        let config = firestore::load_aegis_instance_config(&store).await?;
        let auth = store.auth_store(
            &identity.refresh_token,
            authentication.login_session_ttl_seconds(),
        )?;
        let audience = format!(
            "{}/aegis/namespaces/{namespace}",
            identity.api_token.audience
        );
        let state = endpoint::aegis::AegisState::new(endpoint::aegis::AegisStateParts {
            store: store.clone(),
            auth: Arc::new(auth.clone()),
            issuer: jwt_issuer.clone(),
            client_ca: &config.client_ca,
            direct_client_ca: &config.direct_client_ca,
            server_ca: &config.server_ca,
            tls: &config.tls,
            api_issuer: &format!(
                "{}/namespaces/{namespace}",
                identity.api_token.iss.trim_end_matches('/')
            ),
            api_audience: &audience,
            user_api_audience: &identity.api_token.audience,
            namespace: namespace.clone(),
            cfg: &config.config,
        })?;
        let grant = endpoint::aegis::AegisAgentTokenGrant {
            auth,
            store: store.clone(),
            issuer: jwt_issuer.clone(),
            api_audience: audience,
        };
        namespace_routes = namespace_routes.nest(
            &format!("/namespaces/{namespace}"),
            endpoint::aegis::router(state, grant),
        );
    }

    use phylax_core::oauth::path::*;
    let mut api = namespace_routes
        .route(
            "/info",
            get(move || {
                let info = info.clone();
                async move { axum::Json(info) }
            }),
        )
        .merge(phylax_core::json_refresh_token_router(
            aegis_dto::identity::USER_TOKEN_PATH,
            endpoint::credentials::UserTokenGrant {
                auth: user_auth,
                identities: identities.clone(),
                issuer: jwt_issuer.clone(),
                audience: identity.api_token.audience.clone(),
            },
        ))
        .merge(phylax_core::form_revoke_token_router(
            aegis_dto::identity::USER_REVOKE_PATH,
            revoke.clone(),
        ));
    if let Some(login) = authentication.oauth {
        let oauth = phylax_core::AuthorizationCodeOAuthEndpoint::new(
            identities.auth_store(&identity, login.login_session_ttl_seconds)?,
            phylax_oidc::OidcProvider::connect(
                phylax_oidc::OidcOptions {
                    issuer_url: login.issuer_url.clone(),
                    client_id: login.client_id.clone(),
                    client_secret: oidc_client_secret
                        .context("OAuth is enabled; supply AEGIS_OIDC_CLIENT_SECRET")?,
                    redirect_uri: login.redirect_uri.clone(),
                }
                .validate()?,
            )
            .await?,
            endpoint::oauth::OAuthAccessPolicy {
                identities: identities.clone(),
                provider: login.issuer_url.clone(),
                public_client_id: "aegis-tool".into(),
                audience: identity.api_token.audience.clone(),
                base_scopes: vec!["aegis:read".into(), "aegis:user".into()],
            },
            jwt_issuer.clone(),
            phylax_core::AuthorizationCodeOAuthConfig {
                public_client_id: "aegis-tool".into(),
                login_session_ttl_seconds: login.login_session_ttl_seconds,
                authorization_code_ttl_seconds: login.authorization_code_ttl_seconds,
                state_max_len: 512,
            },
        )?;
        api = api
            .merge(phylax_core::authorization_code_oauth_router(
                OAUTH_AUTHORIZE,
                OAUTH_CALLBACK,
                OAUTH_TOKEN,
                oauth,
            ))
            .merge(phylax_core::form_revoke_token_router(OAUTH_REVOKE, revoke));
    } else {
        anyhow::ensure!(
            oidc_client_secret.is_none(),
            "OIDC secret supplied but OAuth is not configured"
        );
    }
    Ok(Router::new()
        .route("/health", get(|| async { "OK" }))
        .nest("/v2", api))
}