Skip to main content

cloud_sdk/authentication/
policy.rs

1use core::fmt;
2
3use crate::transport::{EndpointIdentity, EndpointScheme};
4use crate::{ProviderId, ServiceId};
5
6use super::ScopeValue;
7
8/// Provider or operation requirement for one authentication-scope field.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum ScopeRequirement<T> {
11    /// The credential must contain the exact expected value.
12    Required(T),
13    /// The credential may omit the field; a supplied value must match.
14    Optional(T),
15    /// The credential must omit the field.
16    Forbidden,
17}
18
19/// Immutable borrowed scope bound to one admitted credential.
20#[derive(Clone, Copy)]
21pub struct AuthenticationScope<'a> {
22    provider: Option<ProviderId>,
23    service: Option<ServiceId>,
24    endpoint: Option<EndpointIdentity<'a>>,
25    audience: Option<ScopeValue<'a>>,
26    account: Option<ScopeValue<'a>>,
27    tenant: Option<ScopeValue<'a>>,
28}
29
30impl<'a> AuthenticationScope<'a> {
31    /// Creates a completely unscoped credential identity.
32    ///
33    /// A policy with any required field rejects this value.
34    #[must_use]
35    pub const fn unscoped() -> Self {
36        Self {
37            provider: None,
38            service: None,
39            endpoint: None,
40            audience: None,
41            account: None,
42            tenant: None,
43        }
44    }
45
46    /// Binds a provider.
47    #[must_use]
48    pub const fn with_provider(mut self, value: ProviderId) -> Self {
49        self.provider = Some(value);
50        self
51    }
52
53    /// Binds a provider-owned service.
54    #[must_use]
55    pub const fn with_service(mut self, value: ServiceId) -> Self {
56        self.service = Some(value);
57        self
58    }
59
60    /// Binds a normalized endpoint identity.
61    #[must_use]
62    pub const fn with_endpoint(mut self, value: EndpointIdentity<'a>) -> Self {
63        self.endpoint = Some(value);
64        self
65    }
66
67    /// Binds a provider-owned audience.
68    #[must_use]
69    pub const fn with_audience(mut self, value: ScopeValue<'a>) -> Self {
70        self.audience = Some(value);
71        self
72    }
73
74    /// Binds a provider-owned account.
75    #[must_use]
76    pub const fn with_account(mut self, value: ScopeValue<'a>) -> Self {
77        self.account = Some(value);
78        self
79    }
80
81    /// Binds a provider-owned tenant.
82    #[must_use]
83    pub const fn with_tenant(mut self, value: ScopeValue<'a>) -> Self {
84        self.tenant = Some(value);
85        self
86    }
87}
88
89impl fmt::Debug for AuthenticationScope<'_> {
90    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91        formatter
92            .debug_struct("AuthenticationScope")
93            .field("provider", &self.provider)
94            .field("service", &self.service)
95            .field("endpoint", &self.endpoint)
96            .field("audience", &self.audience.map(|_| "[redacted]"))
97            .field("account", &self.account.map(|_| "[redacted]"))
98            .field("tenant", &self.tenant.map(|_| "[redacted]"))
99            .finish()
100    }
101}
102
103/// Complete provider or operation-owned authentication-scope policy.
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub struct AuthenticationScopePolicy<'a> {
106    provider: ScopeRequirement<ProviderId>,
107    service: ScopeRequirement<ServiceId>,
108    endpoint: ScopeRequirement<EndpointIdentity<'a>>,
109    audience: ScopeRequirement<ScopeValue<'a>>,
110    account: ScopeRequirement<ScopeValue<'a>>,
111    tenant: ScopeRequirement<ScopeValue<'a>>,
112}
113
114impl<'a> AuthenticationScopePolicy<'a> {
115    /// Creates a policy with an explicit rule for every scope field.
116    #[must_use]
117    pub const fn new(
118        provider: ScopeRequirement<ProviderId>,
119        service: ScopeRequirement<ServiceId>,
120        endpoint: ScopeRequirement<EndpointIdentity<'a>>,
121        audience: ScopeRequirement<ScopeValue<'a>>,
122        account: ScopeRequirement<ScopeValue<'a>>,
123        tenant: ScopeRequirement<ScopeValue<'a>>,
124    ) -> Self {
125        Self {
126            provider,
127            service,
128            endpoint,
129            audience,
130            account,
131            tenant,
132        }
133    }
134
135    /// Returns the provider rule for adapter identity verification.
136    #[must_use]
137    pub const fn provider_requirement(self) -> ScopeRequirement<ProviderId> {
138        self.provider
139    }
140
141    /// Returns the service rule for adapter identity verification.
142    #[must_use]
143    pub const fn service_requirement(self) -> ScopeRequirement<ServiceId> {
144        self.service
145    }
146
147    /// Returns the endpoint rule for adapter destination verification.
148    #[must_use]
149    pub const fn endpoint_requirement(self) -> ScopeRequirement<EndpointIdentity<'a>> {
150        self.endpoint
151    }
152
153    /// Returns the audience rule for adapter identity verification.
154    #[must_use]
155    pub const fn audience_requirement(self) -> ScopeRequirement<ScopeValue<'a>> {
156        self.audience
157    }
158
159    /// Returns the account rule for adapter identity verification.
160    #[must_use]
161    pub const fn account_requirement(self) -> ScopeRequirement<ScopeValue<'a>> {
162        self.account
163    }
164
165    /// Returns the tenant rule for adapter identity verification.
166    #[must_use]
167    pub const fn tenant_requirement(self) -> ScopeRequirement<ScopeValue<'a>> {
168        self.tenant
169    }
170
171    /// Validates one credential scope before authorization-header construction.
172    pub fn validate(self, scope: AuthenticationScope<'a>) -> Result<(), AuthenticationScopeError> {
173        validate_endpoint_security(self.endpoint)?;
174        validate_field(scope.provider, self.provider, ScopeField::Provider)?;
175        validate_field(scope.service, self.service, ScopeField::Service)?;
176        validate_field(scope.endpoint, self.endpoint, ScopeField::Endpoint)?;
177        validate_field(scope.audience, self.audience, ScopeField::Audience)?;
178        validate_field(scope.account, self.account, ScopeField::Account)?;
179        validate_field(scope.tenant, self.tenant, ScopeField::Tenant)
180    }
181}
182
183/// Authentication-scope field that failed policy validation.
184#[derive(Clone, Copy, Debug, Eq, PartialEq)]
185pub enum ScopeField {
186    /// Provider namespace.
187    Provider,
188    /// Provider-owned service namespace.
189    Service,
190    /// Normalized endpoint identity.
191    Endpoint,
192    /// Provider-owned audience.
193    Audience,
194    /// Provider-owned account.
195    Account,
196    /// Provider-owned tenant.
197    Tenant,
198}
199
200/// Payload-free reason a scope field failed validation.
201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
202pub enum ScopeViolation {
203    /// A required field was omitted.
204    MissingRequired,
205    /// A forbidden field was supplied.
206    SuppliedForbidden,
207    /// A supplied field did not equal the expected value.
208    Mismatch,
209    /// An endpoint policy expected a non-HTTPS identity.
210    InsecureEndpoint,
211}
212
213/// Payload-free authentication-scope policy failure.
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub struct AuthenticationScopeError {
216    field: ScopeField,
217    violation: ScopeViolation,
218}
219
220impl AuthenticationScopeError {
221    /// Returns the rejected field without exposing its value.
222    #[must_use]
223    pub const fn field(self) -> ScopeField {
224        self.field
225    }
226
227    /// Returns the payload-free rejection category.
228    #[must_use]
229    pub const fn violation(self) -> ScopeViolation {
230        self.violation
231    }
232}
233
234impl fmt::Display for AuthenticationScopeError {
235    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
236        formatter.write_str(match self.violation {
237            ScopeViolation::MissingRequired => "required authentication scope field is missing",
238            ScopeViolation::SuppliedForbidden => {
239                "forbidden authentication scope field was supplied"
240            }
241            ScopeViolation::Mismatch => "authentication scope field does not match policy",
242            ScopeViolation::InsecureEndpoint => "authentication endpoint policy is not HTTPS",
243        })
244    }
245}
246
247impl core::error::Error for AuthenticationScopeError {}
248
249fn validate_field<T: Copy + Eq>(
250    actual: Option<T>,
251    requirement: ScopeRequirement<T>,
252    field: ScopeField,
253) -> Result<(), AuthenticationScopeError> {
254    let violation = match (actual, requirement) {
255        (None, ScopeRequirement::Required(_)) => Some(ScopeViolation::MissingRequired),
256        (Some(_), ScopeRequirement::Forbidden) => Some(ScopeViolation::SuppliedForbidden),
257        (
258            Some(actual),
259            ScopeRequirement::Required(expected) | ScopeRequirement::Optional(expected),
260        ) if actual != expected => Some(ScopeViolation::Mismatch),
261        _ => None,
262    };
263    violation.map_or(Ok(()), |violation| {
264        Err(AuthenticationScopeError { field, violation })
265    })
266}
267
268fn validate_endpoint_security(
269    requirement: ScopeRequirement<EndpointIdentity<'_>>,
270) -> Result<(), AuthenticationScopeError> {
271    let expected = match requirement {
272        ScopeRequirement::Required(value) | ScopeRequirement::Optional(value) => value,
273        ScopeRequirement::Forbidden => return Ok(()),
274    };
275    if expected.scheme() == EndpointScheme::Https {
276        Ok(())
277    } else {
278        Err(AuthenticationScopeError {
279            field: ScopeField::Endpoint,
280            violation: ScopeViolation::InsecureEndpoint,
281        })
282    }
283}