cloud_sdk_reqwest/shared/
scope.rs1use 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum CredentialScopeError {
13 ValueRejected(ScopeValueError),
15 AllocationFailed,
17 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
40pub type BearerCredentialScopeError = CredentialScopeError;
42pub 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 pub struct $name {
138 inner: OwnedCredentialScope,
139 }
140
141 impl $name {
142 #[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 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 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 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
252pub struct BearerCredential {
254 pub(crate) token: BearerToken,
255 pub(crate) scope: BearerCredentialScope,
256}
257
258impl BearerCredential {
259 #[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}