cloud_sdk_reqwest/shared/
scope.rs1use 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum BearerCredentialScopeError {
12 ValueRejected(ScopeValueError),
14 AllocationFailed,
16 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
40pub 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 #[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 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 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 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
152pub struct BearerCredential {
154 pub(crate) token: BearerToken,
155 pub(crate) scope: BearerCredentialScope,
156}
157
158impl BearerCredential {
159 #[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}