Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions examples/aws/security/secrets_manager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# rustcloud — AWS Secrets Manager

AWS Secrets Manager stores, rotates, and retrieves application secrets —
database credentials, API keys, OAuth tokens. RustCloud wraps it with the
same flat-function pattern used across all AWS services.

## Configure AWS Credentials

```sh
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_DEFAULT_REGION="us-east-1"
```

## Initialize the Client

```rust
use aws_sdk_secretsmanager::Client;

#[tokio::main]
async fn main() {
let config = aws_config::load_from_env().await;
let client = Client::new(&config);
}
```

## Operations

### Create a Secret

Store a plaintext string or a JSON map:

```rust
use rustcloud::aws::aws_apis::security::aws_secrets_manager;

// Plaintext
let arn = aws_secrets_manager::create_secret(&client, "prod/api-key", "sk-abc123").await?;

// JSON map (recommended for multiple fields)
let arn = aws_secrets_manager::create_secret(
&client,
"prod/db-credentials",
r#"{"host":"db.example.com","username":"app","password":"s3cr3t"}"#,
).await?;

println!("ARN: {}", arn);
```

### Get a Secret

```rust
let value = aws_secrets_manager::get_secret(&client, "prod/db-credentials").await?;
// value is the raw secret string — parse JSON if needed
let parsed: serde_json::Value = serde_json::from_str(&value)?;
println!("Host: {}", parsed["host"]);
```

### Update a Secret

```rust
aws_secrets_manager::update_secret(
&client,
"prod/db-credentials",
r#"{"host":"db.example.com","username":"app","password":"n3wpassword"}"#,
).await?;
```

### List Secrets

```rust
let names = aws_secrets_manager::list_secrets(&client).await?;
for name in names {
println!("{}", name);
}
```

### Delete a Secret

```rust
// Schedule deletion with a 7-day recovery window (minimum)
aws_secrets_manager::delete_secret(&client, "prod/api-key", Some(7)).await?;

// Use AWS default (30-day recovery window)
aws_secrets_manager::delete_secret(&client, "prod/api-key", None).await?;
```

## Naming conventions

Use path-style names (`service/env/name`) to group and control access via IAM:

```
prod/database/primary-password
prod/api/stripe-key
staging/database/primary-password
```

## IAM + KMS + Secrets Manager

The three AWS security services work together:

| Service | Purpose |
|----------------------|-------------------------------------------|
| IAM | Controls *who* can access secrets |
| KMS | Encrypts secret values at rest |
| Secrets Manager | Stores and retrieves the actual values |

## Supported operations

| Function | Description |
|-----------------|--------------------------------------------------|
| `create_secret` | Store a new secret, returns ARN |
| `get_secret` | Retrieve current secret string by name or ARN |
| `update_secret` | Overwrite secret value, returns ARN |
| `delete_secret` | Schedule deletion with configurable grace period |
| `list_secrets` | List all secret names in the account |

## References

