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 request signing.
117///
118/// Implementations receive a request URI that is already percent-encoded and ready
119/// for transport. They must derive canonical paths, queries, and headers as local
120/// views without normalizing or rebuilding the existing wire URI.
121///
122/// Header authentication must preserve the URI. Query authentication must preserve
123/// the existing URI representation and append only protocol-encoded authentication
124/// fields. In particular, existing percent escapes, parameter order, duplicate keys,
125/// empty values, and literal `+` characters are caller-owned wire data.
126pub trait SignRequest: Debug + Send + Sync + Unpin + 'static {
127    /// Credential used by this builder.
128    ///
129    /// Typically, it will be a credential.
130    type Credential: Send + Sync + Unpin + 'static;
131
132    /// Return the timestamp through which the credential must remain usable
133    /// for the requested signing operation.
134    ///
135    /// Implementations own the signing clock, the service-specific meaning of
136    /// `expires_in`, and any transport, RPC, or artifact-lifetime headroom. This
137    /// method must not perform I/O or mutate state. When a deadline depends on an
138    /// artifact's signing time, [`SignRequest::sign_request`] must derive both the
139    /// deadline check and the artifact from the same captured timestamp.
140    fn required_valid_until(
141        &self,
142        _credential: &Self::Credential,
143        expires_in: Option<Duration>,
144    ) -> Timestamp {
145        Timestamp::now() + expires_in.unwrap_or_default()
146    }
147
148    /// Sign a request head.
149    ///
150    /// On `Err`, an implementation must leave the entire request head unchanged. On
151    /// `Ok`, it may change only `req.uri` and `req.headers`; the method, version, and
152    /// extensions remain caller-owned. [`crate::Signer`] enforces this commit boundary
153    /// when it invokes the implementation, but implementations must also uphold it for
154    /// callers that invoke this method directly.
155    ///
156    /// ## Credential
157    ///
158    /// The `credential` parameter is the credential required by the signer to sign the request.
159    /// Implementations with expiring credentials must validate it against
160    /// [`SignRequest::required_valid_until`] before mutating the request or performing
161    /// external signing calls. [`crate::Signer`] performs the same validation before
162    /// invoking this method, while direct callers rely on the implementation.
163    ///
164    /// ## Expires In
165    ///
166    /// The `expires_in` parameter requests a validity duration when the service supports
167    /// one. It is not a universal header-versus-query mode selector. Each service and
168    /// credential type defines whether the value selects presigning, configures an
169    /// expiration, is ignored, or is rejected.
170    fn sign_request<'a>(
171        &'a self,
172        ctx: &'a Context,
173        req: &'a mut http::request::Parts,
174        credential: Option<&'a Self::Credential>,
175        expires_in: Option<Duration>,
176    ) -> impl Future<Output = Result<()>> + MaybeSend + 'a;
177}
178
179/// SignRequestDyn is the dyn version of [`SignRequest`].
180pub trait SignRequestDyn: Debug + Send + Sync + Unpin + 'static {
181    /// Credential used by this builder.
182    type Credential: Send + Sync + Unpin + 'static;
183
184    /// Dyn version of [`SignRequest::required_valid_until`].
185    fn required_valid_until_dyn(
186        &self,
187        _credential: &Self::Credential,
188        expires_in: Option<Duration>,
189    ) -> Timestamp {
190        Timestamp::now() + expires_in.unwrap_or_default()
191    }
192
193    /// Dyn version of [`SignRequest::sign_request`].
194    fn sign_request_dyn<'a>(
195        &'a self,
196        ctx: &'a Context,
197        req: &'a mut http::request::Parts,
198        credential: Option<&'a Self::Credential>,
199        expires_in: Option<Duration>,
200    ) -> BoxedFuture<'a, Result<()>>;
201}
202
203impl<T> SignRequestDyn for T
204where
205    T: SignRequest + ?Sized,
206{
207    type Credential = T::Credential;
208
209    fn required_valid_until_dyn(
210        &self,
211        credential: &Self::Credential,
212        expires_in: Option<Duration>,
213    ) -> Timestamp {
214        self.required_valid_until(credential, expires_in)
215    }
216
217    fn sign_request_dyn<'a>(
218        &'a self,
219        ctx: &'a Context,
220        req: &'a mut http::request::Parts,
221        credential: Option<&'a Self::Credential>,
222        expires_in: Option<Duration>,
223    ) -> BoxedFuture<'a, Result<()>> {
224        Box::pin(self.sign_request(ctx, req, credential, expires_in))
225    }
226}
227
228impl<T> SignRequest for std::sync::Arc<T>
229where
230    T: SignRequestDyn + ?Sized,
231{
232    type Credential = T::Credential;
233
234    fn required_valid_until(
235        &self,
236        credential: &Self::Credential,
237        expires_in: Option<Duration>,
238    ) -> Timestamp {
239        self.deref()
240            .required_valid_until_dyn(credential, expires_in)
241    }
242
243    async fn sign_request(
244        &self,
245        ctx: &Context,
246        req: &mut http::request::Parts,
247        credential: Option<&Self::Credential>,
248        expires_in: Option<Duration>,
249    ) -> Result<()> {
250        self.deref()
251            .sign_request_dyn(ctx, req, credential, expires_in)
252            .await
253    }
254}
255
256/// A chain of credential providers that will be tried in order.
257///
258/// This is a generic implementation that can be used by any service to chain multiple
259/// credential providers together. The chain will try each provider in order until one
260/// returns credentials or all providers have been exhausted.
261///
262/// # Example
263///
264/// ```no_run
265/// use reqsign_core::{ProvideCredentialChain, Context, ProvideCredential, Result};
266///
267/// #[derive(Debug)]
268/// struct MyCredential {
269///     token: String,
270/// }
271///
272/// #[derive(Debug)]
273/// struct EnvironmentProvider;
274///
275/// impl ProvideCredential for EnvironmentProvider {
276///     type Credential = MyCredential;
277///
278///     async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
279///         // Implementation
280///         Ok(None)
281///     }
282/// }
283///
284/// # async fn example(ctx: Context) {
285/// let chain = ProvideCredentialChain::new()
286///     .push(EnvironmentProvider);
287///
288/// let credentials = chain.provide_credential(&ctx).await;
289/// # }
290/// ```
291pub struct ProvideCredentialChain<C> {
292    providers: Vec<Box<dyn ProvideCredentialDyn<Credential = C>>>,
293}
294
295impl<C> ProvideCredentialChain<C>
296where
297    C: Send + Sync + Unpin + 'static,
298{
299    /// Create a new empty credential provider chain.
300    pub fn new() -> Self {
301        Self {
302            providers: Vec::new(),
303        }
304    }
305
306    /// Add a credential provider to the chain.
307    pub fn push(mut self, provider: impl ProvideCredential<Credential = C> + 'static) -> Self {
308        self.providers.push(Box::new(provider));
309        self
310    }
311
312    /// Add a credential provider to the front of the chain.
313    ///
314    /// This provider will be tried first before all existing providers.
315    pub fn push_front(
316        mut self,
317        provider: impl ProvideCredential<Credential = C> + 'static,
318    ) -> Self {
319        self.providers.insert(0, Box::new(provider));
320        self
321    }
322
323    /// Create a credential provider chain from a vector of providers.
324    pub fn from_vec(providers: Vec<Box<dyn ProvideCredentialDyn<Credential = C>>>) -> Self {
325        Self { providers }
326    }
327
328    /// Get the number of providers in the chain.
329    pub fn len(&self) -> usize {
330        self.providers.len()
331    }
332
333    /// Check if the chain is empty.
334    pub fn is_empty(&self) -> bool {
335        self.providers.is_empty()
336    }
337}
338
339impl<C> Default for ProvideCredentialChain<C>
340where
341    C: Send + Sync + Unpin + 'static,
342{
343    fn default() -> Self {
344        Self::new()
345    }
346}
347
348impl<C> Debug for ProvideCredentialChain<C>
349where
350    C: Send + Sync + Unpin + 'static,
351{
352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353        f.debug_struct("ProvideCredentialChain")
354            .field("providers_count", &self.providers.len())
355            .finish()
356    }
357}
358
359impl<C> ProvideCredential for ProvideCredentialChain<C>
360where
361    C: Send + Sync + Unpin + 'static,
362{
363    type Credential = C;
364
365    async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
366        for provider in &self.providers {
367            log::debug!("Trying credential provider: {provider:?}");
368
369            match provider.provide_credential_dyn(ctx).await {
370                Ok(Some(cred)) => {
371                    log::debug!("Successfully loaded credential from provider: {provider:?}");
372                    return Ok(Some(cred));
373                }
374                Ok(None) => {
375                    log::debug!("No credential found in provider: {provider:?}");
376                    continue;
377                }
378                Err(e) => {
379                    log::warn!("Error loading credential from provider {provider:?}: {e:?}");
380                    // Continue to next provider on error
381                    continue;
382                }
383            }
384        }
385
386        Ok(None)
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[derive(Clone, Debug)]
395    struct ExactCredential {
396        valid_at: Timestamp,
397    }
398
399    impl SigningCredential for ExactCredential {
400        fn is_valid(&self) -> bool {
401            false
402        }
403
404        fn is_valid_at(&self, timestamp: Timestamp) -> bool {
405            self.valid_at == timestamp
406        }
407    }
408
409    #[test]
410    fn option_forwards_exact_validity_check() {
411        let timestamp = Timestamp::from_second(42).expect("timestamp must be valid");
412        let credential = Some(ExactCredential {
413            valid_at: timestamp,
414        });
415
416        assert!(credential.is_valid_at(timestamp));
417        assert!(!credential.is_valid_at(timestamp + Duration::from_secs(1)));
418        assert!(!None::<ExactCredential>.is_valid_at(timestamp));
419    }
420}