Skip to main content

cloud_sdk/operation/
prepared.rs

1//! Prepared operation storage, endpoint binding, and execution.
2
3use core::fmt;
4
5use cloud_sdk_sanitization::sanitize_bytes;
6
7use crate::authentication::{
8    AsyncAuthenticatedTransport, AuthenticatedRequest, AuthenticationScopePolicy,
9    BlockingAuthenticatedTransport, drive_async_authenticated,
10};
11use crate::operation::{
12    CheckedResponseGuard, OperationId, OperationImpact, OperationMetadata, ResponsePolicy,
13    ResponsePolicyError,
14};
15use crate::transport::{
16    BoundTransport, EndpointIdentity, RawResponsePolicy, RequestHeaders, ResponseBuffer,
17    TransportRequest,
18};
19
20mod body;
21mod construction;
22mod error;
23mod read_only_post;
24mod service;
25mod storage;
26pub use body::{BodyReplayability, RequestBodySensitivity};
27use error::{EndpointCheckError, map_endpoint_error};
28pub use error::{PreparedExecutionError, PreparedRequestPolicyError};
29pub use read_only_post::ApprovedReadOnlyPostQuery;
30pub use service::ProviderService;
31pub use storage::{PreparationStorage, PrepareOperation};
32
33/// Complete request, endpoint, operation metadata, and response policy.
34#[derive(Clone, Copy)]
35pub struct PreparedRequest<'request> {
36    request: TransportRequest<'request>,
37    service: ProviderService<'request>,
38    metadata: OperationMetadata,
39    response_policy: ResponsePolicy,
40    authentication_policy: AuthenticationScopePolicy<'request>,
41    raw_response_policy: RawResponsePolicy<'request>,
42    operation_id: Option<OperationId>,
43    body_replayability: BodyReplayability,
44    body_sensitivity: RequestBodySensitivity,
45    authorization_evidence_required: bool,
46    read_only_post_approval: Option<ApprovedReadOnlyPostQuery>,
47}
48
49impl<'request> PreparedRequest<'request> {
50    /// Binds a validated provider operation identifier.
51    ///
52    /// Closed approved operations bind their identifier during construction;
53    /// this method cannot replace that security identity.
54    #[must_use]
55    pub const fn with_operation_id(mut self, operation_id: OperationId) -> Self {
56        if self.read_only_post_approval.is_none() {
57            self.operation_id = Some(operation_id);
58        }
59        self
60    }
61
62    /// Marks the immutable prepared body snapshot as byte-for-byte replayable.
63    ///
64    /// Providers must call this only after preparation has completed and when
65    /// the borrowed body bytes cannot change for the prepared request lifetime.
66    #[must_use]
67    pub const fn with_replayable_body(mut self) -> Self {
68        self.body_replayability = BodyReplayability::Replayable;
69        self
70    }
71
72    /// Upgrades the explicit body classification to sensitive.
73    ///
74    /// This cannot downgrade a sensitive body. Providers should classify the
75    /// body at construction; this helper supports reviewed wrapper policies.
76    #[must_use]
77    pub const fn with_sensitive_body(mut self) -> Self {
78        self.body_sensitivity = RequestBodySensitivity::Sensitive;
79        self
80    }
81
82    /// Requires provider-owned authorization evidence during plan construction.
83    ///
84    /// This marker can only tighten a prepared request. Generic plan builders
85    /// reject marked requests; provider wrappers must use the evidence-aware
86    /// digest builder and retain their typed dispatch validation.
87    #[must_use]
88    pub const fn with_required_authorization_evidence(mut self) -> Self {
89        self.authorization_evidence_required = true;
90        self
91    }
92
93    /// Returns the validated transport request.
94    #[must_use]
95    pub const fn transport_request(self) -> TransportRequest<'request> {
96        self.request
97    }
98
99    /// Returns the bound provider service.
100    #[must_use]
101    pub const fn service(self) -> ProviderService<'request> {
102        self.service
103    }
104
105    /// Returns complete safety and retry metadata.
106    #[must_use]
107    pub const fn metadata(self) -> OperationMetadata {
108        self.metadata
109    }
110
111    /// Returns complete checked-response policy.
112    #[must_use]
113    pub const fn response_policy(self) -> ResponsePolicy {
114        self.response_policy
115    }
116
117    /// Returns the complete provider-owned authentication-scope policy.
118    #[must_use]
119    pub const fn authentication_policy(self) -> AuthenticationScopePolicy<'request> {
120        self.authentication_policy
121    }
122
123    /// Returns the complete status-class raw response policy.
124    #[must_use]
125    pub const fn raw_response_policy(self) -> RawResponsePolicy<'request> {
126        self.raw_response_policy
127    }
128
129    /// Returns the request with its mandatory authentication and raw wire policy.
130    #[must_use]
131    pub(crate) const fn authenticated_request(self) -> AuthenticatedRequest<'request, 'request> {
132        AuthenticatedRequest::new(
133            self.request,
134            self.authentication_policy,
135            &self.raw_response_policy,
136        )
137    }
138
139    /// Returns the provider operation identifier when one was bound.
140    #[must_use]
141    pub const fn operation_id(self) -> Option<OperationId> {
142        self.operation_id
143    }
144
145    /// Returns the explicit request-body replay capability.
146    #[must_use]
147    pub const fn body_replayability(self) -> BodyReplayability {
148        self.body_replayability
149    }
150
151    /// Returns the provider-declared request-body sensitivity.
152    #[must_use]
153    pub const fn body_sensitivity(self) -> RequestBodySensitivity {
154        self.body_sensitivity
155    }
156
157    /// Reports whether plan construction requires provider-owned evidence.
158    #[must_use]
159    pub const fn authorization_evidence_required(self) -> bool {
160        self.authorization_evidence_required
161    }
162
163    pub(crate) fn with_request_headers<'headers>(
164        self,
165        headers: RequestHeaders<'headers>,
166    ) -> PreparedRequest<'headers>
167    where
168        'request: 'headers,
169    {
170        let request: TransportRequest<'headers> = self.request;
171        PreparedRequest {
172            request: request.with_headers(headers),
173            service: self.service,
174            metadata: self.metadata,
175            response_policy: self.response_policy,
176            authentication_policy: self.authentication_policy,
177            raw_response_policy: self.raw_response_policy,
178            operation_id: self.operation_id,
179            body_replayability: self.body_replayability,
180            body_sensitivity: self.body_sensitivity,
181            authorization_evidence_required: self.authorization_evidence_required,
182            read_only_post_approval: self.read_only_post_approval,
183        }
184    }
185
186    pub(crate) fn has_same_retry_policy(&self, other: &Self) -> bool {
187        self.service == other.service
188            && self.metadata == other.metadata
189            && self.response_policy == other.response_policy
190            && self.authentication_policy == other.authentication_policy
191            && self.raw_response_policy == other.raw_response_policy
192            && self.operation_id == other.operation_id
193            && self.body_replayability == other.body_replayability
194            && self.body_sensitivity == other.body_sensitivity
195            && self.authorization_evidence_required == other.authorization_evidence_required
196            && self.read_only_post_approval == other.read_only_post_approval
197            && self.has_same_header_policy(other)
198    }
199
200    fn has_same_header_policy(&self, other: &Self) -> bool {
201        let left = self.request.headers().as_slice();
202        let right = other.request.headers().as_slice();
203        left.len() == right.len()
204            && left
205                .iter()
206                .zip(right)
207                .all(|(left, right)| left.sensitivity() == right.sensitivity())
208    }
209
210    /// Applies the complete prepared response policy without executing transport.
211    pub fn validate_response<'buffer>(
212        self,
213        response: ResponseBuffer<'buffer>,
214    ) -> Result<CheckedResponseGuard<'buffer>, ResponsePolicyError> {
215        self.response_policy
216            .validate(response, self.metadata.request_id_policy())
217    }
218
219    /// Applies operation-owned metadata policy before provider error decoding.
220    ///
221    /// This is the error-status counterpart to [`Self::validate_response`].
222    /// It extracts and protects, discards, or admits retention of the provider
223    /// request identifier without applying success-status or body policy.
224    pub fn apply_response_metadata_policy(
225        self,
226        response: &mut ResponseBuffer<'_>,
227    ) -> Result<(), ResponsePolicyError> {
228        super::policy::apply_request_id_policy(response, self.metadata.request_id_policy())
229    }
230
231    /// Verifies endpoint identity, executes once, and validates the response.
232    pub fn execute_blocking<'buffer, T>(
233        self,
234        transport: &T,
235        response_storage: &'buffer mut [u8],
236        response_header_storage: &'buffer mut [u8],
237    ) -> Result<CheckedResponseGuard<'buffer>, PreparedExecutionError<T::Error>>
238    where
239        T: BlockingAuthenticatedTransport + BoundTransport,
240    {
241        let response = self.send_blocking(transport, response_storage, response_header_storage)?;
242        self.response_policy
243            .validate(response, self.metadata.request_id_policy())
244            .map_err(PreparedExecutionError::ResponsePolicy)
245    }
246
247    pub(crate) fn send_blocking<'buffer, T>(
248        self,
249        transport: &T,
250        response_storage: &'buffer mut [u8],
251        response_header_storage: &'buffer mut [u8],
252    ) -> Result<ResponseBuffer<'buffer>, PreparedExecutionError<T::Error>>
253    where
254        T: BlockingAuthenticatedTransport + BoundTransport,
255    {
256        if self.requires_execution_permit() {
257            sanitize_bytes(response_storage);
258            sanitize_bytes(response_header_storage);
259            return Err(PreparedExecutionError::AuthorizationRequired);
260        }
261        self.send_blocking_authorized(transport, None, response_storage, response_header_storage)
262    }
263
264    pub(crate) fn execute_blocking_authorized<'buffer, T>(
265        self,
266        transport: &T,
267        confirmed_endpoint: Option<EndpointIdentity<'_>>,
268        response_storage: &'buffer mut [u8],
269        response_header_storage: &'buffer mut [u8],
270    ) -> Result<CheckedResponseGuard<'buffer>, PreparedExecutionError<T::Error>>
271    where
272        T: BlockingAuthenticatedTransport + BoundTransport,
273    {
274        let response = self.send_blocking_authorized(
275            transport,
276            confirmed_endpoint,
277            response_storage,
278            response_header_storage,
279        )?;
280        self.response_policy
281            .validate(response, self.metadata.request_id_policy())
282            .map_err(PreparedExecutionError::ResponsePolicy)
283    }
284
285    pub(crate) fn send_blocking_authorized<'buffer, T>(
286        self,
287        transport: &T,
288        confirmed_endpoint: Option<EndpointIdentity<'_>>,
289        response_storage: &'buffer mut [u8],
290        response_header_storage: &'buffer mut [u8],
291    ) -> Result<ResponseBuffer<'buffer>, PreparedExecutionError<T::Error>>
292    where
293        T: BlockingAuthenticatedTransport + BoundTransport,
294    {
295        let mut response = ResponseBuffer::new(
296            response_storage,
297            self.raw_response_policy.max_body_bytes(),
298            response_header_storage,
299        );
300        self.verify_endpoint(transport, confirmed_endpoint)
301            .map_err(map_endpoint_error)?;
302        transport
303            .send_authenticated(self.authenticated_request(), response.writer())
304            .map_err(PreparedExecutionError::Transport)?;
305        Ok(response)
306    }
307
308    /// Async equivalent of [`Self::execute_blocking`] without owning an executor.
309    pub async fn execute_async<'transport, 'buffer, T>(
310        &'transport self,
311        transport: &'transport T,
312        response_storage: &'buffer mut [u8],
313        response_header_storage: &'buffer mut [u8],
314    ) -> Result<CheckedResponseGuard<'buffer>, PreparedExecutionError<T::Error>>
315    where
316        T: AsyncAuthenticatedTransport + BoundTransport,
317        'request: 'transport,
318    {
319        let response = self
320            .send_async(transport, response_storage, response_header_storage)
321            .await?;
322        self.response_policy
323            .validate(response, self.metadata.request_id_policy())
324            .map_err(PreparedExecutionError::ResponsePolicy)
325    }
326
327    pub(crate) async fn send_async<'transport, 'buffer, T>(
328        &'transport self,
329        transport: &'transport T,
330        response_storage: &'buffer mut [u8],
331        response_header_storage: &'buffer mut [u8],
332    ) -> Result<ResponseBuffer<'buffer>, PreparedExecutionError<T::Error>>
333    where
334        T: AsyncAuthenticatedTransport + BoundTransport,
335        'request: 'transport,
336    {
337        if self.requires_execution_permit() {
338            sanitize_bytes(response_storage);
339            sanitize_bytes(response_header_storage);
340            return Err(PreparedExecutionError::AuthorizationRequired);
341        }
342        self.send_async_authorized(transport, None, response_storage, response_header_storage)
343            .await
344    }
345
346    pub(crate) async fn execute_async_authorized<'transport, 'buffer, T>(
347        &'transport self,
348        transport: &'transport T,
349        confirmed_endpoint: Option<EndpointIdentity<'_>>,
350        response_storage: &'buffer mut [u8],
351        response_header_storage: &'buffer mut [u8],
352    ) -> Result<CheckedResponseGuard<'buffer>, PreparedExecutionError<T::Error>>
353    where
354        T: AsyncAuthenticatedTransport + BoundTransport,
355        'request: 'transport,
356    {
357        let response = self
358            .send_async_authorized(
359                transport,
360                confirmed_endpoint,
361                response_storage,
362                response_header_storage,
363            )
364            .await?;
365        self.response_policy
366            .validate(response, self.metadata.request_id_policy())
367            .map_err(PreparedExecutionError::ResponsePolicy)
368    }
369
370    pub(crate) async fn send_async_authorized<'transport, 'buffer, T>(
371        &'transport self,
372        transport: &'transport T,
373        confirmed_endpoint: Option<EndpointIdentity<'_>>,
374        response_storage: &'buffer mut [u8],
375        response_header_storage: &'buffer mut [u8],
376    ) -> Result<ResponseBuffer<'buffer>, PreparedExecutionError<T::Error>>
377    where
378        T: AsyncAuthenticatedTransport + BoundTransport,
379        'request: 'transport,
380    {
381        let mut response = ResponseBuffer::new(
382            response_storage,
383            self.raw_response_policy.max_body_bytes(),
384            response_header_storage,
385        );
386        self.verify_endpoint(transport, confirmed_endpoint)
387            .map_err(map_endpoint_error)?;
388        drive_async_authenticated(transport, self.authenticated_request(), response.writer())
389            .await
390            .map_err(|error| match error {
391                crate::transport::AsyncExecutionError::Transport(error) => {
392                    PreparedExecutionError::Transport(error)
393                }
394                crate::transport::AsyncExecutionError::Response(error) => {
395                    PreparedExecutionError::ResponseWriter(error)
396                }
397            })?;
398        Ok(response)
399    }
400
401    pub(crate) fn requires_execution_permit(self) -> bool {
402        let approved_read_only_post = self
403            .read_only_post_approval
404            .and_then(|approval| {
405                approval.validate(
406                    self.request,
407                    self.service,
408                    self.metadata,
409                    self.authentication_policy,
410                    self.body_sensitivity,
411                )
412            })
413            .is_some_and(|operation_id| self.operation_id == Some(operation_id));
414        (!self.request.method().permits_direct_read_only() && !approved_read_only_post)
415            || !matches!(self.metadata.impact(), OperationImpact::ReadOnly)
416            || matches!(self.metadata.cost_intent(), super::CostIntent::MayIncurCost)
417    }
418
419    fn verify_endpoint<T>(
420        self,
421        transport: &T,
422        confirmed_endpoint: Option<EndpointIdentity<'_>>,
423    ) -> Result<(), EndpointCheckError>
424    where
425        T: BoundTransport,
426    {
427        let actual = transport
428            .endpoint_identity()
429            .map_err(EndpointCheckError::Invalid)?;
430        match confirmed_endpoint {
431            Some(expected) if actual == expected => Ok(()),
432            Some(_) => Err(EndpointCheckError::Mismatch),
433            None => self
434                .service
435                .endpoint_policy()
436                .verify(actual)
437                .map_err(|_| EndpointCheckError::Mismatch),
438        }
439    }
440}
441
442impl fmt::Debug for PreparedRequest<'_> {
443    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
444        formatter
445            .debug_struct("PreparedRequest")
446            .field("request", &self.request)
447            .field("service", &self.service)
448            .field("metadata", &self.metadata)
449            .field("response_policy", &self.response_policy)
450            .field("authentication_policy", &self.authentication_policy)
451            .field("raw_response_policy", &self.raw_response_policy)
452            .field("operation_id", &self.operation_id)
453            .field("body_replayability", &self.body_replayability)
454            .field("body_sensitivity", &self.body_sensitivity)
455            .field(
456                "authorization_evidence_required",
457                &self.authorization_evidence_required,
458            )
459            .field("read_only_post_approval", &self.read_only_post_approval)
460            .finish()
461    }
462}