Skip to main content

securitydept_token_set_context/access_token_substrate/propagation/
config.rs

1use regex::Regex;
2use serde::{Deserialize, Serialize};
3use typed_builder::TypedBuilder;
4
5/// Controls how a validated upstream bearer token may be propagated downstream.
6#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "snake_case")]
9pub enum BearerPropagationPolicy {
10    /// Forward the original bearer token only after destination and token
11    /// checks pass.
12    ValidateThenForward,
13    /**
14     * not implemented yet, planned for future
15     * */
16    /// Exchange the upstream token for a downstream-specific token before
17    /// calling the target.
18    ExchangeForDownstreamToken,
19}
20
21pub(crate) fn default_bearer_propagation_policy() -> BearerPropagationPolicy {
22    BearerPropagationPolicy::ValidateThenForward
23}
24
25fn default_true() -> bool {
26    true
27}
28
29/// Server-side token propagation configuration.
30#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TypedBuilder)]
32pub struct TokenPropagatorConfig {
33    /// Default propagation policy applied by the server.
34    #[builder(default = BearerPropagationPolicy::ValidateThenForward)]
35    #[serde(default = "default_bearer_propagation_policy")]
36    pub default_policy: BearerPropagationPolicy,
37    /// Explicit destination allowlist for direct bearer forwarding.
38    #[builder(default)]
39    #[serde(default)]
40    pub destination_policy: PropagationDestinationPolicy,
41    /// Additional token claim checks required before forwarding.
42    #[builder(default)]
43    #[serde(default)]
44    pub token_validation: PropagatedTokenValidationConfig,
45}
46
47impl Default for TokenPropagatorConfig {
48    fn default() -> Self {
49        Self {
50            default_policy: default_bearer_propagation_policy(),
51            destination_policy: PropagationDestinationPolicy::default(),
52            token_validation: PropagatedTokenValidationConfig::default(),
53        }
54    }
55}
56
57/// Allowlist and safety guards for downstream targets.
58#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TypedBuilder)]
60pub struct PropagationDestinationPolicy {
61    /// Stable service identities that may receive forwarded credentials.
62    #[builder(default)]
63    #[serde(default)]
64    pub allowed_node_ids: Vec<String>,
65    /// Explicit network targets that may receive forwarded credentials.
66    #[builder(default)]
67    #[serde(default)]
68    pub allowed_targets: Vec<AllowedPropagationTarget>,
69    /// Reject direct IP-literal targets for loopback/private/link-local style
70    /// addresses unless they are explicitly allowed by a matching CIDR
71    /// rule.
72    #[builder(default = true)]
73    #[serde(default = "default_true")]
74    pub deny_sensitive_ip_literals: bool,
75    /// Require callers that build targets from URLs to provide an explicit
76    /// port.
77    #[builder(default = true)]
78    #[serde(default = "default_true")]
79    pub require_explicit_port: bool,
80}
81
82/// Normalized scheme used for downstream propagation rules.
83#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85#[serde(rename_all = "snake_case")]
86pub enum PropagationScheme {
87    Https,
88    Http,
89}
90
91impl PropagationScheme {
92    pub fn as_str(&self) -> &'static str {
93        match self {
94            Self::Https => "https",
95            Self::Http => "http",
96        }
97    }
98}
99
100/// A single downstream target allowlist rule.
101#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(tag = "kind", rename_all = "snake_case")]
104pub enum AllowedPropagationTarget {
105    /// Match one exact origin tuple.
106    ExactOrigin {
107        scheme: PropagationScheme,
108        hostname: String,
109        port: u16,
110    },
111    /// Match one domain suffix such as `mesh.internal.example.com`.
112    DomainSuffix {
113        scheme: PropagationScheme,
114        domain_suffix: String,
115        port: u16,
116    },
117    /// Match domains with a compiled regex. Serialized with `serde_regex`.
118    DomainRegex {
119        scheme: PropagationScheme,
120        #[serde(with = "serde_regex")]
121        #[cfg_attr(feature = "config-schema", schemars(with = "String"))]
122        domain_regex: Regex,
123        port: u16,
124    },
125    /// Match IP-literal targets inside the configured CIDR.
126    Cidr {
127        scheme: PropagationScheme,
128        cidr: String,
129        port: u16,
130    },
131}
132
133impl PartialEq for AllowedPropagationTarget {
134    fn eq(&self, other: &Self) -> bool {
135        match (self, other) {
136            (
137                Self::ExactOrigin {
138                    scheme: left_scheme,
139                    hostname: left_hostname,
140                    port: left_port,
141                },
142                Self::ExactOrigin {
143                    scheme: right_scheme,
144                    hostname: right_hostname,
145                    port: right_port,
146                },
147            ) => {
148                left_scheme == right_scheme
149                    && left_hostname == right_hostname
150                    && left_port == right_port
151            }
152            (
153                Self::DomainSuffix {
154                    scheme: left_scheme,
155                    domain_suffix: left_suffix,
156                    port: left_port,
157                },
158                Self::DomainSuffix {
159                    scheme: right_scheme,
160                    domain_suffix: right_suffix,
161                    port: right_port,
162                },
163            ) => {
164                left_scheme == right_scheme
165                    && left_suffix == right_suffix
166                    && left_port == right_port
167            }
168            (
169                Self::DomainRegex {
170                    scheme: left_scheme,
171                    domain_regex: left_regex,
172                    port: left_port,
173                },
174                Self::DomainRegex {
175                    scheme: right_scheme,
176                    domain_regex: right_regex,
177                    port: right_port,
178                },
179            ) => {
180                left_scheme == right_scheme
181                    && left_regex.as_str() == right_regex.as_str()
182                    && left_port == right_port
183            }
184            (
185                Self::Cidr {
186                    scheme: left_scheme,
187                    cidr: left_cidr,
188                    port: left_port,
189                },
190                Self::Cidr {
191                    scheme: right_scheme,
192                    cidr: right_cidr,
193                    port: right_port,
194                },
195            ) => left_scheme == right_scheme && left_cidr == right_cidr && left_port == right_port,
196            _ => false,
197        }
198    }
199}
200
201impl Eq for AllowedPropagationTarget {}
202
203/// Additional token constraints evaluated before a bearer token may be
204/// forwarded.
205#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
206#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TypedBuilder)]
207pub struct PropagatedTokenValidationConfig {
208    /// Allowed issuers for the upstream token source.
209    #[builder(default)]
210    #[serde(default)]
211    pub required_issuers: Vec<String>,
212    /// At least one audience must match when this list is not empty.
213    #[builder(default)]
214    #[serde(default)]
215    pub allowed_audiences: Vec<String>,
216    /// Every listed scope must be present when this list is not empty.
217    #[builder(default)]
218    #[serde(default)]
219    pub required_scopes: Vec<String>,
220    /// Allowed authorized-party values when this list is not empty.
221    #[builder(default)]
222    #[serde(default)]
223    pub allowed_azp: Vec<String>,
224}