| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| use crate::time::Timestamp; |
| use crate::{BoxedFuture, Context, MaybeSend, Result}; |
| use std::fmt::Debug; |
| use std::future::Future; |
| use std::ops::Deref; |
| use std::time::Duration; |
| |
| /// A credential that can distinguish cache freshness from exact usability. |
| /// |
| /// Both checks must reject credentials that lack fields required for authentication. |
| pub trait SigningCredential: Clone + Debug + Send + Sync + Unpin + 'static { |
| /// Return whether a cached credential can be reused without refreshing it. |
| /// |
| /// Implementations may include a proactive refresh window in this check. |
| fn is_valid(&self) -> bool; |
| |
| /// Return whether the credential is usable at this exact timestamp. |
| /// |
| /// Implementations with an expiration time should not add a refresh or |
| /// operation-specific buffer here. The default preserves the behavior of |
| /// implementations that only provide [`SigningCredential::is_valid`]. |
| fn is_valid_at(&self, _ts: Timestamp) -> bool { |
| self.is_valid() |
| } |
| } |
| |
| impl<T: SigningCredential> SigningCredential for Option<T> { |
| fn is_valid(&self) -> bool { |
| let Some(ctx) = self else { |
| return false; |
| }; |
| |
| ctx.is_valid() |
| } |
| |
| fn is_valid_at(&self, ts: Timestamp) -> bool { |
| let Some(ctx) = self else { |
| return false; |
| }; |
| |
| ctx.is_valid_at(ts) |
| } |
| } |
| |
| /// ProvideCredential is the trait used by signer to load the credential from the environment. |
| ///` |
| /// Service may require different credential to sign the request, for example, AWS require |
| /// access key and secret key, while Google Cloud Storage require token. |
| pub trait ProvideCredential: Debug + Send + Sync + Unpin + 'static { |
| /// Credential returned by this loader. |
| /// |
| /// Typically, it will be a credential. |
| type Credential: Send + Sync + Unpin + 'static; |
| |
| /// Load signing credential from current env. |
| fn provide_credential( |
| &self, |
| ctx: &Context, |
| ) -> impl Future<Output = Result<Option<Self::Credential>>> + MaybeSend; |
| } |
| |
| /// ProvideCredentialDyn is the dyn version of [`ProvideCredential`]. |
| pub trait ProvideCredentialDyn: Debug + Send + Sync + Unpin + 'static { |
| /// Credential returned by this loader. |
| type Credential: Send + Sync + Unpin + 'static; |
| |
| /// Dyn version of [`ProvideCredential::provide_credential`]. |
| fn provide_credential_dyn<'a>( |
| &'a self, |
| ctx: &'a Context, |
| ) -> BoxedFuture<'a, Result<Option<Self::Credential>>>; |
| } |
| |
| impl<T> ProvideCredentialDyn for T |
| where |
| T: ProvideCredential + ?Sized, |
| { |
| type Credential = T::Credential; |
| |
| fn provide_credential_dyn<'a>( |
| &'a self, |
| ctx: &'a Context, |
| ) -> BoxedFuture<'a, Result<Option<Self::Credential>>> { |
| Box::pin(self.provide_credential(ctx)) |
| } |
| } |
| |
| impl<T> ProvideCredential for std::sync::Arc<T> |
| where |
| T: ProvideCredentialDyn + ?Sized, |
| { |
| type Credential = T::Credential; |
| |
| async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> { |
| self.deref().provide_credential_dyn(ctx).await |
| } |
| } |
| |
| /// Service-specific credential granting. |
| /// |
| /// A granter uses an existing service credential to authorize one bounded, |
| /// expiring credential transition. The source and result remain in the same |
| /// service credential family, while the concrete implementation owns the |
| /// service-specific resource, permission, policy, and identity semantics. |
| /// |
| /// This trait standardizes orchestration and lifecycle, not a cross-service |
| /// scope model, and does not promise that every service can express strict |
| /// monotonic downscoping. Implementations must validate the concrete source |
| /// credential variant before performing I/O. Returned credentials must own |
| /// credential material that is independent from the source credential and |
| /// must not retain or share its secret buffers. Implementations must also keep |
| /// secrets out of [`Debug`] output and returned errors. |
| pub trait GrantCredential: Debug + Send + Sync + Unpin + 'static { |
| /// Credential used as the source and returned as the granted result. |
| type Credential: SigningCredential; |
| |
| /// Return the timestamp through which the source credential must remain usable. |
| /// |
| /// This method must not perform I/O or mutate state. It must conservatively |
| /// include any service I/O headroom unless current service-specific cache |
| /// state proves that the operation can complete without that I/O. |
| fn required_valid_until( |
| &self, |
| credential: &Self::Credential, |
| expires_in: Option<Duration>, |
| ) -> Timestamp; |
| |
| /// Grant a bounded, expiring credential from an existing service credential. |
| /// |
| /// `expires_in` is a service-specific requested lifetime. `None` does not |
| /// mean that the returned credential may be non-expiring. After all I/O, |
| /// the implementation must ensure that the returned credential remains |
| /// exactly usable at the actual completion time and carries or reliably |
| /// derives its absolute expiration. |
| fn grant_credential<'a>( |
| &'a self, |
| ctx: &'a Context, |
| credential: &'a Self::Credential, |
| expires_in: Option<Duration>, |
| ) -> impl Future<Output = Result<Self::Credential>> + MaybeSend + 'a; |
| } |
| |
| /// Dyn version of [`GrantCredential`]. |
| pub trait GrantCredentialDyn: Debug + Send + Sync + Unpin + 'static { |
| /// Credential used as the source and returned as the granted result. |
| type Credential: SigningCredential; |
| |
| /// Dyn version of [`GrantCredential::required_valid_until`]. |
| fn required_valid_until_dyn( |
| &self, |
| credential: &Self::Credential, |
| expires_in: Option<Duration>, |
| ) -> Timestamp; |
| |
| /// Dyn version of [`GrantCredential::grant_credential`]. |
| fn grant_credential_dyn<'a>( |
| &'a self, |
| ctx: &'a Context, |
| credential: &'a Self::Credential, |
| expires_in: Option<Duration>, |
| ) -> BoxedFuture<'a, Result<Self::Credential>>; |
| } |
| |
| impl<T> GrantCredentialDyn for T |
| where |
| T: GrantCredential + ?Sized, |
| { |
| type Credential = T::Credential; |
| |
| fn required_valid_until_dyn( |
| &self, |
| credential: &Self::Credential, |
| expires_in: Option<Duration>, |
| ) -> Timestamp { |
| self.required_valid_until(credential, expires_in) |
| } |
| |
| fn grant_credential_dyn<'a>( |
| &'a self, |
| ctx: &'a Context, |
| credential: &'a Self::Credential, |
| expires_in: Option<Duration>, |
| ) -> BoxedFuture<'a, Result<Self::Credential>> { |
| Box::pin(self.grant_credential(ctx, credential, expires_in)) |
| } |
| } |
| |
| impl<T> GrantCredential for std::sync::Arc<T> |
| where |
| T: GrantCredentialDyn + ?Sized, |
| { |
| type Credential = T::Credential; |
| |
| fn required_valid_until( |
| &self, |
| credential: &Self::Credential, |
| expires_in: Option<Duration>, |
| ) -> Timestamp { |
| self.deref() |
| .required_valid_until_dyn(credential, expires_in) |
| } |
| |
| async fn grant_credential( |
| &self, |
| ctx: &Context, |
| credential: &Self::Credential, |
| expires_in: Option<Duration>, |
| ) -> Result<Self::Credential> { |
| self.deref() |
| .grant_credential_dyn(ctx, credential, expires_in) |
| .await |
| } |
| } |
| |
| /// Service-specific request signing. |
| /// |
| /// Implementations receive a request URI that is already percent-encoded and ready |
| /// for transport. They must derive canonical paths, queries, and headers as local |
| /// views without normalizing or rebuilding the existing wire URI. |
| /// |
| /// Header authentication must preserve the URI. Query authentication must preserve |
| /// the existing URI representation and append only protocol-encoded authentication |
| /// fields. In particular, existing percent escapes, parameter order, duplicate keys, |
| /// empty values, and literal `+` characters are caller-owned wire data. |
| pub trait SignRequest: Debug + Send + Sync + Unpin + 'static { |
| /// Credential used by this builder. |
| /// |
| /// Typically, it will be a credential. |
| type Credential: Send + Sync + Unpin + 'static; |
| |
| /// Return the timestamp through which the credential must remain usable |
| /// for the requested signing operation. |
| /// |
| /// Implementations own the signing clock, the service-specific meaning of |
| /// `expires_in`, and any transport, RPC, or artifact-lifetime headroom. This |
| /// method must not perform I/O or mutate state. When a deadline depends on an |
| /// artifact's signing time, [`SignRequest::sign_request`] must derive both the |
| /// deadline check and the artifact from the same captured timestamp. |
| fn required_valid_until( |
| &self, |
| _credential: &Self::Credential, |
| expires_in: Option<Duration>, |
| ) -> Timestamp { |
| Timestamp::now() + expires_in.unwrap_or_default() |
| } |
| |
| /// Sign a request head. |
| /// |
| /// On `Err`, an implementation must leave the entire request head unchanged. On |
| /// `Ok`, it may change only `req.uri` and `req.headers`; the method, version, and |
| /// extensions remain caller-owned. [`crate::Signer`] enforces this commit boundary |
| /// when it invokes the implementation, but implementations must also uphold it for |
| /// callers that invoke this method directly. |
| /// |
| /// ## Credential |
| /// |
| /// The `credential` parameter is the credential required by the signer to sign the request. |
| /// Implementations with expiring credentials must validate it against |
| /// [`SignRequest::required_valid_until`] before mutating the request or performing |
| /// external signing calls. [`crate::Signer`] performs the same validation before |
| /// invoking this method, while direct callers rely on the implementation. |
| /// |
| /// ## Expires In |
| /// |
| /// The `expires_in` parameter requests a validity duration when the service supports |
| /// one. It is not a universal header-versus-query mode selector. Each service and |
| /// credential type defines whether the value selects presigning, configures an |
| /// expiration, is ignored, or is rejected. |
| fn sign_request<'a>( |
| &'a self, |
| ctx: &'a Context, |
| req: &'a mut http::request::Parts, |
| credential: Option<&'a Self::Credential>, |
| expires_in: Option<Duration>, |
| ) -> impl Future<Output = Result<()>> + MaybeSend + 'a; |
| } |
| |
| /// SignRequestDyn is the dyn version of [`SignRequest`]. |
| pub trait SignRequestDyn: Debug + Send + Sync + Unpin + 'static { |
| /// Credential used by this builder. |
| type Credential: Send + Sync + Unpin + 'static; |
| |
| /// Dyn version of [`SignRequest::required_valid_until`]. |
| fn required_valid_until_dyn( |
| &self, |
| _credential: &Self::Credential, |
| expires_in: Option<Duration>, |
| ) -> Timestamp { |
| Timestamp::now() + expires_in.unwrap_or_default() |
| } |
| |
| /// Dyn version of [`SignRequest::sign_request`]. |
| fn sign_request_dyn<'a>( |
| &'a self, |
| ctx: &'a Context, |
| req: &'a mut http::request::Parts, |
| credential: Option<&'a Self::Credential>, |
| expires_in: Option<Duration>, |
| ) -> BoxedFuture<'a, Result<()>>; |
| } |
| |
| impl<T> SignRequestDyn for T |
| where |
| T: SignRequest + ?Sized, |
| { |
| type Credential = T::Credential; |
| |
| fn required_valid_until_dyn( |
| &self, |
| credential: &Self::Credential, |
| expires_in: Option<Duration>, |
| ) -> Timestamp { |
| self.required_valid_until(credential, expires_in) |
| } |
| |
| fn sign_request_dyn<'a>( |
| &'a self, |
| ctx: &'a Context, |
| req: &'a mut http::request::Parts, |
| credential: Option<&'a Self::Credential>, |
| expires_in: Option<Duration>, |
| ) -> BoxedFuture<'a, Result<()>> { |
| Box::pin(self.sign_request(ctx, req, credential, expires_in)) |
| } |
| } |
| |
| impl<T> SignRequest for std::sync::Arc<T> |
| where |
| T: SignRequestDyn + ?Sized, |
| { |
| type Credential = T::Credential; |
| |
| fn required_valid_until( |
| &self, |
| credential: &Self::Credential, |
| expires_in: Option<Duration>, |
| ) -> Timestamp { |
| self.deref() |
| .required_valid_until_dyn(credential, expires_in) |
| } |
| |
| async fn sign_request( |
| &self, |
| ctx: &Context, |
| req: &mut http::request::Parts, |
| credential: Option<&Self::Credential>, |
| expires_in: Option<Duration>, |
| ) -> Result<()> { |
| self.deref() |
| .sign_request_dyn(ctx, req, credential, expires_in) |
| .await |
| } |
| } |
| |
| /// A chain of credential providers that will be tried in order. |
| /// |
| /// This is a generic implementation that can be used by any service to chain multiple |
| /// credential providers together. The chain will try each provider in order until one |
| /// returns credentials or all providers have been exhausted. |
| /// |
| /// # Example |
| /// |
| /// ```no_run |
| /// use reqsign_core::{ProvideCredentialChain, Context, ProvideCredential, Result}; |
| /// |
| /// #[derive(Debug)] |
| /// struct MyCredential { |
| /// token: String, |
| /// } |
| /// |
| /// #[derive(Debug)] |
| /// struct EnvironmentProvider; |
| /// |
| /// impl ProvideCredential for EnvironmentProvider { |
| /// type Credential = MyCredential; |
| /// |
| /// async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> { |
| /// // Implementation |
| /// Ok(None) |
| /// } |
| /// } |
| /// |
| /// # async fn example(ctx: Context) { |
| /// let chain = ProvideCredentialChain::new() |
| /// .push(EnvironmentProvider); |
| /// |
| /// let credentials = chain.provide_credential(&ctx).await; |
| /// # } |
| /// ``` |
| pub struct ProvideCredentialChain<C> { |
| providers: Vec<Box<dyn ProvideCredentialDyn<Credential = C>>>, |
| } |
| |
| impl<C> ProvideCredentialChain<C> |
| where |
| C: Send + Sync + Unpin + 'static, |
| { |
| /// Create a new empty credential provider chain. |
| pub fn new() -> Self { |
| Self { |
| providers: Vec::new(), |
| } |
| } |
| |
| /// Add a credential provider to the chain. |
| pub fn push(mut self, provider: impl ProvideCredential<Credential = C> + 'static) -> Self { |
| self.providers.push(Box::new(provider)); |
| self |
| } |
| |
| /// Add a credential provider to the front of the chain. |
| /// |
| /// This provider will be tried first before all existing providers. |
| pub fn push_front( |
| mut self, |
| provider: impl ProvideCredential<Credential = C> + 'static, |
| ) -> Self { |
| self.providers.insert(0, Box::new(provider)); |
| self |
| } |
| |
| /// Create a credential provider chain from a vector of providers. |
| pub fn from_vec(providers: Vec<Box<dyn ProvideCredentialDyn<Credential = C>>>) -> Self { |
| Self { providers } |
| } |
| |
| /// Get the number of providers in the chain. |
| pub fn len(&self) -> usize { |
| self.providers.len() |
| } |
| |
| /// Check if the chain is empty. |
| pub fn is_empty(&self) -> bool { |
| self.providers.is_empty() |
| } |
| } |
| |
| impl<C> Default for ProvideCredentialChain<C> |
| where |
| C: Send + Sync + Unpin + 'static, |
| { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
| |
| impl<C> Debug for ProvideCredentialChain<C> |
| where |
| C: Send + Sync + Unpin + 'static, |
| { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| f.debug_struct("ProvideCredentialChain") |
| .field("providers_count", &self.providers.len()) |
| .finish() |
| } |
| } |
| |
| impl<C> ProvideCredential for ProvideCredentialChain<C> |
| where |
| C: Send + Sync + Unpin + 'static, |
| { |
| type Credential = C; |
| |
| async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> { |
| for provider in &self.providers { |
| log::debug!("Trying credential provider: {provider:?}"); |
| |
| match provider.provide_credential_dyn(ctx).await { |
| Ok(Some(cred)) => { |
| log::debug!("Successfully loaded credential from provider: {provider:?}"); |
| return Ok(Some(cred)); |
| } |
| Ok(None) => { |
| log::debug!("No credential found in provider: {provider:?}"); |
| continue; |
| } |
| Err(e) => { |
| log::warn!("Error loading credential from provider {provider:?}: {e:?}"); |
| // Continue to next provider on error |
| continue; |
| } |
| } |
| } |
| |
| Ok(None) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[derive(Clone, Debug)] |
| struct ExactCredential { |
| valid_at: Timestamp, |
| } |
| |
| impl SigningCredential for ExactCredential { |
| fn is_valid(&self) -> bool { |
| false |
| } |
| |
| fn is_valid_at(&self, timestamp: Timestamp) -> bool { |
| self.valid_at == timestamp |
| } |
| } |
| |
| #[test] |
| fn option_forwards_exact_validity_check() { |
| let timestamp = Timestamp::from_second(42).expect("timestamp must be valid"); |
| let credential = Some(ExactCredential { |
| valid_at: timestamp, |
| }); |
| |
| assert!(credential.is_valid_at(timestamp)); |
| assert!(!credential.is_valid_at(timestamp + Duration::from_secs(1))); |
| assert!(!None::<ExactCredential>.is_valid_at(timestamp)); |
| } |
| } |