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 aws_smithy_types::type_erasure::TypeErasedBox;
12use std::any::{Any, TypeId};
13use std::collections::HashMap;
14use std::fmt;
15use std::fmt::Debug;
16use std::sync::atomic::{AtomicUsize, Ordering};
17use std::sync::Arc;
18use std::time::SystemTime;
19
20#[cfg(feature = "http-auth")]
21pub mod http;
22
23new_type_future! {
24    #[doc = "Future for [`IdentityResolver::resolve_identity`]."]
25    pub struct IdentityFuture<'a, Identity, BoxError>;
26}
27
28static NEXT_CACHE_PARTITION: AtomicUsize = AtomicUsize::new(0);
29
30/// Cache partition key for identity caching.
31///
32/// Identities need cache partitioning because a single identity cache is used across
33/// multiple identity providers across multiple auth schemes. In addition, a single auth scheme
34/// may have many different identity providers due to operation-level config overrides.
35///
36/// This partition _must_ be respected when retrieving from the identity cache and _should_
37/// be part of the cache key.
38///
39/// Calling [`IdentityCachePartition::new`] will create a new globally unique cache partition key,
40/// and the [`SharedIdentityResolver`] will automatically create and store a partion on construction.
41/// Thus, every configured identity resolver will be assigned a unique partition.
42#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
43pub struct IdentityCachePartition(usize);
44
45impl IdentityCachePartition {
46    /// Create a new globally unique cache partition key.
47    pub fn new() -> Self {
48        Self(NEXT_CACHE_PARTITION.fetch_add(1, Ordering::Relaxed))
49    }
50
51    /// Helper for unit tests to create an identity cache partition with a known value.
52    #[cfg(feature = "test-util")]
53    pub fn new_for_tests(value: usize) -> IdentityCachePartition {
54        Self(value)
55    }
56}
57
58/// Caching resolver for identities.
59pub trait ResolveCachedIdentity: fmt::Debug + Send + Sync {
60    /// Returns a cached identity, or resolves an identity and caches it if its not already cached.
61    fn resolve_cached_identity<'a>(
62        &'a self,
63        resolver: SharedIdentityResolver,
64        runtime_components: &'a RuntimeComponents,
65        config_bag: &'a ConfigBag,
66    ) -> IdentityFuture<'a>;
67
68    #[doc = include_str!("../../rustdoc/validate_base_client_config.md")]
69    fn validate_base_client_config(
70        &self,
71        runtime_components: &RuntimeComponentsBuilder,
72        cfg: &ConfigBag,
73    ) -> Result<(), BoxError> {
74        let _ = (runtime_components, cfg);
75        Ok(())
76    }
77
78    #[doc = include_str!("../../rustdoc/validate_final_config.md")]
79    fn validate_final_config(
80        &self,
81        runtime_components: &RuntimeComponents,
82        cfg: &ConfigBag,
83    ) -> Result<(), BoxError> {
84        let _ = (runtime_components, cfg);
85        Ok(())
86    }
87}
88
89/// Shared identity cache.
90#[derive(Clone, Debug)]
91pub struct SharedIdentityCache(Arc<dyn ResolveCachedIdentity>);
92
93impl SharedIdentityCache {
94    /// Creates a new [`SharedIdentityCache`] from the given cache implementation.
95    pub fn new(cache: impl ResolveCachedIdentity + 'static) -> Self {
96        Self(Arc::new(cache))
97    }
98}
99
100impl ResolveCachedIdentity for SharedIdentityCache {
101    fn resolve_cached_identity<'a>(
102        &'a self,
103        resolver: SharedIdentityResolver,
104        runtime_components: &'a RuntimeComponents,
105        config_bag: &'a ConfigBag,
106    ) -> IdentityFuture<'a> {
107        self.0
108            .resolve_cached_identity(resolver, runtime_components, config_bag)
109    }
110}
111
112impl ValidateConfig for SharedIdentityResolver {}
113
114impl ValidateConfig for SharedIdentityCache {
115    fn validate_base_client_config(
116        &self,
117        runtime_components: &RuntimeComponentsBuilder,
118        cfg: &ConfigBag,
119    ) -> Result<(), BoxError> {
120        self.0.validate_base_client_config(runtime_components, cfg)
121    }
122
123    fn validate_final_config(
124        &self,
125        runtime_components: &RuntimeComponents,
126        cfg: &ConfigBag,
127    ) -> Result<(), BoxError> {
128        self.0.validate_final_config(runtime_components, cfg)
129    }
130}
131
132impl_shared_conversions!(convert SharedIdentityCache from ResolveCachedIdentity using SharedIdentityCache::new);
133
134/// Resolver for identities.
135///
136/// Every [`AuthScheme`](crate::client::auth::AuthScheme) has one or more compatible
137/// identity resolvers, which are selected from runtime components by the auth scheme
138/// implementation itself.
139///
140/// The identity resolver must return an [`IdentityFuture`] with the resolved identity, or an error
141/// if resolution failed. There is no optionality for identity resolvers. The identity either
142/// resolves successfully, or it fails. The orchestrator will choose exactly one auth scheme
143/// to use, and thus, its chosen identity resolver is the only identity resolver that runs.
144/// There is no fallback to other auth schemes in the absence of an identity.
145pub trait ResolveIdentity: Send + Sync + Debug {
146    /// Asynchronously resolves an identity for a request using the given config.
147    fn resolve_identity<'a>(
148        &'a self,
149        runtime_components: &'a RuntimeComponents,
150        config_bag: &'a ConfigBag,
151    ) -> IdentityFuture<'a>;
152
153    /// Returns a fallback identity.
154    ///
155    /// This method should be used as a fallback plan, i.e., when a call to `resolve_identity`
156    /// is interrupted by a timeout and its future fails to complete.
157    ///
158    /// The fallback identity should be set aside and ready to be returned
159    /// immediately. Therefore, a new identity should NOT be fetched
160    /// within this method, which might cause a long-running operation.
161    fn fallback_on_interrupt(&self) -> Option<Identity> {
162        None
163    }
164
165    /// Returns the location of an identity cache associated with this identity resolver.
166    ///
167    /// By default, identity resolvers will use the identity cache stored in runtime components.
168    /// Implementing types can change the cache location if they want to. Refer to [`IdentityCacheLocation`]
169    /// explaining why a concrete identity resolver might want to change the cache location.
170    fn cache_location(&self) -> IdentityCacheLocation {
171        IdentityCacheLocation::RuntimeComponents
172    }
173
174    /// Returns the identity cache partition associated with this identity resolver.
175    ///
176    /// By default this returns `None` and cache partitioning is left up to `SharedIdentityResolver`.
177    fn cache_partition(&self) -> Option<IdentityCachePartition> {
178        None
179    }
180}
181
182/// Cache location for identity caching.
183///
184/// Identities are usually cached in the identity cache owned by [`RuntimeComponents`]. However,
185/// we do have identities whose caching mechanism is internally managed by their identity resolver,
186/// in which case we want to avoid the `RuntimeComponents`-owned identity cache interfering with
187/// the internal caching policy.
188#[non_exhaustive]
189#[derive(Copy, Clone, Debug, Eq, PartialEq)]
190pub enum IdentityCacheLocation {
191    /// Indicates the identity cache is owned by [`RuntimeComponents`].
192    RuntimeComponents,
193    /// Indicates the identity cache is internally managed by the identity resolver.
194    IdentityResolver,
195}
196
197/// Container for a shared identity resolver.
198#[derive(Clone, Debug)]
199pub struct SharedIdentityResolver {
200    inner: Arc<dyn ResolveIdentity>,
201    cache_partition: IdentityCachePartition,
202}
203
204impl SharedIdentityResolver {
205    /// Creates a new [`SharedIdentityResolver`] from the given resolver.
206    pub fn new(resolver: impl ResolveIdentity + 'static) -> Self {
207        // NOTE: `IdentityCachePartition` is globally unique by construction so even
208        // custom implementations of `ResolveIdentity::cache_partition()` are unique.
209        let partition = match resolver.cache_partition() {
210            Some(p) => p,
211            None => IdentityCachePartition::new(),
212        };
213
214        Self {
215            inner: Arc::new(resolver),
216            cache_partition: partition,
217        }
218    }
219
220    /// Returns the globally unique cache partition key for this identity resolver.
221    ///
222    /// See the [`IdentityCachePartition`] docs for more information on what this is used for
223    /// and why.
224    pub fn cache_partition(&self) -> IdentityCachePartition {
225        self.cache_partition
226    }
227}
228
229impl ResolveIdentity for SharedIdentityResolver {
230    fn resolve_identity<'a>(
231        &'a self,
232        runtime_components: &'a RuntimeComponents,
233        config_bag: &'a ConfigBag,
234    ) -> IdentityFuture<'a> {
235        self.inner.resolve_identity(runtime_components, config_bag)
236    }
237
238    fn cache_location(&self) -> IdentityCacheLocation {
239        self.inner.cache_location()
240    }
241
242    fn cache_partition(&self) -> Option<IdentityCachePartition> {
243        Some(self.cache_partition())
244    }
245}
246
247impl_shared_conversions!(convert SharedIdentityResolver from ResolveIdentity using SharedIdentityResolver::new);
248
249type DataDebug = Arc<dyn (Fn(&Arc<dyn Any + Send + Sync>) -> &dyn Debug) + Send + Sync>;
250
251/// An identity that can be used for authentication.
252///
253/// The [`Identity`] is a container for any arbitrary identity data that may be used
254/// by a [`Sign`](crate::client::auth::Sign) implementation. Under the hood, it
255/// has an `Arc<dyn Any>`, and it is the responsibility of the signer to downcast
256/// to the appropriate data type using the `data()` function.
257///
258/// The `Identity` also holds an optional expiration time, which may duplicate
259/// an expiration time on the identity data. This is because an `Arc<dyn Any>`
260/// can't be downcast to any arbitrary trait, and expiring identities are
261/// common enough to be built-in.
262#[derive(Clone)]
263pub struct Identity {
264    data: Arc<dyn Any + Send + Sync>,
265    data_debug: DataDebug,
266    expiration: Option<SystemTime>,
267    properties: HashMap<TypeId, Arc<TypeErasedBox>>,
268}
269
270impl Identity {
271    /// Creates a new identity with the given data and expiration time.
272    pub fn new<T>(data: T, expiration: Option<SystemTime>) -> Self
273    where
274        T: Any + Debug + Send + Sync,
275    {
276        Self {
277            data: Arc::new(data),
278            data_debug: Arc::new(|d| d.downcast_ref::<T>().expect("type-checked") as _),
279            expiration,
280            properties: HashMap::default(),
281        }
282    }
283
284    /// Returns [`Builder`] for [`Identity`].
285    pub fn builder() -> Builder {
286        Builder::default()
287    }
288
289    /// Returns the raw identity data.
290    pub fn data<T: Any + Debug + Send + Sync + 'static>(&self) -> Option<&T> {
291        self.data.downcast_ref()
292    }
293
294    /// Returns the expiration time for this identity, if any.
295    pub fn expiration(&self) -> Option<SystemTime> {
296        self.expiration
297    }
298
299    /// Returns arbitrary property associated with this `Identity`.
300    pub fn property<T: Any + Debug + Send + Sync + 'static>(&self) -> Option<&T> {
301        self.properties
302            .get(&TypeId::of::<T>())
303            .and_then(|b| b.downcast_ref())
304    }
305}
306
307impl Debug for Identity {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        let mut debug_struct = f.debug_struct("Identity");
310        debug_struct
311            .field("data", (self.data_debug)(&self.data))
312            .field("expiration", &self.expiration);
313        for (i, prop) in self.properties.values().enumerate() {
314            debug_struct.field(&format!("property_{i}"), prop);
315        }
316        debug_struct.finish()
317    }
318}
319
320impl ResolveIdentity for Identity {
321    fn resolve_identity<'a>(
322        &'a self,
323        _runtime_components: &'a RuntimeComponents,
324        _config_bag: &'a ConfigBag,
325    ) -> IdentityFuture<'a> {
326        IdentityFuture::ready(Ok(self.clone()))
327    }
328}
329
330#[derive(Debug)]
331enum ErrorKind {
332    /// Field required to build the target type is missing.
333    MissingRequiredField(&'static str),
334}
335
336/// Error constructing [`Identity`].
337#[derive(Debug)]
338pub struct BuildError {
339    kind: ErrorKind,
340}
341
342impl BuildError {
343    fn missing_required_field(field_name: &'static str) -> Self {
344        BuildError {
345            kind: ErrorKind::MissingRequiredField(field_name),
346        }
347    }
348}
349
350impl fmt::Display for BuildError {
351    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
352        use ErrorKind::*;
353        match self.kind {
354            MissingRequiredField(field_name) => write!(f, "missing required field: `{field_name}`"),
355        }
356    }
357}
358
359impl std::error::Error for BuildError {}
360
361/// Builder for [`Identity`]
362#[derive(Default)]
363pub struct Builder {
364    data: Option<Arc<dyn Any + Send + Sync>>,
365    data_debug: Option<DataDebug>,
366    expiration: Option<SystemTime>,
367    properties: HashMap<TypeId, Arc<TypeErasedBox>>,
368}
369
370impl Builder {
371    /// Set raw identity data for the builder.
372    pub fn data<T: Any + Debug + Send + Sync + 'static>(mut self, data: T) -> Self {
373        self.set_data(data);
374        self
375    }
376
377    /// Set raw identity data for the builder.
378    pub fn set_data<T: Any + Debug + Send + Sync + 'static>(&mut self, data: T) {
379        self.data = Some(Arc::new(data));
380        self.data_debug = Some(Arc::new(|d| {
381            d.downcast_ref::<T>().expect("type-checked") as _
382        }));
383    }
384
385    /// Set expiration for the builder.
386    pub fn expiration(mut self, expiration: SystemTime) -> Self {
387        self.set_expiration(Some(expiration));
388        self
389    }
390
391    /// Set expiration for the builder.
392    pub fn set_expiration(&mut self, expiration: Option<SystemTime>) {
393        self.expiration = expiration;
394    }
395
396    /// Set arbitrary property for the builder.
397    pub fn property<T: Any + Debug + Send + Sync + 'static>(mut self, prop: T) -> Self {
398        self.set_property(prop);
399        self
400    }
401
402    /// Set arbitrary property for the builder.
403    pub fn set_property<T: Any + Debug + Send + Sync + 'static>(&mut self, prop: T) {
404        self.properties
405            .insert(TypeId::of::<T>(), Arc::new(TypeErasedBox::new(prop)));
406    }
407
408    /// Build [`Identity`].
409    pub fn build(self) -> Result<Identity, BuildError> {
410        Ok(Identity {
411            data: self
412                .data
413                .ok_or_else(|| BuildError::missing_required_field("data"))?,
414            data_debug: self
415                .data_debug
416                .expect("should always be set when `data` is set"),
417            expiration: self.expiration,
418            properties: self.properties,
419        })
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426    use aws_smithy_async::time::{SystemTimeSource, TimeSource};
427
428    #[test]
429    fn check_send_sync() {
430        fn is_send_sync<T: Send + Sync>(_: T) {}
431        is_send_sync(Identity::new("foo", None));
432    }
433
434    #[test]
435    fn create_retrieve_identity() {
436        #[derive(Debug)]
437        struct MyIdentityData {
438            first: String,
439            last: String,
440        }
441
442        let ts = SystemTimeSource::new();
443        let expiration = ts.now();
444        let identity = Identity::new(
445            MyIdentityData {
446                first: "foo".into(),
447                last: "bar".into(),
448            },
449            Some(expiration),
450        );
451
452        assert_eq!("foo", identity.data::<MyIdentityData>().unwrap().first);
453        assert_eq!("bar", identity.data::<MyIdentityData>().unwrap().last);
454        assert_eq!(Some(expiration), identity.expiration());
455    }
456
457    #[test]
458    fn insert_get_identity_properties() {
459        #[derive(Debug)]
460        struct MyIdentityData {
461            first: String,
462            last: String,
463        }
464        #[derive(Debug)]
465        struct PropertyAlpha;
466        #[derive(Debug)]
467        struct PropertyBeta;
468
469        let ts = SystemTimeSource::new();
470        let expiration = ts.now();
471        let identity = Identity::builder()
472            .data(MyIdentityData {
473                first: "foo".into(),
474                last: "bar".into(),
475            })
476            .expiration(expiration)
477            .property(PropertyAlpha)
478            .property(PropertyBeta)
479            .build()
480            .unwrap();
481
482        assert_eq!("foo", identity.data::<MyIdentityData>().unwrap().first);
483        assert_eq!("bar", identity.data::<MyIdentityData>().unwrap().last);
484        assert_eq!(Some(expiration), identity.expiration());
485        assert!(identity.property::<PropertyAlpha>().is_some());
486        assert!(identity.property::<PropertyBeta>().is_some());
487    }
488}