Skip to main content

aegis_api/
lib.rs

1mod aegis_store;
2mod config;
3mod endpoint;
4mod firestore;
5mod identity;
6mod path;
7use axum::{Router, routing::get};
8use std::sync::Arc;
9#[derive(Default)]
10pub struct ApplicationOptions {
11    pub database: arche_firestore::DatabaseOptions,
12    pub oidc_client_secret: String,
13}
14
15pub struct ApplicationConfig {
16    database: arche_firestore::DatabaseConfig,
17    oidc_client_secret: String,
18}
19
20impl ApplicationOptions {
21    pub fn from_env() -> anyhow::Result<Self> {
22        Ok(Self {
23            database: arche_firestore::DatabaseOptions::from_env(),
24            oidc_client_secret: std::env::var(aegis_dto::identity::OIDC_SECRET_ENV).map_err(
25                |_| anyhow::anyhow!("AEGIS_OIDC_CLIENT_SECRET must be supplied to the API runtime"),
26            )?,
27        })
28    }
29
30    pub fn validate(self) -> anyhow::Result<ApplicationConfig> {
31        anyhow::ensure!(
32            !self.oidc_client_secret.trim().is_empty(),
33            "OIDC client secret must not be empty"
34        );
35        Ok(ApplicationConfig {
36            database: self.database.validate()?,
37            oidc_client_secret: self.oidc_client_secret,
38        })
39    }
40}
41
42impl ApplicationConfig {
43    pub async fn connect(self) -> anyhow::Result<Router> {
44        let db = self.database.connect().await?;
45        application(db, self.oidc_client_secret).await
46    }
47}
48
49async fn application(
50    db: arche_firestore::Db,
51    oidc_client_secret: String,
52) -> anyhow::Result<Router> {
53    let identity = identity::store(&db)?.load_config().await?;
54    let identities = identity::store(&db)?;
55    let login = identity::load_login(&db).await?;
56    let jwt_issuer = Arc::new(phylax_core::JwtIssuer::from_config(
57        &identity.api_token.jwt_config(),
58    )?);
59    let oauth = phylax_core::AuthorizationCodeOAuthEndpoint::new(
60        identities.auth_store(&identity, login.login_session_ttl_seconds)?,
61        phylax_oidc::OidcProvider::connect(
62            phylax_oidc::OidcOptions {
63                issuer_url: login.issuer_url.clone(),
64                client_id: login.client_id.clone(),
65                client_secret: oidc_client_secret,
66                redirect_uri: login.redirect_uri.clone(),
67            }
68            .validate()?,
69        )
70        .await?,
71        endpoint::oauth::OAuthAccessPolicy {
72            identities: identities.clone(),
73            provider: login.issuer_url.clone(),
74            public_client_id: "aegis-tool".into(),
75            audience: identity.api_token.audience.clone(),
76            base_scopes: vec!["aegis:read".into(), "aegis:user".into()],
77        },
78        jwt_issuer.clone(),
79        phylax_core::AuthorizationCodeOAuthConfig {
80            public_client_id: "aegis-tool".into(),
81            login_session_ttl_seconds: login.login_session_ttl_seconds,
82            authorization_code_ttl_seconds: login.authorization_code_ttl_seconds,
83            state_max_len: 512,
84        },
85    )?;
86    let revoke = endpoint::oauth::UserRevokeTokenGrant {
87        auth: identities.auth_store(&identity, login.login_session_ttl_seconds)?,
88    };
89    let info = serde_json::json!({"issuer":identity.api_token.iss,"protocol":2,"version":env!("CARGO_PKG_VERSION")});
90    let mut namespace_routes = Router::new();
91    for namespace in firestore::list_aegis_namespaces(&db).await? {
92        let store = firestore::AegisDb::new(db.clone(), namespace.clone());
93        let config = firestore::load_aegis_instance_config(&store).await?;
94        let auth = store.auth_store(&identity.refresh_token, login.login_session_ttl_seconds)?;
95        let audience = format!(
96            "{}/aegis/namespaces/{namespace}",
97            identity.api_token.audience
98        );
99        let state = endpoint::aegis::AegisState::new(endpoint::aegis::AegisStateParts {
100            store: store.clone(),
101            auth: Arc::new(auth.clone()),
102            issuer: jwt_issuer.clone(),
103            client_ca: &config.client_ca,
104            direct_client_ca: &config.direct_client_ca,
105            server_ca: &config.server_ca,
106            tls: &config.tls,
107            api_issuer: &format!(
108                "{}/namespaces/{namespace}",
109                identity.api_token.iss.trim_end_matches('/')
110            ),
111            api_audience: &audience,
112            user_api_audience: &identity.api_token.audience,
113            namespace: namespace.clone(),
114            cfg: &config.config,
115        })?;
116        let grant = endpoint::aegis::AegisAgentTokenGrant {
117            auth,
118            store: store.clone(),
119            issuer: jwt_issuer.clone(),
120            api_audience: audience,
121        };
122        namespace_routes = namespace_routes.nest(
123            &format!("/namespaces/{namespace}"),
124            endpoint::aegis::router(state, grant),
125        );
126    }
127
128    use phylax_core::oauth::path::*;
129    let api = namespace_routes
130        .route(
131            "/info",
132            get(move || {
133                let info = info.clone();
134                async move { axum::Json(info) }
135            }),
136        )
137        .merge(phylax_core::authorization_code_oauth_router(
138            OAUTH_AUTHORIZE,
139            OAUTH_CALLBACK,
140            OAUTH_TOKEN,
141            oauth,
142        ))
143        .merge(phylax_core::form_revoke_token_router(OAUTH_REVOKE, revoke));
144    Ok(Router::new()
145        .route("/health", get(|| async { "OK" }))
146        .nest("/v2", api))
147}