aws_secretsmanager_provider 3.1.1

The AWS Secrets Manager Provider is a local HTTP service that you can install and use in your compute environments to read secrets from Secrets Manager and cache them in memory.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
use crate::constants::{MAX_REQ_TIME_SEC, VERSION};
use aws_sdk_secretsmanager::config::interceptors::BeforeTransmitInterceptorContextMut;
use aws_sdk_secretsmanager::config::{ConfigBag, Intercept, RuntimeComponents};
#[cfg(not(test))]
use aws_sdk_secretsmanager::Client as SecretsManagerClient;
use aws_workload_credentials_provider_common::config::types::SecretsManagerConfig;
use std::env::VarError;
use std::fs;
use std::time::Duration;

#[cfg(not(test))]
use std::env::var; // Use the real std::env::var
#[cfg(test)]
use tests::var_test as var;

/// Helper to format error response body in JSON 1.1 format.
///
/// Callers need to pass in the error code (e.g.  InternalFailure,
/// InvalidParameterException, ect.) and the error message. This function will
/// then format a response body in JSON 1.1 format. All values are properly
/// JSON-escaped via serde_json to prevent injection attacks.
///
/// # Arguments
///
/// * `err_code` - The modeled exception name or InternalFailure for 500s.
/// * `msg` - The optional error message or "" for InternalFailure.
///
/// # Returns
///
/// * `String` - The JSON 1.1 response body.
///
/// # Example
///
///
/// assert_eq!(err_response("InternalFailure", ""), "{\"__type\":\"InternalFailure\"}");
/// assert_eq!(
///     err_response("ResourceNotFoundException", "Secrets Manager can't find the specified secret."),
///     "{\"__type\":\"ResourceNotFoundException\",\"message\":\"Secrets Manager can't find the specified secret.\"}"
/// );
///
#[doc(hidden)]
pub fn err_response(err_code: &str, msg: &str) -> String {
    if msg.is_empty() || err_code == "InternalFailure" {
        return String::from("{\"__type\":\"InternalFailure\"}");
    }
    serde_json::json!({
        "__type": err_code,
        "message": msg
    })
    .to_string()
}

/// Helper function to get the SSRF token value.
///
/// Reads the SSRF token from the configured env variable. If the env variable
/// is a reference to a file (namely file://FILENAME), the data is read in from
/// that file.
///
/// # Arguments
///
/// * `ssrf_env_variables` - The list of environment variable names to check for the SSRF token.
///
/// # Returns
///
/// * `Ok(String)` - The SSRF token value.
/// * `Err(Error)` - Error indicating that the variable is not set or could not be read.
#[doc(hidden)]
pub fn get_token(ssrf_env_variables: &[String]) -> Result<String, Box<dyn std::error::Error>> {
    // Iterate through the env name list looking for the first variable set
    #[allow(clippy::redundant_closure)]
    let found = ssrf_env_variables
        .iter()
        .map(|n| var(n))
        .filter_map(|r| r.ok())
        .next();
    if found.is_none() {
        return Err(Box::new(VarError::NotPresent));
    }
    let val = found.unwrap();

    // If the variable is not a reference to a file, just return the value.
    if !val.starts_with("file://") {
        return Ok(val);
    }

    // Read and return the contents of the file.
    let file = val.strip_prefix("file://").unwrap();
    Ok(fs::read_to_string(file)?.trim().to_string())
}

#[doc(hidden)]
#[cfg(not(test))]
pub use time_out_impl as time_out;
#[cfg(test)]
pub use time_out_test as time_out;

/// Helper function to get the time out setting for request processing.
///
/// # Returns
///
/// * `Duration` - How long to wait before canceling the operation.
#[doc(hidden)]
pub fn time_out_impl() -> Duration {
    Duration::from_secs(MAX_REQ_TIME_SEC)
}
#[cfg(test)]
pub fn time_out_test() -> Duration {
    Duration::from_secs(10) // Timeout in 10 seconds for testing.
}

