Skip to main content

aegis_api/
lib.rs

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