Skip to main content

soma_auth/upstream/
runtime.rs

1use std::sync::Arc;
2
3use crate::config::AuthConfig;
4use crate::sqlite::SqliteStore;
5use crate::upstream::cache::OauthClientCache;
6use crate::upstream::config::UpstreamConfig;
7use crate::upstream::encryption::{EncryptionKey, load_key};
8use crate::upstream::manager::UpstreamOauthManager;
9use anyhow::{Context, Result};
10
11pub struct UpstreamOauthRuntime {
12    pub managers: Arc<dashmap::DashMap<String, UpstreamOauthManager>>,
13    pub cache: OauthClientCache,
14    pub sqlite: SqliteStore,
15    pub key: EncryptionKey,
16    pub redirect_uri: String,
17}
18
19/// Build the upstream OAuth runtime for the upstreams that declare an `oauth`
20/// block.
21///
22/// Takes the upstream slice directly rather than a whole product config type
23/// so this runtime stays decoupled from the consumer's config type: `soma-auth`
24/// reads only the upstream list, never anything else.
25pub async fn build_upstream_oauth_runtime(
26    upstreams: &[UpstreamConfig],
27    auth_config: &AuthConfig,
28    encryption_key_raw: Option<&str>,
29) -> Result<Option<UpstreamOauthRuntime>> {
30    let Some(public_url) = auth_config.public_url.as_ref() else {
31        tracing::info!(
32            subsystem = "gateway_client",
33            phase = "oauth.runtime.disabled",
34            "upstream oauth runtime disabled because no public url is configured"
35        );
36        return Ok(None);
37    };
38    let Some(encryption_key_raw) =
39        encryption_key_raw.and_then(|value| (!value.trim().is_empty()).then_some(value))
40    else {
41        tracing::info!(
42            subsystem = "gateway_client",
43            phase = "oauth.runtime.disabled",
44            "upstream oauth runtime disabled because no encryption key is configured"
45        );
46        return Ok(None);
47    };
48    anyhow::ensure!(
49        public_url.scheme() == "https",
50        "public_url must be absolute https:// when upstream oauth is configured"
51    );
52    let key = load_key(encryption_key_raw)
53        .map_err(|error| anyhow::anyhow!("invalid upstream OAuth encryption key: {error}"))?;
54    let sqlite = SqliteStore::open(auth_config.sqlite_path.clone())
55        .await
56        .context("open sqlite store for upstream oauth")?;
57    let redirect_uri = build_upstream_oauth_callback_uri(public_url)?;
58
59    Ok(Some(build_upstream_oauth_runtime_from_parts(
60        upstreams,
61        sqlite,
62        key,
63        redirect_uri,
64    )))
65}
66
67/// Assemble the runtime from pre-loaded parts.
68///
69/// `upstreams` is the upstream slice (decoupled from any product config); only the
70/// entries with an `oauth` block get a manager.
71pub fn build_upstream_oauth_runtime_from_parts(
72    upstreams: &[UpstreamConfig],
73    sqlite: SqliteStore,
74    key: EncryptionKey,
75    redirect_uri: String,
76) -> UpstreamOauthRuntime {
77    let managers = Arc::new(dashmap::DashMap::new());
78    for upstream in upstreams.iter().filter(|upstream| upstream.oauth.is_some()) {
79        managers.insert(
80            upstream.name.clone(),
81            UpstreamOauthManager::new(
82                sqlite.clone(),
83                key.clone(),
84                upstream.clone(),
85                redirect_uri.clone(),
86            ),
87        );
88    }
89    let cache = OauthClientCache::new(Arc::clone(&managers));
90    tracing::info!(
91        subsystem = "gateway_client",
92        phase = "oauth.runtime.ready",
93        oauth_upstream_count = managers.len(),
94        "upstream oauth runtime initialized"
95    );
96    UpstreamOauthRuntime {
97        managers,
98        cache,
99        sqlite,
100        key,
101        redirect_uri,
102    }
103}
104
105pub fn build_upstream_oauth_callback_uri(public_url: &url::Url) -> Result<String> {
106    let mut redirect_uri = public_url.clone();
107    let base_path = redirect_uri.path().trim_end_matches('/');
108    let next_path = if base_path.is_empty() {
109        "/auth/upstream/callback".to_string()
110    } else {
111        format!("{base_path}/auth/upstream/callback")
112    };
113    redirect_uri.set_path(&next_path);
114    redirect_uri.set_query(None);
115    redirect_uri.set_fragment(None);
116    Ok(redirect_uri.to_string())
117}