/// Validates the provided configuration and creates an AWS Secrets Manager client
/// from the latest default AWS configuration.
///
/// # Arguments
///
/// * `config` - A reference to a `SecretsManagerConfig` object containing the necessary configuration
///   parameters for creating the AWS Secrets Manager client.
///
/// # Returns
///
/// * `Ok(SecretsManagerClient)` - An AWS Secrets Manager client if the credentials are valid.
/// * `Err(Box<dyn std::error::Error>)` if there is an error creating the Secrets Manager client
///   or validating the AWS credentials.
#[doc(hidden)]
#[cfg(not(test))]
pub async fn validate_and_create_asm_client(
    config: &SecretsManagerConfig,
) -> Result<(SecretsManagerClient, aws_config::SdkConfig), Box<dyn std::error::Error>> {
    use aws_config::{BehaviorVersion, Region};
    use aws_secretsmanager_caching::error::is_transient_error;

    use crate::credentials_file_provider::FileBasedCredentialsProvider;

    let mut sdk_config = aws_config::load_defaults(BehaviorVersion::latest()).await;

    // Use file-based credentials if configured.
    let has_file_provider = if let Some(path) = discover_credentials_file(config) {
        log::info!("Using file-based credentials from: {}", path.display());
        let provider = FileBasedCredentialsProvider::new(&path).await;
        sdk_config = sdk_config
            .into_builder()
            .credentials_provider(
                aws_sdk_secretsmanager::config::SharedCredentialsProvider::new(provider),
            )
            .build();
        true
    } else {
        log::info!("No credentials file found, using default SDK credential chain");
        false
    };

    let mut asm_builder = aws_sdk_secretsmanager::config::Builder::from(&sdk_config)
        .interceptor(AgentModifierInterceptor);

    #[cfg(debug_assertions)]
    if std::env::var("SMA_DISABLE_IDENTITY_CACHE").is_ok() {
        log::info!("Identity caching disabled via SMA_DISABLE_IDENTITY_CACHE");
        asm_builder =
            asm_builder.identity_cache(aws_sdk_secretsmanager::config::IdentityCache::no_cache());
    }

    if let Some(ref region) = config.region {
        asm_builder.set_region(Some(Region::new(region.clone())));
    }

    if config.validate_credentials && !has_file_provider {
        let mut sts_builder = aws_sdk_sts::config::Builder::from(&sdk_config);
        if let Some(ref region) = config.region {
            sts_builder.set_region(Some(Region::new(region.clone())));
        }

        let sts_client = aws_sdk_sts::Client::from_conf(sts_builder.build());
        match sts_client.get_caller_identity().send().await {
            Ok(_) => (),
            Err(e) if config.ignore_transient_errors && is_transient_error(&e) => (),
            Err(e) => Err(e)?,
        };
    } else if has_file_provider {
        log::info!("Skipping STS credential validation for file-based credentials");
    }

    Ok((
        aws_sdk_secretsmanager::Client::from_conf(asm_builder.build()),
        sdk_config,
    ))
}

/// Discover a credentials file from explicit config.
///
/// Returns the configured credentials file path, if set.
/// If the configured path does not exist yet, it is still returned — the provider
/// will start with an empty cache and the background reload will pick up the file
/// when it appears.
#[cfg(not(test))]
fn discover_credentials_file(config: &SecretsManagerConfig) -> Option<std::path::PathBuf> {
    if let Some(path) = &config.credentials_file_path {
        if !path.is_file() {
            log::warn!(
                "Configured credentials_file_path does not exist yet: {}. \
                 The agent will watch for it to appear.",
                path.display()
            );
        }
        return Some(path.clone());
    }

    None
}

/// SDK interceptor to append the agent name and version to the User-Agent header for CloudTrail records.
#[doc(hidden)]
#[derive(Debug)]
pub struct AgentModifierInterceptor;

