feat: Full AWS SecretsManager support
This commit is contained in:
Generated
-1
@@ -1332,7 +1332,6 @@ dependencies = [
|
||||
"dialoguer",
|
||||
"dirs",
|
||||
"futures",
|
||||
"heck",
|
||||
"human-panic",
|
||||
"indoc",
|
||||
"log",
|
||||
|
||||
@@ -39,7 +39,6 @@ secrecy = "0.10.3"
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
zeroize = "1.8.1"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
heck = "0.5.0"
|
||||
serde_with = "3.14.0"
|
||||
serde_json = "1.0.143"
|
||||
dialoguer = "0.12.0"
|
||||
|
||||
@@ -90,7 +90,8 @@ gman aws sts get-caller-identity
|
||||
- [Installation](#installation)
|
||||
- [Configuration](#configuration)
|
||||
- [Providers](#providers)
|
||||
- [Provider: `local`](#provider-local)
|
||||
- [Local](#provider-local)
|
||||
- [AWS Secrets Manager](#provider-aws_secrets_manager)
|
||||
- [Run Configurations](#run-configurations)
|
||||
- [Environment Variable Secret Injection](#environment-variable-secret-injection)
|
||||
- [Inject Secrets via Command-Line Flags](#inject-secrets-via-command-line-flags)
|
||||
@@ -237,18 +238,17 @@ documented and added without breaking existing setups. The following table shows
|
||||
| 🚫 | Won't Add |
|
||||
|
||||
|
||||
| Provider Name | Status | Configuration Docs | Comments |
|
||||
|--------------------------------------------------------------------------------------------------------------------------|--------|--------------------------|--------------------------------------------|
|
||||
| `local` | ✅ | [Local](#provider-local) | |
|
||||
| [`aws_secrets_manager`](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) | 🕒 | | |
|
||||
| [`aws_ssm_parameter_store`](https://docs.aws.amazon.com/secretsmanager/latest/userguide/integrating_parameterstore.html) | 🕒 | | |
|
||||
| [`hashicorp_vault`](https://www.hashicorp.com/en/products/vault) | 🕒 | | |
|
||||
| [`azure_key_vault`](https://azure.microsoft.com/en-us/products/key-vault/) | 🕒 | | |
|
||||
| [`gcp_secret_manager`](https://cloud.google.com/security/products/secret-manager?hl=en) | 🕒 | | |
|
||||
| [`1password`](https://1password.com/) | 🕒 | | |
|
||||
| [`bitwarden`](https://bitwarden.com/) | 🕒 | | |
|
||||
| [`dashlane`](https://www.dashlane.com/) | 🕒 | | Waiting for CLI support for adding secrets |
|
||||
| [`lastpass`](https://www.lastpass.com/) | 🕒 | | |
|
||||
| Provider Name | Status | Configuration Docs | Comments |
|
||||
|--------------------------------------------------------------------------------------------------------------------------|--------|------------------------------------------------------|--------------------------------------------|
|
||||
| `local` | ✅ | [Local](#provider-local) | |
|
||||
| [`aws_secrets_manager`](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) | ✅ | [AWS Secrets Manager](#provider-aws_secrets_manager) | |
|
||||
| [`hashicorp_vault`](https://www.hashicorp.com/en/products/vault) | 🕒 | | |
|
||||
| [`azure_key_vault`](https://azure.microsoft.com/en-us/products/key-vault/) | 🕒 | | |
|
||||
| [`gcp_secret_manager`](https://cloud.google.com/security/products/secret-manager?hl=en) | 🕒 | | |
|
||||
| [`1password`](https://1password.com/) | 🕒 | | |
|
||||
| [`bitwarden`](https://bitwarden.com/) | 🕒 | | |
|
||||
| [`dashlane`](https://www.dashlane.com/) | 🕒 | | Waiting for CLI support for adding secrets |
|
||||
| [`lastpass`](https://www.lastpass.com/) | 🕒 | | |
|
||||
|
||||
### Provider: `local`
|
||||
|
||||
@@ -403,6 +403,35 @@ Then, all you need to do to run `managarr` with the secrets injected is:
|
||||
gman managarr
|
||||
```
|
||||
|
||||
### Provider: `aws_secrets_manager`
|
||||
|
||||
The `aws_secrets_manager` provider stores secrets in AWS Secrets Manager using the official AWS Rust SDK.
|
||||
|
||||
- Requires two fields: `aws_profile` and `aws_region`.
|
||||
- Uses the shared AWS config/credentials files under the named profile to authenticate.
|
||||
- Implements: `get`, `set`, `update`, `delete`, and `list`.
|
||||
|
||||
Configuration example:
|
||||
|
||||
```yaml
|
||||
default_provider: aws
|
||||
providers:
|
||||
- name: aws
|
||||
type: aws_secrets_manager
|
||||
aws_profile: default # Name from your ~/.aws/config and ~/.aws/credentials
|
||||
aws_region: us-east-1 # Region where your secrets live
|
||||
```
|
||||
|
||||
Important notes:
|
||||
- Deletions are immediate: the provider calls `DeleteSecret` with `force_delete_without_recovery = true`, so there is no
|
||||
recovery window. If you need a recovery window, do not delete via `gman`.
|
||||
- `add` uses `CreateSecret`. If the secret already exists, AWS returns an error. Use `update` to change an existing
|
||||
secret value.
|
||||
- IAM permissions: ensure the configured principal has `secretsmanager:GetSecretValue`, `CreateSecret`, `UpdateSecret`,
|
||||
`DeleteSecret`, and `ListSecrets` for the relevant region and ARNs.
|
||||
- Credential resolution: the provider explicitly selects the given `aws_profile` and `aws_region` via the AWS config
|
||||
loader; it does not fall back to other profiles or env-only defaults.
|
||||
|
||||
## Detailed Usage
|
||||
|
||||
### Storing and Managing Secrets
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use crate::command::preview_command;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use futures::future::join_all;
|
||||
use gman::config::{Config, RunConfig};
|
||||
use gman::providers::SecretProvider;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use clap::{
|
||||
crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser, ValueEnum,
|
||||
CommandFactory, Parser, ValueEnum, crate_authors, crate_description, crate_name, crate_version,
|
||||
};
|
||||
use std::ffi::OsString;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Subcommand;
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{disable_raw_mode, LeaveAlternateScreen};
|
||||
use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode};
|
||||
use gman::config::{get_config_file_path, load_config};
|
||||
use std::io::{self, IsTerminal, Read, Write};
|
||||
use std::panic::PanicHookInfo;
|
||||
|
||||
+1
-1
@@ -137,7 +137,7 @@ impl ProviderConfig {
|
||||
/// ```no_run
|
||||
/// # use gman::config::ProviderConfig;
|
||||
/// let mut provider_config = ProviderConfig::default();
|
||||
/// let provider = provider_config.extract_provider();
|
||||
/// let provider = provider_config.extract_provider();
|
||||
/// println!("using provider: {}", provider.name());
|
||||
/// ```
|
||||
pub fn extract_provider(&mut self) -> &mut dyn SecretProvider {
|
||||
|
||||
@@ -45,49 +45,51 @@ impl SecretProvider for AwsSecretsManagerProvider {
|
||||
|
||||
async fn get_secret(&self, key: &str) -> Result<String> {
|
||||
self.get_client()
|
||||
.await?
|
||||
.get_secret_value()
|
||||
.secret_id(key)
|
||||
.send()
|
||||
.await?
|
||||
.secret_string
|
||||
.with_context(|| format!("Secret '{key}' not found"))
|
||||
.await?
|
||||
.get_secret_value()
|
||||
.secret_id(key)
|
||||
.send()
|
||||
.await?
|
||||
.secret_string
|
||||
.with_context(|| format!("Secret '{key}' not found"))
|
||||
}
|
||||
|
||||
async fn set_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||
self.get_client()
|
||||
.await?
|
||||
.create_secret()
|
||||
.name(key)
|
||||
.secret_string(value)
|
||||
.send()
|
||||
.await.with_context(|| format!("Failed to set secret '{key}'"))?;
|
||||
.await?
|
||||
.create_secret()
|
||||
.name(key)
|
||||
.secret_string(value)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("Failed to set secret '{key}'"))?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||
self.get_client()
|
||||
.await?
|
||||
.update_secret()
|
||||
.secret_id(key)
|
||||
.secret_string(value)
|
||||
.send()
|
||||
.await.with_context(|| format!("Failed to update secret '{key}'"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_secret(&self, key: &str) -> Result<()> {
|
||||
async fn update_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||
self.get_client()
|
||||
.await?
|
||||
.delete_secret()
|
||||
.secret_id(key)
|
||||
.force_delete_without_recovery(true)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("Failed to delete secret '{key}'"))?;
|
||||
Ok(())
|
||||
.await?
|
||||
.update_secret()
|
||||
.secret_id(key)
|
||||
.secret_string(value)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("Failed to update secret '{key}'"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_secret(&self, key: &str) -> Result<()> {
|
||||
self.get_client()
|
||||
.await?
|
||||
.delete_secret()
|
||||
.secret_id(key)
|
||||
.force_delete_without_recovery(true)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("Failed to delete secret '{key}'"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_secrets(&self) -> Result<Vec<String>> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use anyhow::{Context, anyhow, bail};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -6,20 +6,20 @@ use std::{env, fs};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::providers::git_sync::{repo_name_from_url, sync_and_push, SyncOpts};
|
||||
use crate::providers::SecretProvider;
|
||||
use crate::providers::git_sync::{SyncOpts, repo_name_from_url, sync_and_push};
|
||||
use crate::{
|
||||
ARGON_M_COST_KIB, ARGON_P, ARGON_T_COST, HEADER, KDF, KEY_LEN, NONCE_LEN, SALT_LEN, VERSION,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use argon2::{Algorithm, Argon2, Params, Version};
|
||||
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
|
||||
use chacha20poly1305::aead::rand_core::RngCore;
|
||||
use chacha20poly1305::{
|
||||
aead::{Aead, KeyInit, OsRng},
|
||||
Key, XChaCha20Poly1305, XNonce,
|
||||
aead::{Aead, KeyInit, OsRng},
|
||||
};
|
||||
use dialoguer::{theme, Input};
|
||||
use dialoguer::{Input, theme};
|
||||
use log::{debug, error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
|
||||
@@ -7,7 +7,7 @@ mod git_sync;
|
||||
pub mod local;
|
||||
|
||||
use crate::providers::local::LocalProvider;
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use validator::{Validate, ValidationErrors};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
use gman::config::{Config, ProviderConfig};
|
||||
use gman::providers::aws_secrets_manager::AwsSecretsManagerProvider;
|
||||
use gman::providers::{SecretProvider, SupportedProvider};
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use validator::Validate;
|
||||
|
||||
#[test]
|
||||
fn test_aws_provider_name() {
|
||||
let provider = AwsSecretsManagerProvider {
|
||||
aws_profile: Some("default".into()),
|
||||
aws_region: Some("us-east-1".into()),
|
||||
};
|
||||
assert_str_eq!(provider.name(), "AwsSecretsManagerProvider");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aws_provider_validation_ok() {
|
||||
let provider = AwsSecretsManagerProvider {
|
||||
aws_profile: Some("default".into()),
|
||||
aws_region: Some("us-west-2".into()),
|
||||
};
|
||||
assert!(provider.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aws_provider_missing_profile() {
|
||||
let provider = AwsSecretsManagerProvider {
|
||||
aws_profile: None,
|
||||
aws_region: Some("us-west-2".into()),
|
||||
};
|
||||
assert!(provider.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aws_provider_missing_region() {
|
||||
let provider = AwsSecretsManagerProvider {
|
||||
aws_profile: Some("default".into()),
|
||||
aws_region: None,
|
||||
};
|
||||
assert!(provider.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aws_secrets_manager_provider_display_and_validate() {
|
||||
let sp = SupportedProvider::AwsSecretsManager {
|
||||
provider_def: AwsSecretsManagerProvider {
|
||||
aws_profile: Some("default".into()),
|
||||
aws_region: Some("eu-central-1".into()),
|
||||
},
|
||||
};
|
||||
// Validate delegates to inner provider
|
||||
assert!(sp.validate().is_ok());
|
||||
// Display formatting for the enum variant
|
||||
assert_eq!(sp.to_string(), "aws_secrets_manager");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_provider_config_with_aws_deserialize_and_extract() {
|
||||
// Minimal ProviderConfig YAML using the aws_secrets_manager variant
|
||||
let yaml = r#"---
|
||||
name: aws
|
||||
type: aws_secrets_manager
|
||||
aws_profile: default
|
||||
aws_region: us-east-1
|
||||
"#;
|
||||
|
||||
let pc: ProviderConfig = serde_yaml::from_str(yaml).expect("valid provider config yaml");
|
||||
// It should validate (both fields present)
|
||||
assert!(pc.validate().is_ok());
|
||||
|
||||
// Extract the provider and inspect its name via the trait
|
||||
let mut pc_owned = pc.clone();
|
||||
let provider: &mut dyn SecretProvider = pc_owned.extract_provider();
|
||||
assert_eq!(provider.name(), "AwsSecretsManagerProvider");
|
||||
|
||||
// Round-trip through Config to ensure flattening works in a real list
|
||||
let cfg_yaml = r#"---
|
||||
default_provider: aws
|
||||
providers:
|
||||
- name: aws
|
||||
type: aws_secrets_manager
|
||||
aws_profile: default
|
||||
aws_region: us-east-1
|
||||
"#;
|
||||
let cfg: Config = serde_yaml::from_str(cfg_yaml).expect("valid config yaml");
|
||||
assert!(cfg.validate().is_ok());
|
||||
|
||||
let extracted = cfg
|
||||
.extract_provider_config(None)
|
||||
.expect("should find default provider");
|
||||
assert_eq!(extracted.name.as_deref(), Some("aws"));
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use gman::config::Config;
|
||||
use gman::config::{Config, ProviderConfig};
|
||||
use gman::providers::local::LocalProvider;
|
||||
use gman::providers::{SecretProvider, SupportedProvider};
|
||||
use pretty_assertions::assert_eq;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use validator::Validate;
|
||||
@@ -13,6 +14,17 @@ fn test_local_provider_name() {
|
||||
assert_str_eq!(provider.name(), "LocalProvider");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_local_provider_display_and_validate() {
|
||||
let sp = SupportedProvider::Local {
|
||||
provider_def: LocalProvider::default(),
|
||||
};
|
||||
// Validate delegates to inner provider
|
||||
assert!(sp.validate().is_ok());
|
||||
// Display formatting for the enum variant
|
||||
assert_eq!(sp.to_string(), "local");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_local_provider_valid() {
|
||||
let provider = LocalProvider {
|
||||
@@ -54,3 +66,37 @@ fn test_local_provider_default() {
|
||||
assert_eq!(provider.git_user_email, None);
|
||||
assert_eq!(provider.git_executable, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_provider_config_with_local_deserialize_and_extract() {
|
||||
// Minimal ProviderConfig YAML using the local variant
|
||||
let yaml = r#"---
|
||||
name: local
|
||||
type: local
|
||||
"#;
|
||||
|
||||
let pc: ProviderConfig = serde_yaml::from_str(yaml).expect("valid provider config yaml");
|
||||
assert!(pc.validate().is_ok());
|
||||
|
||||
let mut pc_owned = pc.clone();
|
||||
let provider: &mut dyn SecretProvider = pc_owned.extract_provider();
|
||||
assert_eq!(provider.name(), "LocalProvider");
|
||||
|
||||
// Round-trip through Config to ensure flattening works in a real list
|
||||
let cfg_yaml = r#"---
|
||||
default_provider: local
|
||||
providers:
|
||||
- name: local
|
||||
type: local
|
||||
password_file: /tmp/.gman_pass
|
||||
git_branch: main
|
||||
git_remote_url: git@github.com:username/repo.git
|
||||
"#;
|
||||
let cfg: Config = serde_yaml::from_str(cfg_yaml).expect("valid config yaml");
|
||||
assert!(cfg.validate().is_ok());
|
||||
|
||||
let extracted = cfg
|
||||
.extract_provider_config(None)
|
||||
.expect("should find default provider");
|
||||
assert_eq!(extracted.name.as_deref(), Some("local"));
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
mod aws_secrets_manager_tests;
|
||||
mod local_tests;
|
||||
mod provider_tests;
|
||||
|
||||
Reference in New Issue
Block a user