aws_smithy_runtime_api/client/
identity.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::box_error::BoxError;
7use crate::client::runtime_components::sealed::ValidateConfig;
8use crate::client::runtime_components::{RuntimeComponents, RuntimeComponentsBuilder};
9use crate::impl_shared_conversions;
10use aws_smithy_types::config_bag::ConfigBag;
11use std::any::Any;
12use std::fmt;
13use std::fmt::Debug;
14use std::sync::atomic::{AtomicUsize, Ordering};
15use std::sync::Arc;
16use std::time::SystemTime;
17
18#[cfg(feature = "http-auth")]
19pub mod http;
20
21new_type_future! {
22    #[doc = "Future for [`IdentityResolver::resolve_identity`]."]
23    pub struct IdentityFuture<'a, Identity, BoxError>;
24}
25
26static NEXT_CACHE_PARTITION: AtomicUsize = AtomicUsize::new(0);
27
28/// Cache partition key for identity caching.
29///
30/// Identities need cache partitioning because a single identity cache is used across
31/// multiple identity providers across multiple auth schemes. In addition, a single auth scheme
32/// may have many different identity providers due to operation-level config overrides.
33///
34/// This partition _must_ be respected when retrieving from the identity cache and _should_
35/// be part of the cache key.
36///
37/// Calling [`IdentityCachePartition::new`] will create a new globally unique cache partition key,
38/// and the [`SharedIdentityResolver`] will automatically create and store a partion on construction.
39/// Thus, every configured identity resolver will be assigned a unique partition.
40#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
41pub struct IdentityCachePartition(usize);
42
43impl IdentityCachePartition {
44    /// Create a new globally unique cache partition key.
45    pub fn new() -> Self {
46        Self(NEXT_CACHE_PARTITION.fetch_add(1, Ordering::Relaxed))
47    }
48
49    /// Helper for unit tests to create an identity cache partition with a known value.
50    #[cfg(feature = "test-util")]
51    pub fn new_for_tests(value: usize) -> IdentityCachePartition {
52        Self(value)
53    }
54}
55
56/// Caching resolver for identities.
57pub trait ResolveCachedIdentity: fmt::Debug + Send + Sync {
58    /// Returns a cached identity, or resolves an identity and caches it if its not already cached.
59    fn resolve_cached_identity<'a>(
60        &'a self,
61        resolver: SharedIdentityResolver,
62        runtime_components: &'a RuntimeComponents,
63        config_bag: &'a ConfigBag,
64    ) -> IdentityFuture<'a>;
65
66    #[doc = include_str!("../../rustdoc/validate_base_client_config.md")]
67    fn validate_base_client_config(
68        &self,
69        runtime_components: &RuntimeComponentsBuilder,
70        cfg: &ConfigBag,
71    ) -> Result<(), BoxError> {
72        let _ = (runtime_components, cfg);
73        Ok(())
74    }
75
76    #[doc = include_str!("../../rustdoc/validate_final_config.md")]
77    fn validate_final_config(
78        &self,
79        runtime_components: &RuntimeComponents,
80        cfg: &ConfigBag,
81    ) -> Result<(), BoxError> {
82        let _ = (runtime_components, cfg);
83        Ok(())
84    }
85}
86
87/// Shared identity cache.
88#[derive(Clone, Debug)]
89pub struct SharedIdentityCache(Arc<dyn ResolveCachedIdentity>);
90
91impl SharedIdentityCache {
92    /// Creates a new [`SharedIdentityCache`] from the given cache implementation.
93    pub fn new(cache: impl ResolveCachedIdentity + 'static) -> Self {
94        Self(Arc::new(cache))
95    }
96}
97
98impl ResolveCachedIdentity for SharedIdentityCache {
99    fn resolve_cached_identity<'a>(
100        &'a self,
101        resolver: SharedIdentityResolver,
102        runtime_components: &'a RuntimeComponents,
103        config_bag: &'a ConfigBag,
104    ) -> IdentityFuture<'a> {
105        self.0
106            .resolve_cached_identity(resolver, runtime_components, config_bag)
107    }
108}
109
110impl ValidateConfig for SharedIdentityResolver {}
111
112impl ValidateConfig for SharedIdentityCache {
113    fn validate_base_client_config(
114        &self,
115        runtime_components: &RuntimeComponentsBuilder,
116        cfg: &ConfigBag,
117    ) -> Result<(), BoxError> {
118        self.0.validate_base_client_config(runtime_components, cfg)
119    }
120
121    fn validate_final_config(
122        &self,
123        runtime_components: &RuntimeComponents,
124        cfg: &ConfigBag,
125    ) -> Result<(), BoxError> {
126        self.0.validate_final_config(runtime_components, cfg)
127    }
128}
129
130impl_shared_conversions!(convert SharedIdentityCache from ResolveCachedIdentity using SharedIdentityCache::new);
131
132/// Resolver for identities.
133///
134/// Every [`AuthScheme`](crate::client::auth::AuthScheme) has one or more compatible
135/// identity resolvers, which are selected from runtime components by the auth scheme
136/// implementation itself.
137///
138/// The identity resolver must return an [`IdentityFuture`] with the resolved identity, or an error
139/// if resolution failed. There is no optionality for identity resolvers. The identity either
140/// resolves successfully, or it fails. The orchestrator will choose exactly one auth scheme
141/// to use, and thus, its chosen identity resolver is the only identity resolver that runs.
142/// There is no fallback to other auth schemes in the absence of an identity.
143pub trait ResolveIdentity: Send + Sync + Debug {
144    /// Asynchronously resolves an identity for a request using the given config.
145    fn resolve_identity<'a>(
146        &'a self,
147        runtime_components: &'a RuntimeComponents,
148        config_bag: &'a ConfigBag,
149    ) -> IdentityFuture<'a>;
150
151    /// Returns a fallback identity.
152    ///
153    /// This method should be used as a fallback plan, i.e., when a call to `resolve_identity`
154    /// is interrupted by a timeout and its future fails to complete.
155    ///
156    /// The fallback identity should be set aside and ready to be returned
157    /// immediately. Therefore, a new identity should NOT be fetched
158    /// within this method, which might cause a long-running operation.
159    fn fallback_on_interrupt(&self) -> Option<Identity> {
160        None
161    }
162
163    /// Returns the location of an identity cache associated with this identity resolver.
164    ///
165    /// By default, identity resolvers will use the identity cache stored in runtime components.
166    /// Implementing types can change the cache location if they want to. Refer to [`IdentityCacheLocation`]
167    /// explaining why a concrete identity resolver might want to change the cache location.
168    fn cache_location(&self) -> IdentityCacheLocation {
169        IdentityCacheLocation::RuntimeComponents
170    }
171
172    /// Returns the identity cache partition associated with this identity resolver.
173    ///
174    /// By default this returns `None` and cache partitioning is left up to `SharedIdentityResolver`.
175    fn cache_partition(&self) -> Option<IdentityCachePartition> {
176        None
177    }
178}
179
180/// Cache location for identity caching.
181///
182/// Identities are usually cached in the identity cache owned by [`RuntimeComponents`]. However,
183/// we do have identities whose caching mechanism is internally managed by their identity resolver,
184/// in which case we want to avoid the `RuntimeComponents`-owned identity cache interfering with
185/// the internal caching policy.
186#[non_exhaustive]
187#[derive(Copy, Clone, Debug, Eq, PartialEq)]
188pub enum IdentityCacheLocation {
189    /// Indicates the identity cache is owned by [`RuntimeComponents`].
190    RuntimeComponents,
191    /// Indicates the identity cache is internally managed by the identity resolver.
192    IdentityResolver,
193}
194
195/// Container for a shared identity resolver.
196#[derive(Clone, Debug)]
197pub struct SharedIdentityResolver {
198    inner: Arc<dyn ResolveIdentity>,
199    cache_partition: IdentityCachePartition,
200}
201
202impl SharedIdentityResolver {
203    /// Creates a new [`SharedIdentityResolver`] from the given resolver.
204    pub fn new(resolver: impl ResolveIdentity + 'static) -> Self {
205        // NOTE: `IdentityCachePartition` is globally unique by construction so even
206        // custom implementations of `ResolveIdentity::cache_partition()` are unique.
207        let partition = match resolver.cache_partition() {
208            Some(p) => p,
209            None => IdentityCachePartition::new(),
210        };
211
212        Self {
213            inner: Arc::new(resolver),
214            cache_partition: partition,
215        }
216    }
217
218    /// Returns the globally unique cache partition key for this identity resolver.
219    ///
220    /// See the [`IdentityCachePartition`] docs for more information on what this is used for
221    /// and why.
222    pub fn cache_partition(&self) -> IdentityCachePartition {
223        self.cache_partition
224    }
225}
226
227impl ResolveIdentity for SharedIdentityResolver {
228    fn resolve_identity<'a>(
229        &'a self,
230        runtime_components: &'a RuntimeComponents,
231        config_bag: &'a ConfigBag,
232    ) -> IdentityFuture<'a> {
233        self.inner.resolve_identity(runtime_components, config_bag)
234    }
235
236    fn cache_location(&self) -> IdentityCacheLocation {
237        self.inner.cache_location()
238    }
239
240    fn cache_partition(&self) -> Option<IdentityCachePartition> {
241        Some(self.cache_partition())
242    }
243}
244
245impl_shared_conversions!(convert SharedIdentityResolver from ResolveIdentity using SharedIdentityResolver::new);
246
247/// An identity that can be used for authentication.
248///
249/// The [`Identity`] is a container for any arbitrary identity data that may be used
250/// by a [`Sign`](crate::client::auth::Sign) implementation. Under the hood, it
251/// has an `Arc<dyn Any>`, and it is the responsibility of the signer to downcast
252/// to the appropriate data type using the `data()` function.
253///
254/// The `Identity` also holds an optional expiration time, which may duplicate
255/// an expiration time on the identity data. This is because an `Arc<dyn Any>`
256/// can't be downcast to any arbitrary trait, and expiring identities are
257/// common enough to be built-in.
258#[derive(Clone)]
259pub struct Identity {
260    data: Arc<dyn Any + Send + Sync>,
261    #[allow(clippy::type_complexity)]
262    data_debug: Arc<dyn (Fn(&Arc<dyn Any + Send + Sync>) -> &dyn Debug) + Send + Sync>,
263    expiration: Option<SystemTime>,
264}
265
266impl Identity {
267    /// Creates a new identity with the given data and expiration time.
268    pub fn new<T>(data: T, expiration: Option<SystemTime>) -> Self
269    where
270        T: Any + Debug + Send + Sync,
271    {
272        Self {
273            data: Arc::new(data),
274            data_debug: Arc::new(|d| d.downcast_ref::<T>().expect("type-checked") as _),
275            expiration,
276        }
277    }
278
279    /// Returns the raw identity data.
280    pub fn data<T: Any + Debug + Send + Sync + 'static>(&self) -> Option<&T> {
281        self.data.downcast_ref()
282    }
283
284    /// Returns the expiration time for this identity, if any.
285    pub fn expiration(&self) -> Option<SystemTime> {
286        self.expiration
287    }
288}
289
290impl Debug for Identity {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        f.debug_struct("Identity")
293            .field("data", (self.data_debug)(&self.data))
294            .field("expiration", &self.expiration)
295            .finish()
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use aws_smithy_async::time::{SystemTimeSource, TimeSource};
303
304    #[test]
305    fn check_send_sync() {
306        fn is_send_sync<T: Send + Sync>(_: T) {}
307        is_send_sync(Identity::new("foo", None));
308    }
309
310    #[test]
311    fn create_retrieve_identity() {
312        #[derive(Debug)]
313        struct MyIdentityData {
314            first: String,
315            last: String,
316        }
317
318        let ts = SystemTimeSource::new();
319        let expiration = ts.now();
320        let identity = Identity::new(
321            MyIdentityData {
322                first: "foo".into(),
323                last: "bar".into(),
324            },
325            Some(expiration),
326        );
327
328        assert_eq!("foo", identity.data::<MyIdentityData>().unwrap().first);
329        assert_eq!("bar", identity.data::<MyIdentityData>().unwrap().last);
330        assert_eq!(Some(expiration), identity.expiration());
331    }
332}