- [AWS Secrets Manager Developer Guide](https://docs.aws.amazon.com/secretsmanager/latest/userguide/)
- [aws-sdk-secretsmanager crate](https://docs.rs/aws-sdk-secretsmanager/latest/aws_sdk_secretsmanager/)
- [IAM example](iam.md)
- [KMS example](kms.md)
119 changes: 119 additions & 0 deletions rustcloud/src/aws/aws_apis/security/aws_secrets_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#![allow(clippy::result_large_err)]

use aws_sdk_secretsmanager::{Client, Error};

/// Create a new secret and return its ARN.
/// `secret_string` should be a plaintext value or a JSON-encoded key/value map.
pub async fn create_secret(
client: &Client,
name: &str,
secret_string: &str,
) -> Result<String, Error> {
let resp = client
.create_secret()
.name(name)
.secret_string(secret_string)
.send()
.await;
match resp {
Ok(result) => {
let arn = result.arn().unwrap_or_default().to_string();
println!("Secret ARN: {}", arn);
Ok(arn)
}
Err(e) => {
println!("Error: {:?}", e);
Err(e.into())
}
}
}

/// Retrieve the current secret value by name or ARN.
/// Returns the secret string; use `get_secret_value` directly for binary secrets.
pub async fn get_secret(client: &Client, secret_id: &str) -> Result<String, Error> {
let resp = client.get_secret_value().secret_id(secret_id).send().await;
match resp {
Ok(result) => {
let value = result.secret_string().unwrap_or_default().to_string();
println!("Retrieved secret: {}", secret_id);
Ok(value)
}
Err(e) => {
println!("Error: {:?}", e);
Err(e.into())
}
}
}

/// Overwrite the secret value for an existing secret. Returns the secret ARN.
pub async fn update_secret(
client: &Client,
secret_id: &str,
new_secret_string: &str,
) -> Result<String, Error> {
let resp = client
.update_secret()
.secret_id(secret_id)
.secret_string(new_secret_string)
.send()
.await;
match resp {
Ok(result) => {
let arn = result.arn().unwrap_or_default().to_string();
println!("Updated secret ARN: {}", arn);
Ok(arn)
}
Err(e) => {
println!("Error: {:?}", e);
Err(e.into())
}
}
}

/// Schedule a secret for deletion.
/// `recovery_window_days` (7–30) sets the grace period before permanent deletion.
/// Pass `None` to use the AWS default (30 days).
pub async fn delete_secret(
client: &Client,
secret_id: &str,
recovery_window_days: Option<i64>,
) -> Result<String, Error> {
let mut builder = client.delete_secret().secret_id(secret_id);
if let Some(days) = recovery_window_days {
builder = builder.recovery_window_in_days(days);
}
let resp = builder.send().await;
match resp {
Ok(result) => {
let arn = result.arn().unwrap_or_default().to_string();
println!("Scheduled deletion for: {}", arn);
Ok(arn)
}
Err(e) => {
println!("Error: {:?}", e);
Err(e.into())
}
}
}

/// List all secrets in the account, returning their names.
pub async fn list_secrets(client: &Client) -> Result<Vec<String>, Error> {
let resp = client.list_secrets().send().await;
match resp {
Ok(result) => {
let names: Vec<String> = result
.secret_list()
.iter()
.filter_map(|s| s.name().map(|n| n.to_string()))
.collect();
for name in &names {
println!("Secret: {}", name);
}
Ok(names)
}
Err(e) => {
println!("Error: {:?}", e);
Err(e.into())
}
}
}
68 changes: 68 additions & 0 deletions rustcloud/src/tests/aws_secrets_manager_operations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use crate::aws::aws_apis::security::aws_secrets_manager;
use aws_sdk_secretsmanager::Client;

async fn get_client() -> Client {
let config = aws_config::load_from_env().await;
Client::new(&config)
}

#[tokio::test]
async fn test_create_secret() {
let client = get_client().await;
let result = aws_secrets_manager::create_secret(
&client,
"rustcloud/test/db-password",
r#"{"username":"admin","password":"s3cr3t"}"#,
)
.await;
assert!(result.is_ok());
let arn = result.unwrap();
assert!(arn.contains("rustcloud"));
println!("Created secret ARN: {}", arn);
}

#[tokio::test]
async fn test_get_secret() {
let client = get_client().await;
let result =
aws_secrets_manager::get_secret(&client, "rustcloud/test/db-password").await;
assert!(result.is_ok());
let value = result.unwrap();
assert!(!value.is_empty());
println!("Secret value length: {}", value.len());
}

#[tokio::test]
async fn test_update_secret() {
let client = get_client().await;
let result = aws_secrets_manager::update_secret(
&client,
"rustcloud/test/db-password",
r#"{"username":"admin","password":"n3wpassword"}"#,
)
.await;
assert!(result.is_ok());
println!("Updated secret: {:?}", result.unwrap());
}

#[tokio::test]
async fn test_list_secrets() {
let client = get_client().await;
let result = aws_secrets_manager::list_secrets(&client).await;
assert!(result.is_ok());
println!("Secrets: {:?}", result.unwrap());
}

#[tokio::test]
async fn test_delete_secret() {
let client = get_client().await;
// Use minimum 7-day recovery window
let result = aws_secrets_manager::delete_secret(
&client,
"rustcloud/test/db-password",
Some(7),
)
.await;
assert!(result.is_ok());
println!("Scheduled deletion: {:?}", result.unwrap());
}