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}