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}