cloud_sdk_reqwest/shared/
scope.rs1use 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum CredentialScopeError {
15 ValueRejected(ScopeValueError),
17 AllocationFailed,
19 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
42pub type BearerCredentialScopeError = CredentialScopeError;
44pub 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 pub struct $name {
140 inner: OwnedCredentialScope,
141 }
142
143 impl $name {
144 #[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 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 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 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
254pub struct BearerCredential {
256 pub(crate) token: BearerToken,
257 pub(crate) scope: BearerCredentialScope,
258 pub(crate) lifetime: Option<CredentialLifetime>,
259}
260
261impl BearerCredential {
262 #[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 #[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}