aws_utils_secretsmanager 0.4.0

AWS Secrets Manager utilities for retrieving secret values
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
# aws_utils_secretsmanager

AWS Secrets Manager utilities for retrieving secret values from AWS Secrets Manager.

## Features

- Simple interface for retrieving secrets from AWS Secrets Manager
- Support for secret versioning with version ID and version stage
- Custom error handling with detailed error types
- Support for custom AWS endpoints (useful for testing with LocalStack)
- Support for AWS SDK's default credential chain

## Installation

Add this to your `Cargo.toml`:

```toml
[dependencies]
aws_utils_secretsmanager = "0.1.0"
```

## Usage

### Basic Example

```rust
use aws_utils_secretsmanager::{make_client_with_timeout_default, secretsmanager::get_secret_value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create Secrets Manager client with default timeout configuration
    let client = make_client_with_timeout_default(None).await;
    
    // Get secret value
    let secret = get_secret_value(&client, "my-secret-name").await?;
    println!("Secret value: {}", secret);
    
    Ok(())
}
```

### Using Custom Endpoint

```rust
use aws_utils_secretsmanager::{make_client_with_timeout_default, secretsmanager::get_secret_value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create client with custom endpoint (e.g., LocalStack)
    let client = make_client_with_timeout_default(Some("http://localhost:4566".to_string())).await;
    
    let secret = get_secret_value(&client, "test-secret").await?;
    println!("Secret value: {}", secret);
    
    Ok(())
}
```

### Using Custom Timeout Configuration

```rust
use std::time::Duration;
use aws_utils_secretsmanager::{make_client_with_timeout, secretsmanager::get_secret_value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create client with custom timeout settings
    let client = make_client_with_timeout(
        None,
        Some(Duration::from_secs(5)),      // 5 second connect timeout
        Some(Duration::from_secs(30)),     // 30 second operation timeout
        Some(Duration::from_secs(25)),     // 25 second operation attempt timeout
        Some(Duration::from_secs(20)),     // 20 second read timeout
    ).await;
    
    let secret = get_secret_value(&client, "test-secret").await?;
    println!("Secret value: {}", secret);
    
    Ok(())
}
```

### Using with TimeoutConfig

```rust
use aws_config::timeout::{TimeoutConfig, TimeoutConfigBuilder};
use aws_utils_secretsmanager::{make_client, secretsmanager::get_secret_value};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build custom timeout configuration
    let timeout_config = TimeoutConfigBuilder::new()
        .connect_timeout(Duration::from_secs(10))
        .operation_timeout(Duration::from_secs(120))
        .build();
    
    // Create client with custom timeout configuration
    let client = make_client(None, Some(timeout_config), None).await;
    
    let secret = get_secret_value(&client, "long-running-secret").await?;
    println!("Secret value: {}", secret);
    
    Ok(())
}
```

### Logging AWS Communication

`make_client` accepts an optional [`SharedInterceptor`]. By passing an interceptor that
implements `aws_sdk_secretsmanager::config::Intercept`, you can run custom logic — such as
logging — every time the client communicates with AWS.

