Skip to main content

cloud_sdk_reqwest/blocking/
client.rs

1use core::fmt;
2use std::sync::Arc;
3
4use cloud_sdk::authentication::{
5    AuthenticatedRequest, BlockingAuthenticatedTransport, CredentialGeneration, CredentialLifetime,
6};
7use cloud_sdk::transport::{
8    BoundTransport, EndpointIdentity, EndpointIdentityError, ResponseStorageSanitizer,
9    ResponseWriter, TransportFailure,
10};
11use cloud_sdk_sanitization::{SecretBuffer, sanitize_bytes};
12
13use super::RawBlockingClient;
14use crate::shared::{
15    AuthenticatedTransportFailure, BearerCredential, BearerCredentialScope,
16    BearerCredentialSnapshot, BearerRefreshHandoff, BearerToken, CredentialStateError,
17    CredentialStore, CredentialUpdateError, HttpsEndpoint, TokenRefreshError, TokenRotationError,
18    TransportError, map_authentication_error, validate_bearer_authentication,
19};
20
21/// Hardened provider-neutral reqwest blocking bearer transport.
22#[derive(Clone)]
23pub struct BlockingClient {
24    client: RawBlockingClient,
25    endpoint: HttpsEndpoint,
26    scope: Arc<BearerCredentialScope>,
27    credentials: Arc<CredentialStore>,
28    allow_insecure_loopback: bool,
29}
30
31impl BlockingClient {
32    pub(super) fn new(
33        client: RawBlockingClient,
34        endpoint: HttpsEndpoint,
35        credential: BearerCredential,
36        allow_insecure_loopback: bool,
37    ) -> Self {
38        Self {
39            client,
40            endpoint,
41            scope: Arc::new(credential.scope),
42            credentials: Arc::new(CredentialStore::new(credential.token, credential.lifetime)),
43            allow_insecure_loopback,
44        }
45    }
46
47    /// Captures the current generation without exposing token bytes.
48    pub fn credential_snapshot(&self) -> Result<BearerCredentialSnapshot, CredentialStateError> {
49        self.credentials.snapshot()
50    }
51
52    /// Atomically replaces the bearer token used by newly started requests.
53    ///
54    /// In-flight requests retain their previous snapshot. The immutable scope
55    /// cannot change during rotation.
56    pub fn rotate_bearer_token(
57        &self,
58        replacement: BearerToken,
59    ) -> Result<CredentialGeneration, CredentialUpdateError> {
60        self.credentials.rotate(replacement)
61    }
62
63    /// Atomically replaces an expiring token and its complete lifetime.
64    pub fn rotate_bearer_token_with_lifetime(
65        &self,
66        replacement: BearerToken,
67        lifetime: CredentialLifetime,
68    ) -> Result<CredentialGeneration, CredentialUpdateError> {
69        self.credentials.rotate_with_lifetime(replacement, lifetime)
70    }
71
72    /// Validates and rotates mutable bytes, clearing the complete source.
73    pub fn rotate_bearer_token_from_mut_bytes(
74        &self,
75        source: &mut [u8],
76    ) -> Result<CredentialGeneration, TokenRotationError> {
77        self.credentials.rotate_from_mut_bytes(source)
78    }
79
80    /// Validates and rotates guarded storage, which clears on return.
81    pub fn rotate_bearer_token_from_secret_buffer(
82        &self,
83        source: SecretBuffer<'_>,
84    ) -> Result<CredentialGeneration, TokenRotationError> {
85        self.credentials.rotate_from_secret_buffer(source)
86    }
87
88    /// Clears mutable input and atomically installs an expiring replacement.
89    pub fn rotate_bearer_token_from_mut_bytes_with_lifetime(
90        &self,
91        source: &mut [u8],
92        lifetime: CredentialLifetime,
93    ) -> Result<CredentialGeneration, TokenRotationError> {
94        self.credentials
95            .rotate_from_mut_bytes_with_lifetime(source, lifetime)
96    }
97
98    /// Consumes guarded input and atomically installs an expiring replacement.
99    pub fn rotate_bearer_token_from_secret_buffer_with_lifetime(
100        &self,
101        source: SecretBuffer<'_>,
102        lifetime: CredentialLifetime,
103    ) -> Result<CredentialGeneration, TokenRotationError> {
104        self.credentials
105            .rotate_from_secret_buffer_with_lifetime(source, lifetime)
106    }
107
108    /// Installs a refresh only if its captured generation is still current.
109    pub fn refresh_bearer_token(
110        &self,
111        handoff: BearerRefreshHandoff,
112        replacement: BearerToken,
113    ) -> Result<CredentialGeneration, TokenRefreshError> {
114        self.credentials.refresh(handoff, replacement)
115    }
116
117    /// Installs an expiring refresh if its time-qualified handoff is current.
118    pub fn refresh_bearer_token_with_lifetime(
119        &self,
120        handoff: BearerRefreshHandoff,
121        replacement: BearerToken,
122        lifetime: CredentialLifetime,
123    ) -> Result<CredentialGeneration, TokenRefreshError> {
124        self.credentials
125            .refresh_with_lifetime(handoff, replacement, lifetime)
126    }
127
128    /// Validates refreshed mutable bytes, clears them, and rejects stale work.
129    pub fn refresh_bearer_token_from_mut_bytes(
130        &self,
131        handoff: BearerRefreshHandoff,
132        source: &mut [u8],
133    ) -> Result<CredentialGeneration, TokenRefreshError> {
134        self.credentials.refresh_from_mut_bytes(handoff, source)
135    }
136
137    /// Consumes guarded refreshed storage and rejects stale work.
138    pub fn refresh_bearer_token_from_secret_buffer(
139        &self,
140        handoff: BearerRefreshHandoff,
141        source: SecretBuffer<'_>,
142    ) -> Result<CredentialGeneration, TokenRefreshError> {
143        self.credentials.refresh_from_secret_buffer(handoff, source)
144    }
145
146    /// Clears mutable refresh input and atomically installs its lifetime.
147    pub fn refresh_bearer_token_from_mut_bytes_with_lifetime(
148        &self,
149        handoff: BearerRefreshHandoff,
150        source: &mut [u8],
151        lifetime: CredentialLifetime,
152    ) -> Result<CredentialGeneration, TokenRefreshError> {
153        self.credentials
154            .refresh_from_mut_bytes_with_lifetime(handoff, source, lifetime)
155    }
156
157    /// Consumes guarded refresh input and atomically installs its lifetime.
158    pub fn refresh_bearer_token_from_secret_buffer_with_lifetime(
159        &self,
160        handoff: BearerRefreshHandoff,
161        source: SecretBuffer<'_>,
162        lifetime: CredentialLifetime,
163    ) -> Result<CredentialGeneration, TokenRefreshError> {
164        self.credentials
165            .refresh_from_secret_buffer_with_lifetime(handoff, source, lifetime)
166    }
167
168    fn send_inner(
169        &self,
170        authenticated: AuthenticatedRequest<'_, '_>,
171        response_writer: &mut ResponseWriter<'_>,
172    ) -> Result<(), AuthenticatedTransportFailure> {
173        let endpoint_identity = self.endpoint.identity().map_err(|_| {
174            TransportFailure::not_sent(TransportError::AuthenticationEndpointMismatch)
175        })?;
176        validate_bearer_authentication(
177            endpoint_identity,
178            &self.scope,
179            authenticated.policy(),
180            self.allow_insecure_loopback,
181        )
182        .map_err(|error| TransportFailure::not_sent(map_authentication_error(error)))?;
183        let token_snapshot = self
184            .credentials
185            .snapshot()
186            .map_err(|_| TransportFailure::not_sent(TransportError::CredentialStateUnavailable))?;
187        let authorization = token_snapshot
188            .header_value()
189            .map_err(|_| TransportFailure::not_sent(TransportError::HeaderRejected))?;
190        drop(token_snapshot);
191        self.client
192            .execute_authenticated(
193                authenticated.transport_request(),
194                authenticated.response_policy(),
195                authorization,
196                response_writer,
197            )
198            .map_err(|failure| failure.map(TransportError::RawHttp))
199    }
200}
201
202impl BlockingAuthenticatedTransport for BlockingClient {
203    type Error = AuthenticatedTransportFailure;
204
205    fn send_authenticated(
206        &self,
207        request: AuthenticatedRequest<'_, '_>,
208        response: &mut ResponseWriter<'_>,
209    ) -> Result<(), Self::Error> {
210        self.send_inner(request, response)
211    }
212}
213
214impl ResponseStorageSanitizer for BlockingClient {
215    fn sanitize_response_storage(&self, response_storage: &mut [u8]) {
216        sanitize_bytes(response_storage);
217    }
218}
219
220impl BoundTransport for BlockingClient {
221    fn endpoint_identity(&self) -> Result<EndpointIdentity<'_>, EndpointIdentityError> {
222        self.endpoint.identity()
223    }
224}
225
226impl fmt::Debug for BlockingClient {
227    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
228        formatter
229            .debug_struct("BlockingClient")
230            .field("endpoint", &"[redacted]")
231            .field("scope", &"[redacted]")
232            .field("credentials", &"[redacted]")
233            .finish_non_exhaustive()
234    }
235}