Skip to main content

securitydept_token_set_context/access_token_substrate/config/
mod.rs

1use securitydept_oauth_provider::OidcSharedConfig;
2use securitydept_oauth_resource_server::OAuthResourceServerConfig;
3use serde::Deserialize;
4
5use super::capabilities::TokenPropagation;
6use crate::orchestration::BackendConfigError;
7
8pub mod validator;
9
10pub use validator::{
11    AccessTokenSubstrateConfigValidationError, AccessTokenSubstrateConfigValidator,
12    NoopAccessTokenSubstrateConfigValidator,
13};
14
15// ---------------------------------------------------------------------------
16// Config source trait
17// ---------------------------------------------------------------------------
18
19/// Trait for types that supply access-token substrate configuration components.
20///
21/// Mirrors [`BackendOidcModeConfigSource`] for the substrate layer.
22/// Implementors expose component-config accessors and gain default `resolve_*`
23/// helper methods that apply `[oidc]` shared defaults and validate each
24/// component.
25///
26/// [`BackendOidcModeConfigSource`]: crate::backend_oidc_mode::BackendOidcModeConfigSource
27pub trait AccessTokenSubstrateConfigSource {
28    // --- Component accessors ---
29
30    /// Access the raw `[oauth_resource_server]` config block.
31    fn resource_server_config(&self) -> &OAuthResourceServerConfig;
32
33    /// Access the token propagation capability axis.
34    fn token_propagation(&self) -> &TokenPropagation;
35
36    // --- Resolve helpers (default implementations) ---
37
38    /// Apply OIDC shared defaults to the resource-server config and validate.
39    ///
40    /// Delegates to [`OAuthResourceServerConfig::resolve_config`] which handles
41    /// `well_known_url`, `client_id`, `client_secret` inheritance from the
42    /// `[oidc]` shared block.
43    fn resolve_resource_server(
44        &self,
45        shared: &OidcSharedConfig,
46    ) -> Result<OAuthResourceServerConfig, BackendConfigError> {
47        let mut rs = self.resource_server_config().clone();
48        rs.resolve_config(shared)?;
49        Ok(rs)
50    }
51
52    /// **Recommended entry point.** Resolve all substrate sub-configs in one
53    /// step.
54    ///
55    /// When an `[oidc]` shared config is provided, resource-server config
56    /// inherits provider defaults. When `None`, resource-server config is
57    /// returned as-is (valid for deployments without OIDC discovery).
58    fn resolve_all(
59        &self,
60        shared: Option<&OidcSharedConfig>,
61    ) -> Result<ResolvedAccessTokenSubstrateConfig, BackendConfigError> {
62        let validator = NoopAccessTokenSubstrateConfigValidator;
63        self.resolve_all_with_validator(shared, &validator)
64    }
65
66    fn resolve_all_with_validator<V>(
67        &self,
68        shared: Option<&OidcSharedConfig>,
69        validator: &V,
70    ) -> Result<ResolvedAccessTokenSubstrateConfig, BackendConfigError>
71    where
72        V: AccessTokenSubstrateConfigValidator,
73    {
74        let raw_config = AccessTokenSubstrateConfig {
75            resource_server: self.resource_server_config().clone(),
76            token_propagation: self.token_propagation().clone(),
77        };
78        validator
79            .validate_raw_access_token_substrate_config(&raw_config)
80            .map_err(BackendConfigError::AccessTokenSubstrateValidation)?;
81
82        let resource_server = if let Some(shared) = shared {
83            self.resolve_resource_server(shared)?
84        } else {
85            self.resource_server_config().clone()
86        };
87
88        Ok(ResolvedAccessTokenSubstrateConfig {
89            resource_server,
90            token_propagation: self.token_propagation().clone(),
91        })
92    }
93}
94
95// ---------------------------------------------------------------------------
96// Raw config (TOML / env deserialisable)
97// ---------------------------------------------------------------------------
98
99/// Unified configuration for the access-token substrate layer.
100///
101/// This struct owns the configuration for all cross-mode substrate concerns:
102/// resource-server verification and token propagation policy.
103///
104/// `resource_server` is optional at parse time — when absent it defaults to
105/// unconfigured. Call
106/// [`resolve_all`](AccessTokenSubstrateConfigSource::resolve_all) with the OIDC
107/// shared defaults to produce a [`ResolvedAccessTokenSubstrateConfig`].
108#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
109#[derive(Debug, Clone, Deserialize, Default)]
110pub struct AccessTokenSubstrateConfig {
111    /// OAuth resource-server verifier configuration.
112    #[serde(default, flatten)]
113    pub resource_server: OAuthResourceServerConfig,
114
115    /// Token propagation capability axis.
116    #[serde(default)]
117    pub token_propagation: TokenPropagation,
118}
119
120impl AccessTokenSubstrateConfigSource for AccessTokenSubstrateConfig {
121    fn resource_server_config(&self) -> &OAuthResourceServerConfig {
122        &self.resource_server
123    }
124
125    fn token_propagation(&self) -> &TokenPropagation {
126        &self.token_propagation
127    }
128}
129
130// ---------------------------------------------------------------------------
131// Resolved (validated) config
132// ---------------------------------------------------------------------------
133
134/// Validated configuration bundle for the access-token substrate.
135///
136/// Produced by [`AccessTokenSubstrateConfigSource::resolve_all`]. The
137/// `resource_server` field has had OIDC shared defaults applied and passed
138/// validation.
139#[derive(Debug, Clone)]
140pub struct ResolvedAccessTokenSubstrateConfig {
141    /// OAuth resource-server config with shared defaults applied.
142    pub resource_server: OAuthResourceServerConfig,
143    /// Token propagation capability axis (pass-through, no extra validation).
144    pub token_propagation: TokenPropagation,
145}
146
147#[cfg(test)]
148mod tests {
149    use std::sync::{
150        Arc,
151        atomic::{AtomicUsize, Ordering},
152    };
153
154    use securitydept_oauth_provider::{OAuthProviderRemoteConfig, OidcSharedConfig};
155    use securitydept_oauth_resource_server::OAuthResourceServerIntrospectionConfig;
156    use securitydept_utils::secret::SecretString;
157
158    use super::*;
159
160    #[test]
161    fn resolve_all_inherits_shared_defaults() {
162        let shared = OidcSharedConfig {
163            remote: OAuthProviderRemoteConfig {
164                well_known_url: Some(
165                    "https://auth.example.com/.well-known/openid-configuration".to_string(),
166                ),
167                ..Default::default()
168            },
169            client_id: Some("shared-app".to_string()),
170            client_secret: Some(SecretString::from("shared-secret")),
171            ..Default::default()
172        };
173
174        let raw = AccessTokenSubstrateConfig {
175            resource_server: OAuthResourceServerConfig {
176                introspection: Some(OAuthResourceServerIntrospectionConfig::default()),
177                ..Default::default()
178            },
179            ..Default::default()
180        };
181
182        let resolved = raw.resolve_all(Some(&shared)).expect("should resolve");
183        assert_eq!(
184            resolved
185                .resource_server
186                .introspection
187                .as_ref()
188                .unwrap()
189                .client_id
190                .as_deref(),
191            Some("shared-app"),
192            "introspection.client_id should inherit from [oidc]"
193        );
194    }
195
196    #[test]
197    fn resolve_all_without_shared_returns_raw() {
198        let raw = AccessTokenSubstrateConfig {
199            resource_server: OAuthResourceServerConfig::default(),
200            token_propagation: TokenPropagation::Disabled,
201        };
202
203        let resolved = raw
204            .resolve_all(None)
205            .expect("should resolve without shared");
206        assert!(
207            resolved.resource_server.remote.well_known_url.is_none(),
208            "no shared defaults should be applied"
209        );
210        assert!(matches!(
211            resolved.token_propagation,
212            TokenPropagation::Disabled
213        ));
214    }
215
216    #[test]
217    fn resolve_all_propagation_axis_passes_through() {
218        use crate::access_token_substrate::propagation::{
219            PropagationDestinationPolicy, TokenPropagatorConfig,
220        };
221
222        let raw = AccessTokenSubstrateConfig {
223            resource_server: OAuthResourceServerConfig::default(),
224            token_propagation: TokenPropagation::Enabled {
225                config: TokenPropagatorConfig {
226                    destination_policy: PropagationDestinationPolicy {
227                        allowed_targets: vec![],
228                        ..Default::default()
229                    },
230                    ..Default::default()
231                },
232            },
233        };
234
235        let resolved = raw.resolve_all(None).expect("should resolve");
236        assert!(matches!(
237            resolved.token_propagation,
238            TokenPropagation::Enabled { .. }
239        ));
240    }
241
242    #[test]
243    fn resolve_all_with_validator_rejects_raw_config() {
244        struct RejectEnabledPropagation;
245
246        impl AccessTokenSubstrateConfigValidator for RejectEnabledPropagation {
247            fn validate_raw_access_token_substrate_config(
248                &self,
249                config: &AccessTokenSubstrateConfig,
250            ) -> Result<(), AccessTokenSubstrateConfigValidationError> {
251                if matches!(config.token_propagation, TokenPropagation::Enabled { .. }) {
252                    return Err(AccessTokenSubstrateConfigValidationError::new(
253                        "token_propagation",
254                        "disabled_by_host",
255                        "token propagation is disabled by the host",
256                    ));
257                }
258
259                Ok(())
260            }
261        }
262
263        use crate::access_token_substrate::propagation::TokenPropagatorConfig;
264
265        let raw = AccessTokenSubstrateConfig {
266            token_propagation: TokenPropagation::Enabled {
267                config: TokenPropagatorConfig::default(),
268            },
269            ..Default::default()
270        };
271
272        let error = raw
273            .resolve_all_with_validator(None, &RejectEnabledPropagation)
274            .expect_err("validator should reject enabled propagation");
275
276        assert!(matches!(
277            error,
278            BackendConfigError::AccessTokenSubstrateValidation(ref validation)
279                if validation.field_path == "token_propagation"
280                    && validation.code == "disabled_by_host"
281        ));
282    }
283
284    #[test]
285    fn resolve_all_with_validator_accepts_validator_composition() {
286        #[derive(Clone)]
287        struct CountValidator(Arc<AtomicUsize>);
288
289        impl AccessTokenSubstrateConfigValidator for CountValidator {
290            fn validate_raw_access_token_substrate_config(
291                &self,
292                _config: &AccessTokenSubstrateConfig,
293            ) -> Result<(), AccessTokenSubstrateConfigValidationError> {
294                self.0.fetch_add(1, Ordering::SeqCst);
295                Ok(())
296            }
297        }
298
299        let calls = Arc::new(AtomicUsize::new(0));
300        let validators = [
301            CountValidator(Arc::clone(&calls)),
302            CountValidator(Arc::clone(&calls)),
303        ];
304        let raw = AccessTokenSubstrateConfig::default();
305
306        raw.resolve_all_with_validator(None, &validators)
307            .expect("composed validators should pass");
308
309        assert_eq!(calls.load(Ordering::SeqCst), 2);
310    }
311}