soma_auth/upstream/
runtime.rs1use 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
19pub 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
67pub 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}