The interceptor below logs each request, response, and operation result. It uses the
[`tracing`](https://crates.io/crates/tracing) crate, which is also what the AWS SDK uses
internally.

```rust
use aws_utils_secretsmanager::make_client;
use aws_sdk_secretsmanager::config::{
    ConfigBag, Intercept, RuntimeComponents, SharedInterceptor,
    interceptors::{
        AfterDeserializationInterceptorContextRef, BeforeDeserializationInterceptorContextRef,
        BeforeTransmitInterceptorContextRef,
    },
};

type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;

#[derive(Debug, Clone)]
struct LoggingInterceptor;

impl Intercept for LoggingInterceptor {
    fn name(&self) -> &'static str {
        "SecretsManagerLoggingInterceptor"
    }

    // Called just before each HTTP request is sent (once per retry attempt).
    fn read_before_transmit(
        &self,
        context: &BeforeTransmitInterceptorContextRef<'_>,
        _runtime_components: &RuntimeComponents,
        _cfg: &mut ConfigBag,
    ) -> Result<(), BoxError> {
        let request = context.request();
        tracing::info!(
            method = %request.method(),
            uri = %request.uri(),
            "SecretsManager -> AWS request"
        );
        Ok(())
    }

    // Called right after each HTTP response is received.
    fn read_before_deserialization(
        &self,
        context: &BeforeDeserializationInterceptorContextRef<'_>,
        _runtime_components: &RuntimeComponents,
        _cfg: &mut ConfigBag,
    ) -> Result<(), BoxError> {
        let response = context.response();
        tracing::info!(status = %response.status(), "AWS -> SecretsManager response");
        Ok(())
    }

    // Called once when the operation completes (after retries), with success or error.
    fn read_after_deserialization(
        &self,
        context: &AfterDeserializationInterceptorContextRef<'_>,
        _runtime_components: &RuntimeComponents,
        _cfg: &mut ConfigBag,
    ) -> Result<(), BoxError> {
        match context.output_or_error() {
            Ok(_) => tracing::info!("SecretsManager operation succeeded"),
            Err(err) => tracing::warn!(error = %err, "SecretsManager operation failed"),
        }
        Ok(())
    }
}

# async fn run() {
// Pass the interceptor as the third argument.
let client = make_client(None, None, Some(SharedInterceptor::new(LoggingInterceptor))).await;
# }
```

`tracing` does not emit anything until a subscriber is initialized. Set one up once in your
application (for example with `tracing-subscriber`) and control verbosity with `RUST_LOG`:

```rust
// Add `tracing-subscriber` to your dependencies.
tracing_subscriber::fmt()
    .with_env_filter(
        tracing_subscriber::EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| "info".into()),
    )
    .init();
```

Example output (`RUST_LOG=info`):

```text
INFO SecretsManagerLoggingInterceptor: SecretsManager -> AWS request method=POST uri=https://secretsmanager.ap-northeast-1.amazonaws.com/
INFO SecretsManagerLoggingInterceptor: AWS -> SecretsManager response status=200
INFO SecretsManagerLoggingInterceptor: SecretsManager operation succeeded
```

### Getting Raw Secret Output with Versioning

```rust
use aws_utils_secretsmanager::{make_client_with_timeout_default, secretsmanager::get_secret_value_raw};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = make_client_with_timeout_default(None).await;
    
    // Get specific version of secret
    let output = get_secret_value_raw(
        &client,
        Some("my-secret-name"),
        Some("version-uuid"),      // Version ID
        Some("AWSCURRENT")         // Version stage
    ).await?;
    
    if let Some(secret_string) = output.secret_string() {
        println!("Secret: {}", secret_string);
    }
    
    if let Some(version_id) = output.version_id() {
        println!("Version ID: {}", version_id);
    }
    
    Ok(())
}
```

### Getting Latest Secret Version

```rust
use aws_utils_secretsmanager::{make_client_with_timeout_default, secretsmanager::get_secret_value_raw};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = make_client_with_timeout_default(None).await;
    
    // Get current version (default behavior)
    let output = get_secret_value_raw(
        &client,
        Some("my-secret-name"),
        None::<String>,           // No specific version ID
        Some("AWSCURRENT")        // Get current version
    ).await?;
    
    println!("Current secret: {:?}", output.secret_string());
    
    Ok(())
}
```

## API Reference

### Functions

#### `make_client_with_timeout_default(endpoint_url: Option<String>) -> Client`

Creates an AWS Secrets Manager client with default timeout configuration.

- `endpoint_url`: Optional custom endpoint URL for testing (e.g., LocalStack)
- Returns: Configured AWS Secrets Manager Client with default timeouts
- Default timeouts:
  - Connect timeout: 3100 seconds
  - Operation timeout: 60 seconds
  - Operation attempt timeout: 55 seconds
  - Read timeout: 50 seconds

#### `make_client_with_timeout(endpoint_url: Option<String>, connect_timeout: Option<Duration>, operation_timeout: Option<Duration>, operation_attempt_timeout: Option<Duration>, read_timeout: Option<Duration>) -> Client`

Creates an AWS Secrets Manager client with custom timeout configuration.

- `endpoint_url`: Optional custom endpoint URL for testing (e.g., LocalStack)
- `connect_timeout`: Optional timeout for establishing connections
- `operation_timeout`: Optional timeout for entire operations
- `operation_attempt_timeout`: Optional timeout for individual operation attempts
- `read_timeout`: Optional timeout for reading responses
- Returns: Configured AWS Secrets Manager Client with custom timeouts

#### `make_client(endpoint_url: Option<String>, timeout_config: Option<TimeoutConfig>, interceptor: Option<SharedInterceptor>) -> Client`

Creates an AWS Secrets Manager client with optional custom endpoint URL, timeout configuration, and interceptor.

- `endpoint_url`: Optional custom endpoint URL for testing (e.g., LocalStack)
- `timeout_config`: Optional timeout configuration
- `interceptor`: Optional interceptor for running custom logic (e.g. logging) on every AWS communication
- Returns: Configured AWS Secrets Manager Client

#### `get_secret_value(client: &Client, secret_id: &str) -> Result<String, Error>`

Retrieves a secret value as a string from the current version.

- `client`: AWS Secrets Manager client
- `secret_id`: Secret identifier (name or ARN)
- Returns: Secret value as String

#### `get_secret_value_raw(client: &Client, secret_id: Option<impl Into<String>>, version_id: Option<impl Into<String>>, version_stage: Option<impl Into<String>>) -> Result<GetSecretValueOutput, Error>`

Retrieves raw secret output from AWS Secrets Manager with version control.

- `client`: AWS Secrets Manager client
- `secret_id`: Optional secret identifier (name or ARN)
- `version_id`: Optional version UUID to retrieve specific version
- `version_stage`: Optional version stage (e.g., "AWSCURRENT", "AWSPENDING")
- Returns: Raw GetSecretValueOutput from AWS SDK

### Error Types

The crate defines custom error types:

- `Error::BuildError`: AWS SDK build errors
- `Error::AwsSdk`: AWS SDK service errors
- `Error::ValidationError`: Validation errors
- `Error::NotFound`: Secret not found

## Secret Versioning

AWS Secrets Manager supports versioning of secrets. You can:

- Get the current version using `"AWSCURRENT"` stage
- Get the pending version using `"AWSPENDING"` stage
- Get a specific version using the version UUID
- Let AWS choose the version by omitting version parameters

### Version Stages

- `AWSCURRENT`: The current version of the secret
- `AWSPENDING`: The version that will become current after rotation completes
- Custom stages: You can define custom version stages for your workflow

## Testing

Set up your test environment:

```bash
# Optional: Custom Secrets Manager endpoint (e.g., LocalStack)
export SECRETSMANAGER_ENDPOINT_URL=http://localhost:4566

# Run tests
cargo test
```

### Test Commands

```bash
# Run all tests
cargo test

# Run with logging
RUST_LOG=info cargo test -- --nocapture

# Run specific test
cargo test test_get_secret_value -- --nocapture
```

## Authentication

The client uses the AWS SDK's default credential chain for authentication:

- Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`)
- ECS task role (for Fargate/ECS)
- EC2 instance profile
- AWS credentials file
- Other configured credential providers

## Use Cases

### Database Credentials

```rust
use aws_utils_secretsmanager::{make_client_with_timeout_default, secretsmanager::get_secret_value};
use serde_json::Value;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = make_client_with_timeout_default(None).await;
    let secret_json = get_secret_value(&client, "prod/db/credentials").await?;
    
    let credentials: Value = serde_json::from_str(&secret_json)?;
    let username = credentials["username"].as_str().unwrap();
    let password = credentials["password"].as_str().unwrap();
    
    // Use credentials to connect to database
    println!("Connecting as user: {}", username);
    
    Ok(())
}
```

### API Keys

```rust
use aws_utils_secretsmanager::{make_client_with_timeout_default, secretsmanager::get_secret_value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = make_client_with_timeout_default(None).await;
    let api_key = get_secret_value(&client, "prod/external-api/key").await?;
    
    // Use API key for external service calls
    println!("API Key retrieved successfully");
    
    Ok(())
}
```

## License

MIT