aws_smithy_runtime_api/client/
auth.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! APIs for request authentication.
7
8use crate::box_error::BoxError;
9use crate::client::identity::{Identity, SharedIdentityResolver};
10use crate::client::orchestrator::HttpRequest;
11use crate::client::runtime_components::sealed::ValidateConfig;
12use crate::client::runtime_components::{GetIdentityResolver, RuntimeComponents};
13use crate::impl_shared_conversions;
14use aws_smithy_types::config_bag::{ConfigBag, FrozenLayer, Storable, StoreReplace};
15use aws_smithy_types::type_erasure::TypeErasedBox;
16use aws_smithy_types::Document;
17use std::borrow::Cow;
18use std::fmt::{self, Debug};
19use std::sync::Arc;
20
21/// Auth schemes for the HTTP `Authorization` header.
22#[cfg(feature = "http-auth")]
23pub mod http;
24
25/// Static auth scheme option resolver.
26pub mod static_resolver;
27
28/// The output type from the [`ResolveAuthSchemeOptions::resolve_auth_scheme_options_v2`]
29///
30/// The resolver returns a list of these, in the order the auth scheme resolver wishes to use them.
31#[derive(Clone, Debug)]
32pub struct AuthSchemeOption {
33    scheme_id: AuthSchemeId,
34    properties: Option<FrozenLayer>,
35}
36
37impl AuthSchemeOption {
38    /// Builder struct for [`AuthSchemeOption`]
39    pub fn builder() -> AuthSchemeOptionBuilder {
40        AuthSchemeOptionBuilder::default()
41    }
42
43    /// Returns [`AuthSchemeId`], the ID of the scheme
44    pub fn scheme_id(&self) -> &AuthSchemeId {
45        &self.scheme_id
46    }
47
48    /// Returns optional properties for identity resolution or signing
49    ///
50    /// This config layer is applied to the [`ConfigBag`] to ensure the information is
51    /// available during both the identity resolution and signature generation processes.
52    pub fn properties(&self) -> Option<FrozenLayer> {
53        self.properties.clone()
54    }
55}
56
57impl From<AuthSchemeId> for AuthSchemeOption {
58    fn from(auth_scheme_id: AuthSchemeId) -> Self {
59        AuthSchemeOption::builder()
60            .scheme_id(auth_scheme_id)
61            .build()
62            .expect("required fields set")
63    }
64}
65
66/// Builder struct for [`AuthSchemeOption`]
67#[derive(Debug, Default)]
68pub struct AuthSchemeOptionBuilder {
69    scheme_id: Option<AuthSchemeId>,
70    properties: Option<FrozenLayer>,
71}
72
73impl AuthSchemeOptionBuilder {
74    /// Sets [`AuthSchemeId`] for the builder
75    pub fn scheme_id(mut self, auth_scheme_id: AuthSchemeId) -> Self {
76        self.set_scheme_id(Some(auth_scheme_id));
77        self
78    }
79
80    /// Sets [`AuthSchemeId`] for the builder
81    pub fn set_scheme_id(&mut self, auth_scheme_id: Option<AuthSchemeId>) {
82        self.scheme_id = auth_scheme_id;
83    }
84
85    /// Sets the properties for the builder
86    pub fn properties(mut self, properties: FrozenLayer) -> Self {
87        self.set_properties(Some(properties));
88        self
89    }
90
91    /// Sets the properties for the builder
92    pub fn set_properties(&mut self, properties: Option<FrozenLayer>) {
93        self.properties = properties;
94    }
95
96    /// Builds an [`AuthSchemeOption`], otherwise returns an [`AuthSchemeOptionBuilderError`] in the case of error
97    pub fn build(self) -> Result<AuthSchemeOption, AuthSchemeOptionBuilderError> {
98        let scheme_id = self
99            .scheme_id
100            .ok_or(ErrorKind::MissingRequiredField("auth_scheme_id"))?;
101        Ok(AuthSchemeOption {
102            scheme_id,
103            properties: self.properties,
104        })
105    }
106}
107
108#[derive(Debug)]
109enum ErrorKind {
110    MissingRequiredField(&'static str),
111}
112
113impl From<ErrorKind> for AuthSchemeOptionBuilderError {
114    fn from(kind: ErrorKind) -> Self {
115        Self { kind }
116    }
117}
118
119/// The error type returned when failing to build [`AuthSchemeOption`] from the builder
120#[derive(Debug)]
121pub struct AuthSchemeOptionBuilderError {
122    kind: ErrorKind,
123}
124
125impl fmt::Display for AuthSchemeOptionBuilderError {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        match self.kind {
128            ErrorKind::MissingRequiredField(name) => {
129                write!(f, "`{name}` is required")
130            }
131        }
132    }
133}
134
135impl std::error::Error for AuthSchemeOptionBuilderError {}
136
137/// New type around an auth scheme ID.
138///
139/// Each auth scheme must have a unique string identifier associated with it,
140/// which is used to refer to auth schemes by the auth scheme option resolver, and
141/// also used to select an identity resolver to use.
142#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
143pub struct AuthSchemeId {
144    scheme_id: Cow<'static, str>,
145}
146
147// See: https://doc.rust-lang.org/std/convert/trait.AsRef.html#reflexivity
148impl AsRef<AuthSchemeId> for AuthSchemeId {
149    fn as_ref(&self) -> &AuthSchemeId {
150        self
151    }
152}
153
154impl AuthSchemeId {
155    /// Creates a new auth scheme ID.
156    pub const fn new(scheme_id: &'static str) -> Self {
157        Self {
158            scheme_id: Cow::Borrowed(scheme_id),
159        }
160    }
161
162    /// Returns the string equivalent of this auth scheme ID.
163    #[deprecated(
164        note = "This function is no longer functional. Use `inner` instead",
165        since = "1.8.0"
166    )]
167    pub const fn as_str(&self) -> &'static str {
168        match self.scheme_id {
169            Cow::Borrowed(val) => val,
170            Cow::Owned(_) => {
171                // cannot obtain `&'static str` from `String` unless we use `Box::leak`
172                ""
173            }
174        }
175    }
176
177    /// Returns the string equivalent of this auth scheme ID.
178    pub fn inner(&self) -> &str {
179        &self.scheme_id
180    }
181}
182
183impl From<&'static str> for AuthSchemeId {
184    fn from(scheme_id: &'static str) -> Self {
185        Self::new(scheme_id)
186    }
187}
188
189impl From<Cow<'static, str>> for AuthSchemeId {
190    fn from(scheme_id: Cow<'static, str>) -> Self {
191        Self { scheme_id }
192    }
193}
194
195/// Parameters needed to resolve auth scheme options.
196///
197/// Most generated clients will use the [`StaticAuthSchemeOptionResolver`](static_resolver::StaticAuthSchemeOptionResolver),
198/// which doesn't require any parameters for resolution (and has its own empty params struct).
199///
200/// However, more complex auth scheme resolvers may need modeled parameters in order to resolve
201/// the auth scheme options. For those, this params struct holds a type erased box so that any
202/// kind of parameters can be contained within, and type casted by the auth scheme option resolver
203/// implementation.
204#[derive(Debug)]
205pub struct AuthSchemeOptionResolverParams(TypeErasedBox);
206
207impl AuthSchemeOptionResolverParams {
208    /// Creates a new [`AuthSchemeOptionResolverParams`].
209    pub fn new<T: fmt::Debug + Send + Sync + 'static>(params: T) -> Self {
210        Self(TypeErasedBox::new(params))
211    }
212
213    /// Returns the underlying parameters as the type `T` if they are that type.
214    pub fn get<T: fmt::Debug + Send + Sync + 'static>(&self) -> Option<&T> {
215        self.0.downcast_ref()
216    }
217}
218
219impl Storable for AuthSchemeOptionResolverParams {
220    type Storer = StoreReplace<Self>;
221}
222
223new_type_future! {
224    #[doc = "Future for [`ResolveAuthSchemeOptions::resolve_auth_scheme_options_v2`]."]
225    pub struct AuthSchemeOptionsFuture<'a, Vec<AuthSchemeOption>, BoxError>;
226}
227
228// Currently, we don't add `map_ok` to the `new_type_future` macro in general.
229// It's specifically used for `AuthSchemeOptionsFuture`, but we can expand it later if needed.
230impl<'a> AuthSchemeOptionsFuture<'a> {
231    /// Transforms the `Ok` variant inside this `AuthSchemeOptionsFuture` by applying the provided function.
232    ///
233    /// This method maps over the `Ok` variant of the `Result` wrapped by the future,
234    /// applying `map_fn` to the contained `Vec<AuthSchemeOption>`.
235    ///
236    /// The transformation is applied regardless of whether the future's value is already
237    /// available (`Now`) or will be computed asynchronously (`Later`).
238    pub fn map_ok<F>(self, f: F) -> AuthSchemeOptionsFuture<'a>
239    where
240        F: FnOnce(Vec<AuthSchemeOption>) -> Vec<AuthSchemeOption> + Send + 'a,
241    {
242        let inner = self.inner.map_boxed(|result| result.map(f));
243        Self { inner }
244    }
245}
246
247/// Resolver for auth scheme options.
248///
249/// The orchestrator needs to select an auth scheme to sign requests with, and potentially
250/// from several different available auth schemes. Smithy models have a number of ways
251/// to specify which operations can use which auth schemes under which conditions, as
252/// documented in the [Smithy spec](https://smithy.io/2.0/spec/authentication-traits.html).
253///
254/// The orchestrator uses the auth scheme option resolver runtime component to resolve
255/// an ordered list of options that are available to choose from for a given request.
256/// This resolver can be a simple static list, such as with the
257/// [`StaticAuthSchemeOptionResolver`](static_resolver::StaticAuthSchemeOptionResolver),
258/// or it can be a complex code generated resolver that incorporates parameters from both
259/// the model and the resolved endpoint.
260pub trait ResolveAuthSchemeOptions: Send + Sync + fmt::Debug {
261    #[deprecated(
262        note = "This method is deprecated, use `resolve_auth_scheme_options_v2` instead.",
263        since = "1.8.0"
264    )]
265    /// Returns a list of available auth scheme options to choose from.
266    fn resolve_auth_scheme_options(
267        &self,
268        _params: &AuthSchemeOptionResolverParams,
269    ) -> Result<Cow<'_, [AuthSchemeId]>, BoxError> {
270        unimplemented!("This method is deprecated, use `resolve_auth_scheme_options_v2` instead.");
271    }
272
273    #[allow(deprecated)]
274    /// Returns a list of available auth scheme options to choose from.
275    fn resolve_auth_scheme_options_v2<'a>(
276        &'a self,
277        params: &'a AuthSchemeOptionResolverParams,
278        _cfg: &'a ConfigBag,
279        _runtime_components: &'a RuntimeComponents,
280    ) -> AuthSchemeOptionsFuture<'a> {
281        AuthSchemeOptionsFuture::ready({
282            self.resolve_auth_scheme_options(params).map(|options| {
283                options
284                    .iter()
285                    .cloned()
286                    .map(|scheme_id| {
287                        AuthSchemeOption::builder()
288                            .scheme_id(scheme_id)
289                            .build()
290                            .expect("required fields set")
291                    })
292                    .collect::<Vec<_>>()
293            })
294        })
295    }
296}
297
298/// A shared auth scheme option resolver.
299#[derive(Clone, Debug)]
300pub struct SharedAuthSchemeOptionResolver(Arc<dyn ResolveAuthSchemeOptions>);
301
302impl SharedAuthSchemeOptionResolver {
303    /// Creates a new [`SharedAuthSchemeOptionResolver`].
304    pub fn new(auth_scheme_option_resolver: impl ResolveAuthSchemeOptions + 'static) -> Self {
305        Self(Arc::new(auth_scheme_option_resolver))
306    }
307}
308
309impl ResolveAuthSchemeOptions for SharedAuthSchemeOptionResolver {
310    #[allow(deprecated)]
311    fn resolve_auth_scheme_options(
312        &self,
313        params: &AuthSchemeOptionResolverParams,
314    ) -> Result<Cow<'_, [AuthSchemeId]>, BoxError> {
315        (*self.0).resolve_auth_scheme_options(params)
316    }
317
318    fn resolve_auth_scheme_options_v2<'a>(
319        &'a self,
320        params: &'a AuthSchemeOptionResolverParams,
321        cfg: &'a ConfigBag,
322        runtime_components: &'a RuntimeComponents,
323    ) -> AuthSchemeOptionsFuture<'a> {
324        (*self.0).resolve_auth_scheme_options_v2(params, cfg, runtime_components)
325    }
326}
327
328impl_shared_conversions!(
329    convert SharedAuthSchemeOptionResolver
330    from ResolveAuthSchemeOptions
331    using SharedAuthSchemeOptionResolver::new
332);
333
334/// An auth scheme.
335///
336/// Auth schemes have unique identifiers (the `scheme_id`),
337/// and provide an identity resolver and a signer.
338pub trait AuthScheme: Send + Sync + fmt::Debug {
339    /// Returns the unique identifier associated with this auth scheme.
340    ///
341    /// This identifier is used to refer to this auth scheme from the
342    /// [`ResolveAuthSchemeOptions`], and is also associated with
343    /// identity resolvers in the config.
344    fn scheme_id(&self) -> AuthSchemeId;
345
346    /// Returns the identity resolver that can resolve an identity for this scheme, if one is available.
347    ///
348    /// The [`AuthScheme`] doesn't actually own an identity resolver. Rather, identity resolvers
349    /// are configured as runtime components. The auth scheme merely chooses a compatible identity
350    /// resolver from the runtime components via the [`GetIdentityResolver`] trait. The trait is
351    /// given rather than the full set of runtime components to prevent complex resolution logic
352    /// involving multiple components from taking place in this function, since that's not the
353    /// intended use of this design.
354    fn identity_resolver(
355        &self,
356        identity_resolvers: &dyn GetIdentityResolver,
357    ) -> Option<SharedIdentityResolver>;
358
359    /// Returns the signing implementation for this auth scheme.
360    fn signer(&self) -> &dyn Sign;
361}
362
363/// Container for a shared auth scheme implementation.
364#[derive(Clone, Debug)]
365pub struct SharedAuthScheme(Arc<dyn AuthScheme>);
366
367impl SharedAuthScheme {
368    /// Creates a new [`SharedAuthScheme`] from the given auth scheme.
369    pub fn new(auth_scheme: impl AuthScheme + 'static) -> Self {
370        Self(Arc::new(auth_scheme))
371    }
372}
373
374impl AuthScheme for SharedAuthScheme {
375    fn scheme_id(&self) -> AuthSchemeId {
376        self.0.scheme_id()
377    }
378
379    fn identity_resolver(
380        &self,
381        identity_resolvers: &dyn GetIdentityResolver,
382    ) -> Option<SharedIdentityResolver> {
383        self.0.identity_resolver(identity_resolvers)
384    }
385
386    fn signer(&self) -> &dyn Sign {
387        self.0.signer()
388    }
389}
390
391impl ValidateConfig for SharedAuthScheme {}
392
393impl_shared_conversions!(convert SharedAuthScheme from AuthScheme using SharedAuthScheme::new);
394
395/// Signing implementation for an auth scheme.
396pub trait Sign: Send + Sync + fmt::Debug {
397    /// Sign the given request with the given identity, components, and config.
398    ///
399    /// If the provided identity is incompatible with this signer, an error must be returned.
400    fn sign_http_request(
401        &self,
402        request: &mut HttpRequest,
403        identity: &Identity,
404        auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
405        runtime_components: &RuntimeComponents,
406        config_bag: &ConfigBag,
407    ) -> Result<(), BoxError>;
408}
409
410/// Endpoint configuration for the selected auth scheme.
411///
412/// The configuration held by this struct originates from the endpoint rule set in the service model.
413///
414/// This struct gets added to the request state by the auth orchestrator.
415#[non_exhaustive]
416#[derive(Clone, Debug)]
417pub struct AuthSchemeEndpointConfig<'a>(Option<&'a Document>);
418
419impl<'a> AuthSchemeEndpointConfig<'a> {
420    /// Creates an empty [`AuthSchemeEndpointConfig`].
421    pub fn empty() -> Self {
422        Self(None)
423    }
424
425    /// Returns the endpoint configuration as a [`Document`].
426    pub fn as_document(&self) -> Option<&'a Document> {
427        self.0
428    }
429}
430
431impl<'a> From<Option<&'a Document>> for AuthSchemeEndpointConfig<'a> {
432    fn from(value: Option<&'a Document>) -> Self {
433        Self(value)
434    }
435}
436
437impl<'a> From<&'a Document> for AuthSchemeEndpointConfig<'a> {
438    fn from(value: &'a Document) -> Self {
439        Self(Some(value))
440    }
441}
442
443/// An ordered list of [AuthSchemeId]s
444///
445/// Can be used to reorder already-resolved auth schemes by an auth scheme resolver.
446/// This list is intended as a hint rather than a strict override;
447/// any schemes not present in the resolved auth schemes will be ignored.
448#[derive(Clone, Debug, Default, Eq, PartialEq)]
449pub struct AuthSchemePreference {
450    preference_list: Vec<AuthSchemeId>,
451}
452
453impl Storable for AuthSchemePreference {
454    type Storer = StoreReplace<Self>;
455}
456
457impl IntoIterator for AuthSchemePreference {
458    type Item = AuthSchemeId;
459    type IntoIter = std::vec::IntoIter<Self::Item>;
460
461    fn into_iter(self) -> Self::IntoIter {
462        self.preference_list.into_iter()
463    }
464}
465
466impl<T> From<T> for AuthSchemePreference
467where
468    T: AsRef<[AuthSchemeId]>,
469{
470    fn from(slice: T) -> Self {
471        AuthSchemePreference {
472            preference_list: slice.as_ref().to_vec(),
473        }
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    #[tokio::test]
482    async fn test_map_ok_now_variant() {
483        let input = ["sigv4", "http"]
484            .map(|s| AuthSchemeOption::from(AuthSchemeId::from(s)))
485            .to_vec();
486        let fut = AuthSchemeOptionsFuture::ready(Ok(input));
487
488        let mapped = fut.map_ok(|opts| {
489            opts.into_iter()
490                .filter(|opt| opt.scheme_id().inner() != "http")
491                .collect()
492        });
493
494        let result = mapped.await;
495        assert!(result.is_ok());
496        let vec = result.unwrap();
497        assert_eq!(1, vec.len());
498        assert_eq!("sigv4", vec[0].scheme_id().inner());
499    }
500
501    #[tokio::test]
502    async fn test_map_ok_now_variant_error_no_op() {
503        let fut = AuthSchemeOptionsFuture::ready(Err(BoxError::from("oops")));
504
505        let mapped = fut.map_ok(|opts| opts); // no-op
506
507        let result = mapped.await;
508        assert!(result.is_err());
509        assert_eq!(result.unwrap_err().to_string(), "oops");
510    }
511
512    #[tokio::test]
513    async fn test_map_ok_later_variant() {
514        let input = ["foo", "bar"]
515            .map(|s| AuthSchemeOption::from(AuthSchemeId::from(s)))
516            .to_vec();
517        let fut = AuthSchemeOptionsFuture::new(async move { Ok(input) });
518
519        let mapped = fut.map_ok(|opts| {
520            opts.into_iter()
521                .map(|mut opt| {
522                    opt.scheme_id =
523                        AuthSchemeId::from(Cow::Owned(opt.scheme_id().inner().to_uppercase()));
524                    opt
525                })
526                .collect()
527        });
528
529        let result = mapped.await;
530        assert!(result.is_ok());
531        let vec = result.unwrap();
532        assert_eq!(vec[0].scheme_id().inner(), "FOO");
533        assert_eq!(vec[1].scheme_id().inner(), "BAR");
534    }
535
536    #[tokio::test]
537    async fn test_map_ok_later_variant_error_no_op() {
538        let fut = AuthSchemeOptionsFuture::new(async move { Err(BoxError::from("later fail")) });
539
540        let mapped = fut.map_ok(|opts| opts); // no-op
541
542        let result = mapped.await;
543        assert!(result.is_err());
544        assert_eq!(result.unwrap_err().to_string(), "later fail");
545    }
546}