Skip to main content

cloud_sdk_reqwest/shared/
scope.rs

1use core::fmt;
2use std::string::String;
3
4use cloud_sdk::authentication::{
5    AuthenticationScope, CredentialLifetime, ScopeValue, ScopeValueError,
6};
7use cloud_sdk::transport::EndpointIdentity;
8use cloud_sdk::{ProviderId, ServiceId};
9
10use super::{BearerToken, HttpsEndpoint};
11
12/// Owned authentication-scope construction failure.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum CredentialScopeError {
15    /// A provider-owned scope value failed core validation.
16    ValueRejected(ScopeValueError),
17    /// Adapter-owned scope storage could not be allocated.
18    AllocationFailed,
19    /// A previously admitted endpoint identity could not be recovered.
20    EndpointIdentityRejected,
21}
22
23impl fmt::Display for CredentialScopeError {
24    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25        formatter.write_str(match self {
26            Self::ValueRejected(_) => "credential scope value was rejected",
27            Self::AllocationFailed => "credential scope allocation failed",
28            Self::EndpointIdentityRejected => "credential endpoint identity was rejected",
29        })
30    }
31}
32
33impl core::error::Error for CredentialScopeError {
34    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
35        match self {
36            Self::ValueRejected(error) => Some(error),
37            Self::AllocationFailed | Self::EndpointIdentityRejected => None,
38        }
39    }
40}
41
42/// Compatibility name for bearer-scope construction failures.
43pub type BearerCredentialScopeError = CredentialScopeError;
44/// Basic-scope construction failure.
45pub type BasicCredentialScopeError = CredentialScopeError;
46
47struct OwnedCredentialScope {
48    provider: ProviderId,
49    service: ServiceId,
50    endpoint: HttpsEndpoint,
51    audience: Option<String>,
52    account: Option<String>,
53    tenant: Option<String>,
54}
55
56impl OwnedCredentialScope {
57    const fn new(provider: ProviderId, service: ServiceId, endpoint: HttpsEndpoint) -> Self {
58        Self {
59            provider,
60            service,
61            endpoint,
62            audience: None,
63            account: None,
64            tenant: None,
65        }
66    }
67
68    fn try_with_audience(mut self, value: &str) -> Result<Self, CredentialScopeError> {
69        self.audience = Some(copy_scope_value(value)?);
70        Ok(self)
71    }
72
73    fn try_with_account(mut self, value: &str) -> Result<Self, CredentialScopeError> {
74        self.account = Some(copy_scope_value(value)?);
75        Ok(self)
76    }
77
78    fn try_with_tenant(mut self, value: &str) -> Result<Self, CredentialScopeError> {
79        self.tenant = Some(copy_scope_value(value)?);
80        Ok(self)
81    }
82
83    fn borrowed(&self) -> Result<AuthenticationScope<'_>, CredentialScopeError> {
84        self.borrowed_with_endpoint(self.endpoint_identity()?)
85    }
86
87    fn borrowed_with_endpoint<'a>(
88        &'a self,
89        endpoint: EndpointIdentity<'a>,
90    ) -> Result<AuthenticationScope<'a>, CredentialScopeError> {
91        let mut scope = AuthenticationScope::unscoped()
92            .with_provider(self.provider)
93            .with_service(self.service)
94            .with_endpoint(endpoint);
95        if let Some(value) = self.audience.as_deref() {
96            scope = scope.with_audience(
97                ScopeValue::new(value).map_err(CredentialScopeError::ValueRejected)?,
98            );
99        }
100        if let Some(value) = self.account.as_deref() {
101            scope = scope
102                .with_account(ScopeValue::new(value).map_err(CredentialScopeError::ValueRejected)?);
103        }
104        if let Some(value) = self.tenant.as_deref() {
105            scope = scope
106                .with_tenant(ScopeValue::new(value).map_err(CredentialScopeError::ValueRejected)?);
107        }
108        Ok(scope)
109    }
110
111    fn endpoint_identity(&self) -> Result<EndpointIdentity<'_>, CredentialScopeError> {
112        self.endpoint
113            .identity()
114            .map_err(|_| CredentialScopeError::EndpointIdentityRejected)
115    }
116
117    fn matches_endpoint(&self, endpoint: &HttpsEndpoint) -> bool {
118        self.endpoint_identity()
119            .ok()
120            .zip(endpoint.identity().ok())
121            .is_some_and(|(credential, configured)| credential == configured)
122    }
123}
124
125pub(crate) trait CredentialScopeView {
126    fn provider(&self) -> ProviderId;
127    fn service(&self) -> ServiceId;
128    fn endpoint_identity(&self) -> Result<EndpointIdentity<'_>, CredentialScopeError>;
129    fn borrowed(&self) -> Result<AuthenticationScope<'_>, CredentialScopeError>;
130    fn borrowed_with_endpoint<'a>(
131        &'a self,
132        endpoint: EndpointIdentity<'a>,
133    ) -> Result<AuthenticationScope<'a>, CredentialScopeError>;
134}
135
136macro_rules! define_scope {
137    ($name:ident, $label:literal) => {
138        /// Immutable owned scope attached to one credential lifecycle.
139        pub struct $name {
140            inner: OwnedCredentialScope,
141        }
142
143        impl $name {
144            /// Binds a credential to one provider, service, and transport endpoint.
145            #[must_use]
146            pub const fn new(
147                provider: ProviderId,
148                service: ServiceId,
149                endpoint: HttpsEndpoint,
150            ) -> Self {
151                Self {
152                    inner: OwnedCredentialScope::new(provider, service, endpoint),
153                }
154            }
155
156            /// Binds a provider-owned audience.
157            pub fn try_with_audience(mut self, value: &str) -> Result<Self, CredentialScopeError> {
158                self.inner = self.inner.try_with_audience(value)?;
159                Ok(self)
160            }
161
162            /// Binds a provider-owned account.
163            pub fn try_with_account(mut self, value: &str) -> Result<Self, CredentialScopeError> {
164                self.inner = self.inner.try_with_account(value)?;
165                Ok(self)
166            }
167
168            /// Binds a provider-owned tenant.
169            pub fn try_with_tenant(mut self, value: &str) -> Result<Self, CredentialScopeError> {
170                self.inner = self.inner.try_with_tenant(value)?;
171                Ok(self)
172            }
173
174            pub(crate) const fn provider(&self) -> ProviderId {
175                self.inner.provider
176            }
177
178            pub(crate) const fn service(&self) -> ServiceId {
179                self.inner.service
180            }
181
182            pub(crate) fn endpoint_identity(
183                &self,
184            ) -> Result<EndpointIdentity<'_>, CredentialScopeError> {
185                self.inner.endpoint_identity()
186            }
187
188            pub(crate) fn borrowed(&self) -> Result<AuthenticationScope<'_>, CredentialScopeError> {
189                self.inner.borrowed()
190            }
191
192            pub(crate) fn borrowed_with_endpoint<'a>(
193                &'a self,
194                endpoint: EndpointIdentity<'a>,
195            ) -> Result<AuthenticationScope<'a>, CredentialScopeError> {
196                self.inner.borrowed_with_endpoint(endpoint)
197            }
198
199            pub(crate) fn matches_endpoint(&self, endpoint: &HttpsEndpoint) -> bool {
200                self.inner.matches_endpoint(endpoint)
201            }
202        }
203
204        impl CredentialScopeView for $name {
205            fn provider(&self) -> ProviderId {
206                self.provider()
207            }
208
209            fn service(&self) -> ServiceId {
210                self.service()
211            }
212
213            fn endpoint_identity(&self) -> Result<EndpointIdentity<'_>, CredentialScopeError> {
214                self.endpoint_identity()
215            }
216
217            fn borrowed(&self) -> Result<AuthenticationScope<'_>, CredentialScopeError> {
218                self.borrowed()
219            }
220
221            fn borrowed_with_endpoint<'a>(
222                &'a self,
223                endpoint: EndpointIdentity<'a>,
224            ) -> Result<AuthenticationScope<'a>, CredentialScopeError> {
225                self.borrowed_with_endpoint(endpoint)
226            }
227        }
228
229        impl fmt::Debug for $name {
230            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
231                formatter
232                    .debug_struct($label)
233                    .field("provider", &self.inner.provider)
234                    .field("service", &self.inner.service)
235                    .field("endpoint", &"[redacted]")
236                    .field(
237                        "audience",
238                        &self.inner.audience.as_ref().map(|_| "[redacted]"),
239                    )
240                    .field(
241                        "account",
242                        &self.inner.account.as_ref().map(|_| "[redacted]"),
243                    )
244                    .field("tenant", &self.inner.tenant.as_ref().map(|_| "[redacted]"))
245                    .finish()
246            }
247        }
248    };
249}
250
251define_scope!(BearerCredentialScope, "BearerCredentialScope");
252define_scope!(BasicCredentialScope, "BasicCredentialScope");
253
254/// Initial bearer token and its immutable authentication scope.
255pub struct BearerCredential {
256    pub(crate) token: BearerToken,
257    pub(crate) scope: BearerCredentialScope,
258    pub(crate) lifetime: Option<CredentialLifetime>,
259}
260
261impl BearerCredential {
262    /// Binds a validated token to an immutable scope.
263    #[must_use]
264    pub const fn new(token: BearerToken, scope: BearerCredentialScope) -> Self {
265        Self {
266            token,
267            scope,
268            lifetime: None,
269        }
270    }
271
272    /// Binds an expiring token and its caller-clock lifetime to one scope.
273    #[must_use]
274    pub const fn new_expiring(
275        token: BearerToken,
276        scope: BearerCredentialScope,
277        lifetime: CredentialLifetime,
278    ) -> Self {
279        Self {
280            token,
281            scope,
282            lifetime: Some(lifetime),
283        }
284    }
285}
286
287impl fmt::Debug for BearerCredential {
288    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
289        formatter.write_str("BearerCredential([redacted])")
290    }
291}
292
293fn copy_scope_value(value: &str) -> Result<String, CredentialScopeError> {
294    ScopeValue::new(value).map_err(CredentialScopeError::ValueRejected)?;
295    let mut owned = String::new();
296    owned
297        .try_reserve_exact(value.len())
298        .map_err(|_| CredentialScopeError::AllocationFailed)?;
299    owned.push_str(value);
300    Ok(owned)
301}
302
303#[cfg(test)]
304mod tests {
305    use cloud_sdk::transport::CustomEndpointAcknowledgement;
306    use cloud_sdk::{ProviderId, ServiceId};
307
308    use super::{BasicCredentialScope, CredentialScopeError, ScopeValueError};
309    use crate::shared::HttpsEndpoint;
310
311    #[test]
312    fn owned_scope_validates_values_and_redacts_all_provider_fields() {
313        let provider = ProviderId::new("example").unwrap_or_else(|_| unreachable!());
314        let service = ServiceId::new("compute").unwrap_or_else(|_| unreachable!());
315        let endpoint = HttpsEndpoint::new_custom(
316            "https://api.example.test/v1",
317            CustomEndpointAcknowledgement::trusted_operator_configuration(),
318        )
319        .unwrap_or_else(|_| unreachable!());
320        let scope = BasicCredentialScope::new(provider, service, endpoint)
321            .try_with_audience("secret-audience")
322            .and_then(|scope| scope.try_with_account("secret-account"))
323            .and_then(|scope| scope.try_with_tenant("secret-tenant"));
324        assert!(scope.is_ok());
325        let Ok(scope) = scope else {
326            unreachable!("security fixture construction failed");
327        };
328        let debug = std::format!("{scope:?}");
329        assert!(debug.contains("[redacted]"));
330        assert!(!debug.contains("secret-audience"));
331        assert!(!debug.contains("secret-account"));
332        assert!(!debug.contains("secret-tenant"));
333        assert!(scope.borrowed().is_ok());
334    }
335
336    #[test]
337    fn invalid_scope_value_does_not_create_partial_owned_scope() {
338        let provider = ProviderId::new("example").unwrap_or_else(|_| unreachable!());
339        let service = ServiceId::new("compute").unwrap_or_else(|_| unreachable!());
340        let endpoint = HttpsEndpoint::new_custom(
341            "https://api.example.test/v1",
342            CustomEndpointAcknowledgement::trusted_operator_configuration(),
343        )
344        .unwrap_or_else(|_| unreachable!());
345        let result =
346            BasicCredentialScope::new(provider, service, endpoint).try_with_audience("has space");
347        assert!(matches!(
348            result,
349            Err(CredentialScopeError::ValueRejected(
350                ScopeValueError::InvalidByte
351            ))
352        ));
353    }
354}