Skip to main content

cloud_sdk_reqwest/shared/
scope.rs

1use core::fmt;
2use std::string::String;
3
4use cloud_sdk::authentication::{AuthenticationScope, ScopeValue, ScopeValueError};
5use cloud_sdk::{ProviderId, ServiceId};
6
7use super::{BearerToken, HttpsEndpoint};
8
9/// Owned authentication-scope construction failure.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum BearerCredentialScopeError {
12    /// A provider-owned scope value failed core validation.
13    ValueRejected(ScopeValueError),
14    /// Adapter-owned scope storage could not be allocated.
15    AllocationFailed,
16    /// A previously admitted endpoint identity could not be recovered.
17    EndpointIdentityRejected,
18}
19
20impl fmt::Display for BearerCredentialScopeError {
21    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22        formatter.write_str(match self {
23            Self::ValueRejected(_) => "bearer credential scope value was rejected",
24            Self::AllocationFailed => "bearer credential scope allocation failed",
25            Self::EndpointIdentityRejected => "bearer credential endpoint identity was rejected",
26        })
27    }
28}
29
30impl core::error::Error for BearerCredentialScopeError {
31    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
32        match self {
33            Self::ValueRejected(error) => Some(error),
34            Self::AllocationFailed => None,
35            Self::EndpointIdentityRejected => None,
36        }
37    }
38}
39
40/// Immutable owned scope attached to one bearer-credential lifecycle.
41///
42/// Rotation and refresh replace only the token. They cannot change this scope.
43pub struct BearerCredentialScope {
44    provider: ProviderId,
45    service: ServiceId,
46    endpoint: HttpsEndpoint,
47    audience: Option<String>,
48    account: Option<String>,
49    tenant: Option<String>,
50}
51
52impl BearerCredentialScope {
53    /// Binds a credential to one provider, service, and transport endpoint.
54    #[must_use]
55    pub const fn new(provider: ProviderId, service: ServiceId, endpoint: HttpsEndpoint) -> Self {
56        Self {
57            provider,
58            service,
59            endpoint,
60            audience: None,
61            account: None,
62            tenant: None,
63        }
64    }
65
66    /// Binds a provider-owned audience.
67    pub fn try_with_audience(mut self, value: &str) -> Result<Self, BearerCredentialScopeError> {
68        self.audience = Some(copy_scope_value(value)?);
69        Ok(self)
70    }
71
72    /// Binds a provider-owned account.
73    pub fn try_with_account(mut self, value: &str) -> Result<Self, BearerCredentialScopeError> {
74        self.account = Some(copy_scope_value(value)?);
75        Ok(self)
76    }
77
78    /// Binds a provider-owned tenant.
79    pub fn try_with_tenant(mut self, value: &str) -> Result<Self, BearerCredentialScopeError> {
80        self.tenant = Some(copy_scope_value(value)?);
81        Ok(self)
82    }
83
84    pub(crate) fn borrowed(&self) -> Result<AuthenticationScope<'_>, BearerCredentialScopeError> {
85        self.borrowed_with_endpoint(self.endpoint_identity()?)
86    }
87
88    pub(crate) fn borrowed_with_endpoint<'a>(
89        &'a self,
90        endpoint: cloud_sdk::transport::EndpointIdentity<'a>,
91    ) -> Result<AuthenticationScope<'a>, BearerCredentialScopeError> {
92        let mut scope = AuthenticationScope::unscoped()
93            .with_provider(self.provider)
94            .with_service(self.service)
95            .with_endpoint(endpoint);
96        if let Some(value) = self.audience.as_deref() {
97            scope = scope.with_audience(
98                ScopeValue::new(value).map_err(BearerCredentialScopeError::ValueRejected)?,
99            );
100        }
101        if let Some(value) = self.account.as_deref() {
102            scope = scope.with_account(
103                ScopeValue::new(value).map_err(BearerCredentialScopeError::ValueRejected)?,
104            );
105        }
106        if let Some(value) = self.tenant.as_deref() {
107            scope = scope.with_tenant(
108                ScopeValue::new(value).map_err(BearerCredentialScopeError::ValueRejected)?,
109            );
110        }
111        Ok(scope)
112    }
113
114    pub(crate) const fn provider(&self) -> ProviderId {
115        self.provider
116    }
117
118    pub(crate) const fn service(&self) -> ServiceId {
119        self.service
120    }
121
122    pub(crate) fn endpoint_identity(
123        &self,
124    ) -> Result<cloud_sdk::transport::EndpointIdentity<'_>, BearerCredentialScopeError> {
125        self.endpoint
126            .identity()
127            .map_err(|_| BearerCredentialScopeError::EndpointIdentityRejected)
128    }
129
130    pub(crate) fn matches_endpoint(&self, endpoint: &HttpsEndpoint) -> bool {
131        self.endpoint_identity()
132            .ok()
133            .zip(endpoint.identity().ok())
134            .is_some_and(|(credential, configured)| credential == configured)
135    }
136}
137
138impl fmt::Debug for BearerCredentialScope {
139    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140        formatter
141            .debug_struct("BearerCredentialScope")
142            .field("provider", &self.provider)
143            .field("service", &self.service)
144            .field("endpoint", &"[redacted]")
145            .field("audience", &self.audience.as_ref().map(|_| "[redacted]"))
146            .field("account", &self.account.as_ref().map(|_| "[redacted]"))
147            .field("tenant", &self.tenant.as_ref().map(|_| "[redacted]"))
148            .finish()
149    }
150}
151
152/// Initial bearer token and its immutable authentication scope.
153pub struct BearerCredential {
154    pub(crate) token: BearerToken,
155    pub(crate) scope: BearerCredentialScope,
156}
157
158impl BearerCredential {
159    /// Binds a validated token to an immutable scope.
160    #[must_use]
161    pub const fn new(token: BearerToken, scope: BearerCredentialScope) -> Self {
162        Self { token, scope }
163    }
164}
165
166impl fmt::Debug for BearerCredential {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        formatter.write_str("BearerCredential([redacted])")
169    }
170}
171
172fn copy_scope_value(value: &str) -> Result<String, BearerCredentialScopeError> {
173    ScopeValue::new(value).map_err(BearerCredentialScopeError::ValueRejected)?;
174    let mut owned = String::new();
175    owned
176        .try_reserve_exact(value.len())
177        .map_err(|_| BearerCredentialScopeError::AllocationFailed)?;
178    owned.push_str(value);
179    Ok(owned)
180}
181
182#[cfg(test)]
183mod tests {
184    use cloud_sdk::transport::CustomEndpointAcknowledgement;
185    use cloud_sdk::{ProviderId, ServiceId};
186
187    use super::{BearerCredentialScope, BearerCredentialScopeError, ScopeValueError};
188    use crate::shared::HttpsEndpoint;
189
190    #[test]
191    fn owned_scope_validates_values_and_redacts_all_provider_fields() {
192        let provider = ProviderId::new("example").unwrap_or_else(|_| unreachable!());
193        let service = ServiceId::new("compute").unwrap_or_else(|_| unreachable!());
194        let endpoint = HttpsEndpoint::new_custom(
195            "https://api.example.test/v1",
196            CustomEndpointAcknowledgement::trusted_operator_configuration(),
197        )
198        .unwrap_or_else(|_| unreachable!());
199        let scope = BearerCredentialScope::new(provider, service, endpoint)
200            .try_with_audience("secret-audience")
201            .and_then(|scope| scope.try_with_account("secret-account"))
202            .and_then(|scope| scope.try_with_tenant("secret-tenant"));
203        assert!(scope.is_ok());
204        if let Ok(scope) = scope {
205            let debug = std::format!("{scope:?}");
206            assert!(debug.contains("[redacted]"));
207            assert!(!debug.contains("secret-audience"));
208            assert!(!debug.contains("secret-account"));
209            assert!(!debug.contains("secret-tenant"));
210            assert!(scope.borrowed().is_ok());
211        }
212    }
213
214    #[test]
215    fn invalid_scope_value_does_not_create_partial_owned_scope() {
216        let provider = ProviderId::new("example").unwrap_or_else(|_| unreachable!());
217        let service = ServiceId::new("compute").unwrap_or_else(|_| unreachable!());
218        let endpoint = HttpsEndpoint::new_custom(
219            "https://api.example.test/v1",
220            CustomEndpointAcknowledgement::trusted_operator_configuration(),
221        )
222        .unwrap_or_else(|_| unreachable!());
223        let result = BearerCredentialScope::new(provider, service, endpoint)
224            .try_with_audience("contains space");
225        assert!(matches!(
226            result,
227            Err(BearerCredentialScopeError::ValueRejected(
228                ScopeValueError::InvalidByte
229            ))
230        ));
231    }
232}