/// Creates a Secrets Manager client that uses AssumeRole credentials for cross-account access.
///
/// Builds an `AssumeRoleProvider` that handles automatic STS credential refresh,
/// then configures a Secrets Manager SDK client with those credentials. The
/// `AgentModifierInterceptor` is attached for CloudTrail user-agent tracking.
///
/// # Arguments
///
/// * `config` - The agent configuration, used for optional region override.
/// * `base_config` - The base AWS SDK config providing default credentials (for the
///   AssumeRole call itself), HTTP client, retry config, and region.
/// * `role_arn` - The ARN of the IAM role to assume (e.g. `arn:aws:iam::123456789012:role/MyRole`).
///
/// # Returns
///
/// * `Ok(SecretsManagerClient)` - A Secrets Manager client configured with AssumeRole credentials.
///
/// # Errors
///
/// * `Box<dyn std::error::Error>` - If the `AssumeRoleProvider` fails to build or the
///   SDK client configuration is invalid.
#[doc(hidden)]
#[cfg(not(test))]
pub async fn create_role_asm_client(
    config: &SecretsManagerConfig,
    base_config: &aws_config::SdkConfig,
    role_arn: &str,
) -> Result<SecretsManagerClient, Box<dyn std::error::Error>> {
    use aws_config::Region;

    let provider = aws_config::sts::AssumeRoleProvider::builder(role_arn)
        .configure(base_config)
        .session_name("secrets-manager-provider")
        .build()
        .await;

    // Eagerly validate the assumed role credentials with a GetCallerIdentity call.
    // The AssumeRoleProvider is lazy — without this, STS errors would only surface
    // on the first Secrets Manager request, returning a generic InternalFailure.
    let shared_provider = aws_sdk_secretsmanager::config::SharedCredentialsProvider::new(provider);
    let mut sts_builder = aws_sdk_sts::config::Builder::from(base_config)
        .credentials_provider(shared_provider.clone());
    if let Some(region) = &config.region {
        sts_builder.set_region(Some(Region::new(region.clone())));
    }

    let sts_client = aws_sdk_sts::Client::from_conf(sts_builder.build());

    // if failure, let error propagate to cache manager
    sts_client.get_caller_identity().send().await?;

    let mut asm_builder = aws_sdk_secretsmanager::config::Builder::from(base_config)
        .credentials_provider(shared_provider)
        .interceptor(AgentModifierInterceptor);

    if let Some(region) = &config.region {
        asm_builder.set_region(Some(Region::new(region.clone())));
    }

    Ok(SecretsManagerClient::from_conf(asm_builder.build()))
}

/// SDK interceptor to append the agent name and version to the User-Agent header for CloudTrail records.
///
/// This interceptor adds the agent name and version to the User-Agent header
/// of outbound Secrets Manager SDK requests.
#[doc(hidden)]
impl Intercept for AgentModifierInterceptor {
    fn name(&self) -> &'static str {
        "AgentModifierInterceptor"
    }

    fn modify_before_signing(
        &self,
        context: &mut BeforeTransmitInterceptorContextMut<'_>,
        _runtime_components: &RuntimeComponents,
        _cfg: &mut ConfigBag,
    ) -> Result<(), aws_sdk_secretsmanager::error::BoxError> {
        let request = context.request_mut();
        let agent = request.headers().get("user-agent").unwrap_or_default(); // Get current agent
        let full_agent = format!(
            "{agent} {}/{}",
            aws_workload_credentials_provider_common::constants::PROVIDER_NAME,
            VERSION.unwrap_or("0.0.0")
        );
        request.headers_mut().insert("user-agent", full_agent); // Overwrite header.

        Ok(())
    }
}

#[cfg(test)]
pub mod tests {
    use super::*;
    use aws_workload_credentials_provider_common::config::validator::ConfigValidator;
    use std::cell::RefCell;
    use std::env::temp_dir;
    use std::thread_local;
    use std::time::{SystemTime, UNIX_EPOCH};

