feat: AWS Secrets Manager support
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
use crate::providers::SecretProvider;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use aws_config::Region;
|
||||
use aws_sdk_secretsmanager::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use validator::Validate;
|
||||
|
||||
#[skip_serializing_none]
|
||||
/// Configuration for AWS Secrets Manager provider
|
||||
/// See [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/)
|
||||
/// for more information.
|
||||
///
|
||||
/// This provider stores secrets in AWS Secrets Manager. It requires
|
||||
/// AWS credentials to be configured in the AWS configuration
|
||||
/// files for different AWS profiles.
|
||||
///
|
||||
/// Example
|
||||
/// ```no_run
|
||||
/// use gman::providers::{SecretProvider, SupportedProvider};
|
||||
/// use gman::config::{Config, ProviderConfig};
|
||||
/// use gman::providers::aws_secrets_manager::AwsSecretsManagerProvider;
|
||||
///
|
||||
/// let provider = AwsSecretsManagerProvider {
|
||||
/// aws_profile: Some("prod".to_string()),
|
||||
/// aws_region: Some("us-west-2".to_string()),
|
||||
/// };
|
||||
/// let _ = provider.set_secret("MY_SECRET", "value");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AwsSecretsManagerProvider {
|
||||
#[validate(required)]
|
||||
pub aws_profile: Option<String>,
|
||||
#[validate(required)]
|
||||
pub aws_region: Option<String>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SecretProvider for AwsSecretsManagerProvider {
|
||||
fn name(&self) -> &'static str {
|
||||
"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"))
|
||||
}
|
||||
|
||||
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}'"))?;
|
||||
|
||||
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<()> {
|
||||
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>> {
|
||||
self.get_client()
|
||||
.await?
|
||||
.list_secrets()
|
||||
.send()
|
||||
.await?
|
||||
.secret_list
|
||||
.with_context(|| "No secrets found")
|
||||
.map(|secrets| secrets.into_iter().filter_map(|s| s.name).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl AwsSecretsManagerProvider {
|
||||
async fn get_client(&self) -> Result<Client> {
|
||||
let region = self
|
||||
.aws_region
|
||||
.clone()
|
||||
.with_context(|| "aws_region is required")?;
|
||||
let profile = self
|
||||
.aws_profile
|
||||
.clone()
|
||||
.with_context(|| "aws_profile is required")?;
|
||||
|
||||
let config = aws_config::from_env()
|
||||
.region(Region::new(region))
|
||||
.profile_name(profile)
|
||||
.load()
|
||||
.await;
|
||||
|
||||
Ok(Client::new(&config))
|
||||
}
|
||||
}
|
||||
|
||||
+15
-16
@@ -1,4 +1,4 @@
|
||||
use anyhow::{Context, anyhow, bail};
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
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 as _, engine::general_purpose::STANDARD as B64};
|
||||
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
|
||||
use chacha20poly1305::aead::rand_core::RngCore;
|
||||
use chacha20poly1305::{
|
||||
Key, XChaCha20Poly1305, XNonce,
|
||||
aead::{Aead, KeyInit, OsRng},
|
||||
Key, XChaCha20Poly1305, XNonce,
|
||||
};
|
||||
use dialoguer::{Input, theme};
|
||||
use dialoguer::{theme, Input};
|
||||
use log::{debug, error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
@@ -36,15 +36,13 @@ use validator::Validate;
|
||||
/// Example
|
||||
/// ```no_run
|
||||
/// use gman::providers::local::LocalProvider;
|
||||
/// use gman::providers::SecretProvider;
|
||||
/// use gman::config::Config;
|
||||
/// use gman::providers::{SecretProvider, SupportedProvider};
|
||||
/// use gman::config::{Config, ProviderConfig};
|
||||
///
|
||||
/// let provider = LocalProvider::default();
|
||||
/// let cfg = Config::default();
|
||||
/// // Will prompt for a password when reading/writing secrets unless a
|
||||
/// // password file is configured.
|
||||
/// // provider.set_secret(&cfg, "MY_SECRET", "value")?;
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
/// let _ = provider.set_secret("MY_SECRET", "value");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -71,12 +69,13 @@ impl Default for LocalProvider {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SecretProvider for LocalProvider {
|
||||
fn name(&self) -> &'static str {
|
||||
"LocalProvider"
|
||||
}
|
||||
|
||||
fn get_secret(&self, key: &str) -> Result<String> {
|
||||
async fn get_secret(&self, key: &str) -> Result<String> {
|
||||
let vault_path = self.active_vault_path()?;
|
||||
let vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
||||
let envelope = vault
|
||||
@@ -90,7 +89,7 @@ impl SecretProvider for LocalProvider {
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
fn set_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||
async fn set_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||
let vault_path = self.active_vault_path()?;
|
||||
let mut vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
||||
if vault.contains_key(key) {
|
||||
@@ -109,7 +108,7 @@ impl SecretProvider for LocalProvider {
|
||||
store_vault(&vault_path, &vault).with_context(|| "failed to save secret to the vault")
|
||||
}
|
||||
|
||||
fn update_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||
async fn update_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||
let vault_path = self.active_vault_path()?;
|
||||
let mut vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
||||
|
||||
@@ -132,7 +131,7 @@ impl SecretProvider for LocalProvider {
|
||||
store_vault(&vault_path, &vault).with_context(|| "failed to save secret to the vault")
|
||||
}
|
||||
|
||||
fn delete_secret(&self, key: &str) -> Result<()> {
|
||||
async fn delete_secret(&self, key: &str) -> Result<()> {
|
||||
let vault_path = self.active_vault_path()?;
|
||||
let mut vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
||||
if !vault.contains_key(key) {
|
||||
@@ -144,7 +143,7 @@ impl SecretProvider for LocalProvider {
|
||||
store_vault(&vault_path, &vault).with_context(|| "failed to save secret to the vault")
|
||||
}
|
||||
|
||||
fn list_secrets(&self) -> Result<Vec<String>> {
|
||||
async fn list_secrets(&self) -> Result<Vec<String>> {
|
||||
let vault_path = self.active_vault_path()?;
|
||||
let vault: HashMap<String, String> = load_vault(&vault_path).unwrap_or_default();
|
||||
let keys: Vec<String> = vault.keys().cloned().collect();
|
||||
@@ -152,7 +151,7 @@ impl SecretProvider for LocalProvider {
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
fn sync(&mut self) -> Result<()> {
|
||||
async fn sync(&mut self) -> Result<()> {
|
||||
let mut config_changed = false;
|
||||
|
||||
if self.git_branch.is_none() {
|
||||
|
||||
+23
-18
@@ -2,43 +2,42 @@
|
||||
//!
|
||||
//! Implementations provide storage/backends for secrets and a common
|
||||
//! interface used by the CLI.
|
||||
pub mod aws_secrets_manager;
|
||||
mod git_sync;
|
||||
pub mod local;
|
||||
|
||||
use crate::providers::local::LocalProvider;
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use thiserror::Error;
|
||||
use validator::{Validate, ValidationErrors};
|
||||
|
||||
/// A secret storage backend capable of CRUD and sync, with optional
|
||||
/// update and listing
|
||||
pub trait SecretProvider {
|
||||
/// A secret storage backend capable of CRUD, with optional
|
||||
/// update, listing, and sync support.
|
||||
#[async_trait::async_trait]
|
||||
pub trait SecretProvider: Send + Sync {
|
||||
fn name(&self) -> &'static str;
|
||||
fn get_secret(&self, key: &str) -> Result<String>;
|
||||
fn set_secret(&self, key: &str, value: &str) -> Result<()>;
|
||||
fn update_secret(&self, _key: &str, _value: &str) -> Result<()> {
|
||||
async fn get_secret(&self, key: &str) -> Result<String>;
|
||||
async fn set_secret(&self, key: &str, value: &str) -> Result<()>;
|
||||
async fn update_secret(&self, _key: &str, _value: &str) -> Result<()> {
|
||||
Err(anyhow!(
|
||||
"update secret not supported for provider {}",
|
||||
self.name()
|
||||
))
|
||||
}
|
||||
fn delete_secret(&self, key: &str) -> Result<()>;
|
||||
fn list_secrets(&self) -> Result<Vec<String>> {
|
||||
async fn delete_secret(&self, key: &str) -> Result<()>;
|
||||
async fn list_secrets(&self) -> Result<Vec<String>> {
|
||||
Err(anyhow!(
|
||||
"list secrets is not supported for the provider {}",
|
||||
self.name()
|
||||
))
|
||||
}
|
||||
fn sync(&mut self) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Errors when parsing a provider identifier.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ParseProviderError {
|
||||
#[error("unsupported provider '{0}'")]
|
||||
Unsupported(String),
|
||||
async fn sync(&mut self) -> Result<()> {
|
||||
Err(anyhow!(
|
||||
"sync is not supported for the provider {}",
|
||||
self.name()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Registry of built-in providers.
|
||||
@@ -50,12 +49,17 @@ pub enum SupportedProvider {
|
||||
#[serde(flatten)]
|
||||
provider_def: LocalProvider,
|
||||
},
|
||||
AwsSecretsManager {
|
||||
#[serde(flatten)]
|
||||
provider_def: aws_secrets_manager::AwsSecretsManagerProvider,
|
||||
},
|
||||
}
|
||||
|
||||
impl Validate for SupportedProvider {
|
||||
fn validate(&self) -> Result<(), ValidationErrors> {
|
||||
match self {
|
||||
SupportedProvider::Local { provider_def } => provider_def.validate(),
|
||||
SupportedProvider::AwsSecretsManager { provider_def } => provider_def.validate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +76,7 @@ impl Display for SupportedProvider {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SupportedProvider::Local { .. } => write!(f, "local"),
|
||||
SupportedProvider::AwsSecretsManager { .. } => write!(f, "aws_secrets_manager"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user