Skip to main content

reqsign_core/
api.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::time::Timestamp;
19use crate::{BoxedFuture, Context, MaybeSend, Result};
20use std::fmt::Debug;
21use std::future::Future;
22use std::ops::Deref;
23use std::time::Duration;
24
25/// A credential that can distinguish cache freshness from exact usability.
26///
27/// Both checks must reject credentials that lack fields required for authentication.
28pub trait SigningCredential: Clone + Debug + Send + Sync + Unpin + 'static {
29    /// Return whether a cached credential can be reused without refreshing it.
30    ///
31    /// Implementations may include a proactive refresh window in this check.
32    fn is_valid(&self) -> bool;
33
34    /// Return whether the credential is usable at this exact timestamp.
35    ///
36    /// Implementations with an expiration time should not add a refresh or
37    /// operation-specific buffer here. The default preserves the behavior of
38    /// implementations that only provide [`SigningCredential::is_valid`].
39    fn is_valid_at(&self, _ts: Timestamp) -> bool {
40        self.is_valid()
41    }
42}
43
44impl<T: SigningCredential> SigningCredential for Option<T> {
45    fn is_valid(&self) -> bool {
46        let Some(ctx) = self else {
47            return false;
48        };
49
50        ctx.is_valid()
51    }
52
53    fn is_valid_at(&self, ts: Timestamp) -> bool {
54        let Some(ctx) = self else {
55            return false;
56        };
57
58        ctx.is_valid_at(ts)
59    }
60}
61
62/// ProvideCredential is the trait used by signer to load the credential from the environment.
63///`
64/// Service may require different credential to sign the request, for example, AWS require
65/// access key and secret key, while Google Cloud Storage require token.
66pub trait ProvideCredential: Debug + Send + Sync + Unpin + 'static {
67    /// Credential returned by this loader.
68    ///
69    /// Typically, it will be a credential.
70    type Credential: Send + Sync + Unpin + 'static;
71
72    /// Load signing credential from current env.
73    fn provide_credential(
74        &self,
75        ctx: &Context,
76    ) -> impl Future<Output = Result<Option<Self::Credential>>> + MaybeSend;
77}
78
79/// ProvideCredentialDyn is the dyn version of [`ProvideCredential`].
80pub trait ProvideCredentialDyn: Debug + Send + Sync + Unpin + 'static {
81    /// Credential returned by this loader.
82    type Credential: Send + Sync + Unpin + 'static;
83
84    /// Dyn version of [`ProvideCredential::provide_credential`].
85    fn provide_credential_dyn<'a>(
86        &'a self,
87        ctx: &'a Context,
88    ) -> BoxedFuture<'a, Result<Option<Self::Credential>>>;
89}
90
91impl<T> ProvideCredentialDyn for T
92where
93    T: ProvideCredential + ?Sized,
94{
95    type Credential = T::Credential;
96
97    fn provide_credential_dyn<'a>(
98        &'a self,
99        ctx: &'a Context,
100    ) -> BoxedFuture<'a, Result<Option<Self::Credential>>> {
101        Box::pin(self.provide_credential(ctx))
102    }
103}
104
105impl<T> ProvideCredential for std::sync::Arc<T>
106where
107    T: ProvideCredentialDyn + ?Sized,
108{
109    type Credential = T::Credential;
110
111    async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
112        self.deref().provide_credential_dyn(ctx).await
113    }
114}
115
116/// Service-specific credential granting.
117///
118/// A granter uses an existing service credential to authorize one bounded,
119/// expiring credential transition. The source and result remain in the same
120/// service credential family, while the concrete implementation owns the
121/// service-specific resource, permission, policy, and identity semantics.
122///
123/// This trait standardizes orchestration and lifecycle, not a cross-service
124/// scope model, and does not promise that every service can express strict
125/// monotonic downscoping. Implementations must validate the concrete source
126/// credential variant before performing I/O. Returned credentials must own
127/// credential material that is independent from the source credential and
128/// must not retain or share its secret buffers. Implementations must also keep
129/// secrets out of [`Debug`] output and returned errors.
130pub trait GrantCredential: Debug + Send + Sync + Unpin + 'static {
131    /// Credential used as the source and returned as the granted result.
132    type Credential: SigningCredential;
133
134    /// Return the timestamp through which the source credential must remain usable.
135    ///
136    /// This method must not perform I/O or mutate state. It must conservatively
137    /// include any service I/O headroom unless current service-specific cache
138    /// state proves that the operation can complete without that I/O.
139    fn required_valid_until(
140        &self,
141        credential: &Self::Credential,
142        expires_in: Option<Duration>,
143    ) -> Timestamp;
144
145    /// Grant a bounded, expiring credential from an existing service credential.
146    ///
147    /// `expires_in` is a service-specific requested lifetime. `None` does not
148    /// mean that the returned credential may be non-expiring. After all I/O,
149    /// the implementation must ensure that the returned credential remains
150    /// exactly usable at the actual completion time and carries or reliably
151    /// derives its absolute expiration.
152    fn grant_credential<'a>(
153        &'a self,
154        ctx: &'a Context,
155        credential: &'a Self::Credential,
156        expires_in: Option<Duration>,
157    ) -> impl Future<Output = Result<Self::Credential>> + MaybeSend + 'a;
158}
159
160/// Dyn version of [`GrantCredential`].
161pub trait GrantCredentialDyn: Debug + Send + Sync + Unpin + 'static {
162    /// Credential used as the source and returned as the granted result.
163    type Credential: SigningCredential;
164
165    /// Dyn version of [`GrantCredential::required_valid_until`].
166    fn required_valid_until_dyn(
167        &self,
168        credential: &Self::Credential,
169        expires_in: Option<Duration>,
170    ) -> Timestamp;
171
172    /// Dyn version of [`GrantCredential::grant_credential`].
173    fn grant_credential_dyn<'a>(
174        &'a self,
175        ctx: &'a Context,
176        credential: &'a Self::Credential,
177        expires_in: Option<Duration>,
178    ) -> BoxedFuture<'a, Result<Self::Credential>>;
179}
180
181impl<T> GrantCredentialDyn for T
182where
183    T: GrantCredential + ?Sized,
184{
185    type Credential = T::Credential;
186
187    fn required_valid_until_dyn(
188        &self,
189        credential: &Self::Credential,
190        expires_in: Option<Duration>,
191    ) -> Timestamp {
192        self.required_valid_until(credential, expires_in)
193    }
194
195    fn grant_credential_dyn<'a>(
196        &'a self,
197        ctx: &'a Context,
198        credential: &'a Self::Credential,
199        expires_in: Option<Duration>,
200    ) -> BoxedFuture<'a, Result<Self::Credential>> {
201        Box::pin(self.grant_credential(ctx, credential, expires_in))
202    }
203}
204
205impl<T> GrantCredential for std::sync::Arc<T>
206where
207    T: GrantCredentialDyn + ?Sized,
208{
209    type Credential = T::Credential;
210
211    fn required_valid_until(
212        &self,
213        credential: &Self::Credential,
214        expires_in: Option<Duration>,
215    ) -> Timestamp {
216        self.deref()
217            .required_valid_until_dyn(credential, expires_in)
218    }
219
220    async fn grant_credential(
221        &self,
222        ctx: &Context,
223        credential: &Self::Credential,
224        expires_in: Option<Duration>,
225    ) -> Result<Self::Credential> {
226        self.deref()
227            .grant_credential_dyn(ctx, credential, expires_in)
228            .await
229    }
230}
231
232/// Service-specific request signing.
233///
234/// Implementations receive a request URI that is already percent-encoded and ready
235/// for transport. They must derive canonical paths, queries, and headers as local
236/// views without normalizing or rebuilding the existing wire URI.
237///
238/// Header authentication must preserve the URI. Query authentication must preserve
239/// the existing URI representation and append only protocol-encoded authentication
240/// fields. In particular, existing percent escapes, parameter order, duplicate keys,
241/// empty values, and literal `+` characters are caller-owned wire data.
242pub trait SignRequest: Debug + Send + Sync + Unpin + 'static {
243    /// Credential used by this builder.
244    ///
245    /// Typically, it will be a credential.
246    type Credential: Send + Sync + Unpin + 'static;
247
248    /// Return the timestamp through which the credential must remain usable
249    /// for the requested signing operation.
250    ///
251    /// Implementations own the signing clock, the service-specific meaning of
252    /// `expires_in`, and any transport, RPC, or artifact-lifetime headroom. This
253    /// method must not perform I/O or mutate state. When a deadline depends on an
254    /// artifact's signing time, [`SignRequest::sign_request`] must derive both the
255    /// deadline check and the artifact from the same captured timestamp.
256    fn required_valid_until(
257        &self,
258        _credential: &Self::Credential,
259        expires_in: Option<Duration>,
260    ) -> Timestamp {
261        Timestamp::now() + expires_in.unwrap_or_default()
262    }
263
264    /// Sign a request head.
265    ///
266    /// On `Err`, an implementation must leave the entire request head unchanged. On
267    /// `Ok`, it may change only `req.uri` and `req.headers`; the method, version, and
268    /// extensions remain caller-owned. [`crate::Signer`] enforces this commit boundary
269    /// when it invokes the implementation, but implementations must also uphold it for
270    /// callers that invoke this method directly.
271    ///
272    /// ## Credential
273    ///
274    /// The `credential` parameter is the credential required by the signer to sign the request.
275    /// Implementations with expiring credentials must validate it against
276    /// [`SignRequest::required_valid_until`] before mutating the request or performing
277    /// external signing calls. [`crate::Signer`] performs the same validation before
278    /// invoking this method, while direct callers rely on the implementation.
279    ///
280    /// ## Expires In
281    ///
282    /// The `expires_in` parameter requests a validity duration when the service supports
283    /// one. It is not a universal header-versus-query mode selector. Each service and
284    /// credential type defines whether the value selects presigning, configures an
285    /// expiration, is ignored, or is rejected.
286    fn sign_request<'a>(
287        &'a self,
288        ctx: &'a Context,
289        req: &'a mut http::request::Parts,
290        credential: Option<&'a Self::Credential>,
291        expires_in: Option<Duration>,
292    ) -> impl Future<Output = Result<()>> + MaybeSend + 'a;
293}
294
295/// SignRequestDyn is the dyn version of [`SignRequest`].
296pub trait SignRequestDyn: Debug + Send + Sync + Unpin + 'static {
297    /// Credential used by this builder.
298    type Credential: Send + Sync + Unpin + 'static;
299
300    /// Dyn version of [`SignRequest::required_valid_until`].
301    fn required_valid_until_dyn(
302        &self,
303        _credential: &Self::Credential,
304        expires_in: Option<Duration>,
305    ) -> Timestamp {
306        Timestamp::now() + expires_in.unwrap_or_default()
307    }
308
309    /// Dyn version of [`SignRequest::sign_request`].
310    fn sign_request_dyn<'a>(
311        &'a self,
312        ctx: &'a Context,
313        req: &'a mut http::request::Parts,
314        credential: Option<&'a Self::Credential>,
315        expires_in: Option<Duration>,
316    ) -> BoxedFuture<'a, Result<()>>;
317}
318
319impl<T> SignRequestDyn for T
320where
321    T: SignRequest + ?Sized,
322{
323    type Credential = T::Credential;
324
325    fn required_valid_until_dyn(
326        &self,
327        credential: &Self::Credential,
328        expires_in: Option<Duration>,
329    ) -> Timestamp {
330        self.required_valid_until(credential, expires_in)
331    }
332
333    fn sign_request_dyn<'a>(
334        &'a self,
335        ctx: &'a Context,
336        req: &'a mut http::request::Parts,
337        credential: Option<&'a Self::Credential>,
338        expires_in: Option<Duration>,
339    ) -> BoxedFuture<'a, Result<()>> {
340        Box::pin(self.sign_request(ctx, req, credential, expires_in))
341    }
342}
343
344impl<T> SignRequest for std::sync::Arc<T>
345where
346    T: SignRequestDyn + ?Sized,
347{
348    type Credential = T::Credential;
349
350    fn required_valid_until(
351        &self,
352        credential: &Self::Credential,
353        expires_in: Option<Duration>,
354    ) -> Timestamp {
355        self.deref()
356            .required_valid_until_dyn(credential, expires_in)
357    }
358
359    async fn sign_request(
360        &self,
361        ctx: &Context,
362        req: &mut http::request::Parts,
363        credential: Option<&Self::Credential>,
364        expires_in: Option<Duration>,
365    ) -> Result<()> {
366        self.deref()
367            .sign_request_dyn(ctx, req, credential, expires_in)
368            .await
369    }
370}
371
372/// A chain of credential providers that will be tried in order.
373///
374/// This is a generic implementation that can be used by any service to chain multiple
375/// credential providers together. The chain will try each provider in order until one
376/// returns credentials or all providers have been exhausted.
377///
378/// # Example
379///
380/// ```no_run
381/// use reqsign_core::{ProvideCredentialChain, Context, ProvideCredential, Result};
382///
383/// #[derive(Debug)]
384/// struct MyCredential {
385///     token: String,
386/// }
387///
388/// #[derive(Debug)]
389/// struct EnvironmentProvider;
390///
391/// impl ProvideCredential for EnvironmentProvider {
392///     type Credential = MyCredential;
393///
394///     async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
395///         // Implementation
396///         Ok(None)
397///     }
398/// }
399///
400/// # async fn example(ctx: Context) {
401/// let chain = ProvideCredentialChain::new()
402///     .push(EnvironmentProvider);
403///
404/// let credentials = chain.provide_credential(&ctx).await;
405/// # }
406/// ```
407pub struct ProvideCredentialChain<C> {
408    providers: Vec<Box<dyn ProvideCredentialDyn<Credential = C>>>,
409}
410
411impl<C> ProvideCredentialChain<C>
412where
413    C: Send + Sync + Unpin + 'static,
414{
415    /// Create a new empty credential provider chain.
416    pub fn new() -> Self {
417        Self {
418            providers: Vec::new(),
419        }
420    }
421
422    /// Add a credential provider to the chain.
423    pub fn push(mut self, provider: impl ProvideCredential<Credential = C> + 'static) -> Self {
424        self.providers.push(Box::new(provider));
425        self
426    }
427
428    /// Add a credential provider to the front of the chain.
429    ///
430    /// This provider will be tried first before all existing providers.
431    pub fn push_front(
432        mut self,
433        provider: impl ProvideCredential<Credential = C> + 'static,
434    ) -> Self {
435        self.providers.insert(0, Box::new(provider));
436        self
437    }
438
439    /// Create a credential provider chain from a vector of providers.
440    pub fn from_vec(providers: Vec<Box<dyn ProvideCredentialDyn<Credential = C>>>) -> Self {
441        Self { providers }
442    }
443
444    /// Get the number of providers in the chain.
445    pub fn len(&self) -> usize {
446        self.providers.len()
447    }
448
449    /// Check if the chain is empty.
450    pub fn is_empty(&self) -> bool {
451        self.providers.is_empty()
452    }
453}
454
455impl<C> Default for ProvideCredentialChain<C>
456where
457    C: Send + Sync + Unpin + 'static,
458{
459    fn default() -> Self {
460        Self::new()
461    }
462}
463
464impl<C> Debug for ProvideCredentialChain<C>
465where
466    C: Send + Sync + Unpin + 'static,
467{
468    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
469        f.debug_struct("ProvideCredentialChain")
470            .field("providers_count", &self.providers.len())
471            .finish()
472    }
473}
474
475impl<C> ProvideCredential for ProvideCredentialChain<C>
476where
477    C: Send + Sync + Unpin + 'static,
478{
479    type Credential = C;
480
481    async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
482        for provider in &self.providers {
483            log::debug!("Trying credential provider: {provider:?}");
484
485            match provider.provide_credential_dyn(ctx).await {
486                Ok(Some(cred)) => {
487                    log::debug!("Successfully loaded credential from provider: {provider:?}");
488                    return Ok(Some(cred));
489                }
490                Ok(None) => {
491                    log::debug!("No credential found in provider: {provider:?}");
492                    continue;
493                }
494                Err(e) => {
495                    log::warn!("Error loading credential from provider {provider:?}: {e:?}");
496                    // Continue to next provider on error
497                    continue;
498                }
499            }
500        }
501
502        Ok(None)
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[derive(Clone, Debug)]
511    struct ExactCredential {
512        valid_at: Timestamp,
513    }
514
515    impl SigningCredential for ExactCredential {
516        fn is_valid(&self) -> bool {
517            false
518        }
519
520        fn is_valid_at(&self, timestamp: Timestamp) -> bool {
521            self.valid_at == timestamp
522        }
523    }
524
525    #[test]
526    fn option_forwards_exact_validity_check() {
527        let timestamp = Timestamp::from_second(42).expect("timestamp must be valid");
528        let credential = Some(ExactCredential {
529            valid_at: timestamp,
530        });
531
532        assert!(credential.is_valid_at(timestamp));
533        assert!(!credential.is_valid_at(timestamp + Duration::from_secs(1)));
534        assert!(!None::<ExactCredential>.is_valid_at(timestamp));
535    }
536}