    // Used to cleanup resources after test completion.
    pub struct CleanUp<'a> {
        pub file: Option<&'a str>,
    }

    impl Drop for CleanUp<'_> {
        fn drop(&mut self) {
            // Clear env var injections.
            ENVVAR.set(None);

            // Cleanup temp files.
            if let Some(name) = self.file {
                let _ = std::fs::remove_file(name);
            }
        }
    }

    // Create a temp file name for a test.
    pub fn tmpfile_name(suffix: &str) -> String {
        format!(
            "{}/{}_{:?}_{suffix}",
            temp_dir().display(),
            std::process::id(),
            SystemTime::now().duration_since(UNIX_EPOCH).unwrap()
        )
    }

    // Used to inject env variable values for testing. Uses thread local data since
    // multi-threaded tests setting process wide env variables can collide.
    thread_local! {
        static ENVVAR: RefCell<Option<Vec<(&'static str, &'static str)>>> = const { RefCell::new(None) };
    }
    pub fn set_test_var(key: &'static str, val: &'static str) {
        ENVVAR.set(Some(vec![(key, val)]));
    }
    pub fn set_test_vars(vars: Vec<(&'static str, &'static str)>) {
        ENVVAR.set(Some(vars));
    }

    // Stub std::env::var that reads injected variables from thread_local
    pub fn var_test(key: &str) -> Result<String, VarError> {
        // Shortcut key to force failure.
        if key == "FAIL_TOKEN" {
            return Err(VarError::NotPresent);
        }
        if let Some(varvec) = ENVVAR.with_borrow(|v| v.clone()) {
            let found = varvec.iter().find(|keyval| keyval.0 == key);
            if let Some(some_found) = found {
                return Ok(some_found.1.to_string());
            }
        } else {
            // Return a default value if no value is injected.
            return Ok("xyzzy".to_string()); // Poof!
        }

        Err(VarError::NotPresent) // A fake value was injected but not for this key.
    }

    // Verify we can read the default config variable.
    #[test]
    fn test_env_set() {
        let _cleanup = CleanUp { file: None };
        set_test_var("AWS_TOKEN", "abc123");
        let cfg = ConfigValidator::new()
            .validate(None)
            .expect("config failed");
        assert_eq!(
            get_token(&cfg.secrets_manager.security.ssrf_env_variables).expect("token fail"),
            "abc123"
        );
    }

    // Verify we can use the second variable in the list
    #[test]
    fn test_alt_env_set() {
        let _cleanup = CleanUp { file: None };
        set_test_var("AWS_SESSION_TOKEN", "123abc");
        let cfg = ConfigValidator::new()
            .validate(None)
            .expect("config failed");
        assert_eq!(
            get_token(&cfg.secrets_manager.security.ssrf_env_variables).expect("token fail"),
            "123abc"
        );
    }

    // Verify the variable can point to a file and we use the file contents.
    #[test]
    fn test_file_token() {
        let token = "4 chosen by fair dice roll, guaranteed to be random";
        let tmpfile = tmpfile_name("test_file_token");
        let _cleanup = CleanUp {
            file: Some(&tmpfile),
        };
        std::fs::write(&tmpfile, token).expect("could not write");
        let file = Box::new(format!("file://{tmpfile}"));
        set_test_var("AWS_TOKEN", Box::leak(file));
        let cfg = ConfigValidator::new()
            .validate(None)
            .expect("config failed");
        assert_eq!(
            get_token(&cfg.secrets_manager.security.ssrf_env_variables).expect("token fail"),
            token
        );
    }

    // Verify we correctly handle a missing file.
    #[test]
    fn test_file_token_missing() {
        #[cfg(unix)]
        const NO_SUCH_FILE_ERROR_MSG: &str = "No such file or directory (os error 2)";
        #[cfg(windows)]
        const NO_SUCH_FILE_ERROR_MSG: &str =
            "The system cannot find the file specified. (os error 2)";

        let _cleanup = CleanUp { file: None };
        set_test_var("AWS_TOKEN", "file:///NoSuchFile");
        let cfg = ConfigValidator::new()
            .validate(None)
            .expect("config failed");
        assert_eq!(
            get_token(&cfg.secrets_manager.security.ssrf_env_variables)
                .err()
                .unwrap()
                .to_string(),
            NO_SUCH_FILE_ERROR_MSG
        );
    }

    // Verify the first variable in the list takes precedence
    #[test]
    fn two_tokens() {
        let _cleanup = CleanUp { file: None };
        set_test_vars(vec![
            ("AWS_TOKEN", "yzzyx"),
            ("AWS_SESSION_TOKEN", "CTAtoken"),
        ]); // Good token, unusable token.
        let cfg = ConfigValidator::new()
            .validate(None)
            .expect("config failed");
        assert_eq!(
            get_token(&cfg.secrets_manager.security.ssrf_env_variables).expect("token fail"),
            "yzzyx"
        );
    }

    // Verify we return the correct error when a variable is not set.
    #[test]
    fn test_env_fail() {
        let _cleanup = CleanUp { file: None };
        set_test_var("", "");
        let env_vars = vec!["NOSUCHENV".to_string()];
        assert!(get_token(&env_vars)
            .err()
            .unwrap()
            .downcast_ref::<VarError>()
            .unwrap()
            .eq(&VarError::NotPresent));
    }

    // Make sure the timeout functon returns the correct value.
    #[test]
    fn test_time_out() {
        assert_eq!(time_out_impl(), Duration::from_secs(MAX_REQ_TIME_SEC));
    }
}