* feat: add AWS KMS as secret backend (EE) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: switch from AWS KMS to AWS Secrets Manager as secret backend Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add AWS Secrets Manager integration tests (requires LocalStack) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: mark AWS Secrets Manager as beta Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove leftover KMS handler functions from api-settings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to include AWS Secrets Manager EE impl Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use full commit hash in ee-repo-ref.txt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * sqlx --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
265 lines
8.4 KiB
Rust
265 lines
8.4 KiB
Rust
//! Integration tests for AWS Secrets Manager secret backend.
|
|
//!
|
|
//! These tests require a running LocalStack instance with the `secretsmanager` service.
|
|
//!
|
|
//! ## Setup
|
|
//!
|
|
//! 1. Start LocalStack:
|
|
//!
|
|
//! ```bash
|
|
//! docker run -d --name localstack -p 4566:4566 \
|
|
//! -e SERVICES=secretsmanager \
|
|
//! localstack/localstack:3.8
|
|
//! ```
|
|
//!
|
|
//! 2. Run the tests:
|
|
//!
|
|
//! ```bash
|
|
//! RUN_AWS_SM_TESTS=1 cargo test -p windmill-common --features private,enterprise \
|
|
//! aws_sm_integration -- --nocapture
|
|
//! ```
|
|
//!
|
|
//! ## Environment variables
|
|
//!
|
|
//! - `RUN_AWS_SM_TESTS=1` - Required to run the tests
|
|
//! - `AWS_SM_ENDPOINT` - LocalStack endpoint (default: http://localhost:4566)
|
|
//! - `AWS_SM_REGION` - AWS region (default: us-east-1)
|
|
|
|
#[cfg(all(feature = "private", feature = "enterprise"))]
|
|
mod tests {
|
|
use windmill_common::secret_backend::{
|
|
test_aws_sm_connection, AwsSecretsManagerBackend, AwsSecretsManagerSettings,
|
|
SecretBackend,
|
|
};
|
|
|
|
fn should_run_aws_sm_tests() -> bool {
|
|
std::env::var("RUN_AWS_SM_TESTS")
|
|
.map(|v| v == "1" || v.to_lowercase() == "true")
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
macro_rules! skip_if_no_aws_sm {
|
|
() => {
|
|
if !should_run_aws_sm_tests() {
|
|
println!("Skipping test: RUN_AWS_SM_TESTS=1 not set");
|
|
println!("To run: RUN_AWS_SM_TESTS=1 cargo test -p windmill-common --features private,enterprise aws_sm_integration -- --nocapture");
|
|
println!("Requires: docker run -d -p 4566:4566 -e SERVICES=secretsmanager localstack/localstack:3.8");
|
|
return;
|
|
}
|
|
};
|
|
}
|
|
|
|
fn test_settings() -> AwsSecretsManagerSettings {
|
|
AwsSecretsManagerSettings {
|
|
region: std::env::var("AWS_SM_REGION").unwrap_or_else(|_| "us-east-1".to_string()),
|
|
access_key_id: Some("test".to_string()),
|
|
secret_access_key: Some("test".to_string()),
|
|
endpoint_url: Some(
|
|
std::env::var("AWS_SM_ENDPOINT")
|
|
.unwrap_or_else(|_| "http://localhost:4566".to_string()),
|
|
),
|
|
prefix: Some("windmill-test/".to_string()),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_connection() {
|
|
skip_if_no_aws_sm!();
|
|
let result = test_aws_sm_connection(&test_settings()).await;
|
|
assert!(result.is_ok(), "Connection test failed: {:?}", result.err());
|
|
println!(" ✓ Connection test passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_create_and_get_secret() {
|
|
skip_if_no_aws_sm!();
|
|
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
|
.await
|
|
.unwrap();
|
|
|
|
backend
|
|
.set_secret("test-ws", "u/admin/my_secret", "super-secret-value")
|
|
.await
|
|
.unwrap();
|
|
let value = backend.get_secret("test-ws", "u/admin/my_secret").await.unwrap();
|
|
assert_eq!(value, "super-secret-value");
|
|
|
|
backend.delete_secret("test-ws", "u/admin/my_secret").await.unwrap();
|
|
println!(" ✓ Create + Get + Delete roundtrip passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_update_secret() {
|
|
skip_if_no_aws_sm!();
|
|
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
|
.await
|
|
.unwrap();
|
|
|
|
backend
|
|
.set_secret("test-ws", "u/admin/update_test", "original")
|
|
.await
|
|
.unwrap();
|
|
backend
|
|
.set_secret("test-ws", "u/admin/update_test", "updated-value")
|
|
.await
|
|
.unwrap();
|
|
let value = backend
|
|
.get_secret("test-ws", "u/admin/update_test")
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(value, "updated-value");
|
|
|
|
backend
|
|
.delete_secret("test-ws", "u/admin/update_test")
|
|
.await
|
|
.unwrap();
|
|
println!(" ✓ Update secret passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_and_verify_gone() {
|
|
skip_if_no_aws_sm!();
|
|
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
|
.await
|
|
.unwrap();
|
|
|
|
backend
|
|
.set_secret("test-ws", "u/admin/delete_test", "to-be-deleted")
|
|
.await
|
|
.unwrap();
|
|
backend
|
|
.delete_secret("test-ws", "u/admin/delete_test")
|
|
.await
|
|
.unwrap();
|
|
|
|
let result = backend.get_secret("test-ws", "u/admin/delete_test").await;
|
|
assert!(
|
|
matches!(result, Err(windmill_common::error::Error::NotFound(_))),
|
|
"Expected NotFound after delete, got: {:?}",
|
|
result
|
|
);
|
|
println!(" ✓ Delete + verify gone passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_nonexistent_is_ok() {
|
|
skip_if_no_aws_sm!();
|
|
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
|
.await
|
|
.unwrap();
|
|
|
|
let result = backend
|
|
.delete_secret("test-ws", "u/admin/never_existed")
|
|
.await;
|
|
assert!(
|
|
result.is_ok(),
|
|
"Delete of nonexistent should be Ok, got: {:?}",
|
|
result.err()
|
|
);
|
|
println!(" ✓ Delete nonexistent is Ok");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_nonexistent_returns_not_found() {
|
|
skip_if_no_aws_sm!();
|
|
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
|
.await
|
|
.unwrap();
|
|
|
|
let result = backend
|
|
.get_secret("test-ws", "u/admin/does_not_exist")
|
|
.await;
|
|
assert!(
|
|
matches!(result, Err(windmill_common::error::Error::NotFound(_))),
|
|
"Expected NotFound, got: {:?}",
|
|
result
|
|
);
|
|
println!(" ✓ Get nonexistent returns NotFound");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_unicode_and_special_chars() {
|
|
skip_if_no_aws_sm!();
|
|
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
|
.await
|
|
.unwrap();
|
|
|
|
let secret_value = "p@$$w0rd with unicode: \u{1F512}\u{1F511} and <xml>&\"quotes\"";
|
|
backend
|
|
.set_secret("test-ws", "u/admin/unicode_test", secret_value)
|
|
.await
|
|
.unwrap();
|
|
let retrieved = backend
|
|
.get_secret("test-ws", "u/admin/unicode_test")
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(retrieved, secret_value);
|
|
|
|
backend
|
|
.delete_secret("test-ws", "u/admin/unicode_test")
|
|
.await
|
|
.unwrap();
|
|
println!(" ✓ Unicode and special chars passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_workspace_isolation() {
|
|
skip_if_no_aws_sm!();
|
|
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
|
.await
|
|
.unwrap();
|
|
|
|
// Create secrets in different workspaces with the same path
|
|
backend
|
|
.set_secret("workspace-a", "u/admin/shared_name", "value-a")
|
|
.await
|
|
.unwrap();
|
|
backend
|
|
.set_secret("workspace-b", "u/admin/shared_name", "value-b")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Each workspace should see its own value
|
|
let a = backend
|
|
.get_secret("workspace-a", "u/admin/shared_name")
|
|
.await
|
|
.unwrap();
|
|
let b = backend
|
|
.get_secret("workspace-b", "u/admin/shared_name")
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(a, "value-a");
|
|
assert_eq!(b, "value-b");
|
|
|
|
// Cleanup
|
|
backend
|
|
.delete_secret("workspace-a", "u/admin/shared_name")
|
|
.await
|
|
.unwrap();
|
|
backend
|
|
.delete_secret("workspace-b", "u/admin/shared_name")
|
|
.await
|
|
.unwrap();
|
|
println!(" ✓ Workspace isolation passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_backend_name() {
|
|
skip_if_no_aws_sm!();
|
|
let backend = AwsSecretsManagerBackend::new_with_client(test_settings())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(backend.backend_name(), "aws_secrets_manager");
|
|
println!(" ✓ Backend name is correct");
|
|
}
|
|
}
|
|
|
|
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
|
mod tests {
|
|
#[test]
|
|
fn test_aws_sm_requires_enterprise() {
|
|
println!("AWS Secrets Manager integration tests require Enterprise Edition features");
|
|
println!("Run with: cargo test -p windmill-common --features private,enterprise");
|
|
}
|
|
}
|