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}