Skip to main content

aws_runtime/static_stability/
invalidation.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Auth-failure detection for static-stability credential invalidation.
7
8use aws_smithy_runtime::client::orchestrator::InvalidateResolvedIdentity;
9use aws_smithy_runtime_api::box_error::BoxError;
10use aws_smithy_runtime_api::client::interceptors::context::AfterDeserializationInterceptorContextRef;
11use aws_smithy_runtime_api::client::interceptors::Intercept;
12use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
13use aws_smithy_types::config_bag::ConfigBag;
14use aws_smithy_types::error::metadata::ProvideErrorMetadata;
15use std::error::Error as StdError;
16use std::fmt;
17use std::marker::PhantomData;
18
19/// AWS error codes indicating the request's credentials are no longer valid, so the resolved
20/// identity must be invalidated. Both spellings are listed because AWS services are inconsistent.
21/// `AccessDenied` is intentionally excluded: that is authorization, not credential validity.
22const CREDENTIAL_AUTH_FAILURE_ERRORS: &[&str] =
23    &["ExpiredToken", "ExpiredTokenException", "InvalidToken"];
24
25/// Detects a credential/token auth failure (`ExpiredToken`, `ExpiredTokenException`, or
26/// `InvalidToken`) on the operation response and signals the orchestrator — via the data-free
27/// [`InvalidateResolvedIdentity`] config marker — to invalidate the resolved identity.
28///
29/// This is **detection only**: the interceptor has no identity (the signed request is already gone
30/// post-transmit), so the orchestrator makes the actual `invalidate` call with the in-scope signing
31/// identity. Registered per-operation by AWS codegen (like `AwsErrorCodeClassifier<E>`).
32pub struct CredentialAuthFailureInterceptor<E> {
33    _marker: PhantomData<fn() -> E>,
34}
35
36impl<E> CredentialAuthFailureInterceptor<E> {
37    /// Creates a new [`CredentialAuthFailureInterceptor`].
38    pub fn new() -> Self {
39        Self {
40            _marker: PhantomData,
41        }
42    }
43}
44
45impl<E> Default for CredentialAuthFailureInterceptor<E> {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl<E> fmt::Debug for CredentialAuthFailureInterceptor<E> {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.write_str("CredentialAuthFailureInterceptor")
54    }
55}
56
57impl<E> Intercept for CredentialAuthFailureInterceptor<E>
58where
59    E: StdError + ProvideErrorMetadata + Send + Sync + 'static,
60{
61    fn name(&self) -> &'static str {
62        "CredentialAuthFailure"
63    }
64
65    fn read_after_deserialization(
66        &self,
67        context: &AfterDeserializationInterceptorContextRef<'_>,
68        _runtime_components: &RuntimeComponents,
69        cfg: &mut ConfigBag,
70    ) -> Result<(), BoxError> {
71        let is_auth_failure = context
72            .output_or_error()
73            .err()
74            .and_then(|err| err.as_operation_error())
75            .and_then(|err| err.downcast_ref::<E>())
76            .and_then(|err| err.code())
77            .is_some_and(|code| CREDENTIAL_AUTH_FAILURE_ERRORS.contains(&code));
78        if is_auth_failure {
79            cfg.interceptor_state()
80                .store_put(InvalidateResolvedIdentity);
81        }
82        Ok(())
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use aws_smithy_runtime_api::client::interceptors::context::{Error, Input, InterceptorContext};
90    use aws_smithy_runtime_api::client::orchestrator::OrchestratorError;
91    use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
92    use aws_smithy_types::error::ErrorMetadata;
93
94    // A minimal operation error carrying a modeled error code.
95    #[derive(Debug)]
96    struct CodedError {
97        metadata: ErrorMetadata,
98    }
99
100    impl CodedError {
101        fn new(code: &'static str) -> Self {
102            Self {
103                metadata: ErrorMetadata::builder().code(code).build(),
104            }
105        }
106    }
107
108    impl fmt::Display for CodedError {
109        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110            write!(f, "coded error")
111        }
112    }
113
114    impl StdError for CodedError {}
115
116    impl ProvideErrorMetadata for CodedError {
117        fn meta(&self) -> &ErrorMetadata {
118            &self.metadata
119        }
120    }
121
122    // Runs the interceptor against a deserialized operation error with the given code and reports
123    // whether it set the `InvalidateResolvedIdentity` marker.
124    fn sets_invalidate_marker(code: &'static str) -> bool {
125        let interceptor = CredentialAuthFailureInterceptor::<CodedError>::new();
126        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
127        let mut cfg = ConfigBag::base();
128        let mut ctx = InterceptorContext::new(Input::doesnt_matter());
129        ctx.set_output_or_error(Err(OrchestratorError::operation(Error::erase(
130            CodedError::new(code),
131        ))));
132        let ctx_ref = AfterDeserializationInterceptorContextRef::from(&ctx);
133        interceptor
134            .read_after_deserialization(&ctx_ref, &rc, &mut cfg)
135            .unwrap();
136        cfg.load::<InvalidateResolvedIdentity>().is_some()
137    }
138
139    #[test]
140    fn triggers_on_expired_and_invalid_token_only() {
141        assert!(
142            sets_invalidate_marker("ExpiredToken"),
143            "ExpiredToken must trigger invalidation"
144        );
145        assert!(
146            sets_invalidate_marker("ExpiredTokenException"),
147            "STS/SSO-OIDC ExpiredTokenException must trigger invalidation"
148        );
149        assert!(
150            sets_invalidate_marker("InvalidToken"),
151            "InvalidToken must trigger invalidation"
152        );
153        // AccessDenied is authorization, not credential validity — must NOT invalidate.
154        assert!(
155            !sets_invalidate_marker("AccessDenied"),
156            "AccessDenied is authz, not credential validity"
157        );
158        assert!(
159            !sets_invalidate_marker("ThrottlingException"),
160            "unrelated errors must not trigger invalidation"
161        );
162    }
163}