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