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