feat: GCP Secret Manager support
This commit is contained in:
+5
-5
@@ -163,7 +163,7 @@ fn generate_files_secret_injections(
|
||||
secrets: HashMap<&str, String>,
|
||||
run_config: &RunConfig,
|
||||
) -> Result<Vec<(PathBuf, String, String)>> {
|
||||
let re = Regex::new(r"\{\{([A-Za-z0-9_]+)\}\}")?;
|
||||
let re = Regex::new(r"\{\{(.+)\}\}")?;
|
||||
let mut results = Vec::new();
|
||||
for file in run_config
|
||||
.files
|
||||
@@ -283,14 +283,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_generate_files_secret_injections() {
|
||||
let mut secrets = HashMap::new();
|
||||
secrets.insert("SECRET1", "value1".to_string());
|
||||
secrets.insert("testing/SOME-secret", "value1".to_string());
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("test.txt");
|
||||
fs::write(&file_path, "{{SECRET1}}").unwrap();
|
||||
fs::write(&file_path, "{{testing/SOME-secret}}").unwrap();
|
||||
|
||||
let run_config = RunConfig {
|
||||
name: Some("test".to_string()),
|
||||
secrets: Some(vec!["SECRET1".to_string()]),
|
||||
secrets: Some(vec!["testing/SOME-secret".to_string()]),
|
||||
files: Some(vec![file_path.clone()]),
|
||||
flag: None,
|
||||
flag_position: None,
|
||||
@@ -301,7 +301,7 @@ mod tests {
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].0, file_path);
|
||||
assert_str_eq!(result[0].1, "{{SECRET1}}");
|
||||
assert_str_eq!(result[0].1, "{{testing/SOME-secret}}");
|
||||
assert_str_eq!(result[0].2, "value1");
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,10 @@ impl ProviderConfig {
|
||||
debug!("Using AWS Secrets Manager provider");
|
||||
provider_def
|
||||
}
|
||||
SupportedProvider::GcpSecretManager { provider_def } => {
|
||||
debug!("Using GCP Secret Manager provider");
|
||||
provider_def
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
use crate::providers::SecretProvider;
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use gcloud_sdk::google::cloud::secretmanager::v1;
|
||||
use gcloud_sdk::google::cloud::secretmanager::v1::replication::Automatic;
|
||||
use gcloud_sdk::google::cloud::secretmanager::v1::secret_manager_service_client::SecretManagerServiceClient;
|
||||
use gcloud_sdk::google::cloud::secretmanager::v1::{
|
||||
AccessSecretVersionRequest, AddSecretVersionRequest, CreateSecretRequest, ListSecretsRequest,
|
||||
Replication, Secret, replication,
|
||||
};
|
||||
use gcloud_sdk::proto_ext::secretmanager::SecretPayload;
|
||||
use gcloud_sdk::tonic::Code;
|
||||
use gcloud_sdk::{GoogleApi, GoogleAuthMiddleware};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use v1::DeleteSecretRequest;
|
||||
use validator::Validate;
|
||||
|
||||
type SecretsManagerClient = GoogleApi<SecretManagerServiceClient<GoogleAuthMiddleware>>;
|
||||
|
||||
#[skip_serializing_none]
|
||||
/// Configuration for GCP Secret Manager provider
|
||||
/// See [GCP Secret Manager](https://cloud.google.com/secret-manager)
|
||||
/// for more information.
|
||||
///
|
||||
/// This provider stores secrets in GCP Secret Manager. It requires
|
||||
/// a GCP project ID to be specified.
|
||||
///
|
||||
/// Example
|
||||
/// ```no_run
|
||||
/// use gman::providers::{SecretProvider, SupportedProvider};
|
||||
/// use gman::config::{Config, ProviderConfig};
|
||||
/// use gman::providers::gcp_secret_manager::GcpSecretManagerProvider;
|
||||
///
|
||||
/// let provider = GcpSecretManagerProvider {
|
||||
/// gcp_project_id: Some("my-gcp-project".to_string()),
|
||||
/// };
|
||||
/// let _ = provider.set_secret("MY_SECRET", "value");
|
||||
#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct GcpSecretManagerProvider {
|
||||
#[validate(required)]
|
||||
pub gcp_project_id: Option<String>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SecretProvider for GcpSecretManagerProvider {
|
||||
fn name(&self) -> &'static str {
|
||||
"GcpSecretManagerProvider"
|
||||
}
|
||||
|
||||
async fn get_secret(&self, key: &str) -> Result<String> {
|
||||
let secret_value = self
|
||||
.get_client()
|
||||
.await?
|
||||
.get()
|
||||
.access_secret_version(AccessSecretVersionRequest {
|
||||
name: format!(
|
||||
"projects/{}/secrets/{}/versions/latest",
|
||||
self.gcp_project_id.as_ref().unwrap(),
|
||||
key
|
||||
),
|
||||
})
|
||||
.await?
|
||||
.into_inner()
|
||||
.payload
|
||||
.ok_or_else(|| anyhow!("Secret '{}' not found", key))?
|
||||
.data
|
||||
.ref_sensitive_value()
|
||||
.to_vec();
|
||||
let secret_string = String::from_utf8(secret_value)
|
||||
.with_context(|| format!("Invalid UTF-8 in secret '{})'", key))?;
|
||||
|
||||
Ok(secret_string)
|
||||
}
|
||||
|
||||
async fn set_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||
let parent = format!("projects/{}", self.gcp_project_id.as_ref().unwrap());
|
||||
let secret_name = format!("{}/secrets/{}", parent, key);
|
||||
let secret = Secret {
|
||||
replication: Some(Replication {
|
||||
replication: Some(replication::Replication::Automatic(Automatic {
|
||||
customer_managed_encryption: None,
|
||||
})),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let client = self.get_client().await?;
|
||||
|
||||
client
|
||||
.get()
|
||||
.create_secret(CreateSecretRequest {
|
||||
parent: parent.clone(),
|
||||
secret_id: key.to_string(),
|
||||
secret: Some(secret),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.code() == Code::AlreadyExists {
|
||||
anyhow!("Secret already exists")
|
||||
} else {
|
||||
e.into()
|
||||
}
|
||||
})?;
|
||||
|
||||
let bytes = value.as_ref();
|
||||
let crc32c = crc32c::crc32c(bytes) as i64;
|
||||
client
|
||||
.get()
|
||||
.add_secret_version(AddSecretVersionRequest {
|
||||
parent: secret_name,
|
||||
payload: Some(SecretPayload {
|
||||
data: bytes.to_vec().into(),
|
||||
data_crc32c: Some(crc32c),
|
||||
}),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_secret(&self, key: &str) -> Result<()> {
|
||||
let name = format!(
|
||||
"projects/{}/secrets/{}",
|
||||
self.gcp_project_id.as_ref().unwrap(),
|
||||
key
|
||||
);
|
||||
self.get_client()
|
||||
.await?
|
||||
.get()
|
||||
.delete_secret(DeleteSecretRequest {
|
||||
name,
|
||||
etag: "".to_string(),
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_secret(&self, key: &str, value: &str) -> Result<()> {
|
||||
let parent = format!(
|
||||
"projects/{}/secrets/{}",
|
||||
self.gcp_project_id.as_ref().unwrap(),
|
||||
key
|
||||
);
|
||||
let bytes = value.as_ref();
|
||||
let crc32c = crc32c::crc32c(bytes) as i64;
|
||||
|
||||
self.get_client()
|
||||
.await?
|
||||
.get()
|
||||
.add_secret_version(AddSecretVersionRequest {
|
||||
parent,
|
||||
payload: Some(SecretPayload {
|
||||
data: bytes.to_vec().into(),
|
||||
data_crc32c: Some(crc32c),
|
||||
}),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_secrets(&self) -> Result<Vec<String>> {
|
||||
let request = ListSecretsRequest {
|
||||
parent: format!("projects/{}", self.gcp_project_id.as_ref().unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
let secrets = self
|
||||
.get_client()
|
||||
.await?
|
||||
.get()
|
||||
.list_secrets(request)
|
||||
.await?
|
||||
.into_inner()
|
||||
.secrets
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let full_secret_name = &s.name;
|
||||
|
||||
if let Some(secret_name) = full_secret_name.split("/secrets/").nth(1) {
|
||||
secret_name.to_string()
|
||||
} else {
|
||||
full_secret_name.to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(secrets)
|
||||
}
|
||||
}
|
||||
|
||||
impl GcpSecretManagerProvider {
|
||||
async fn get_client(&self) -> Result<SecretsManagerClient> {
|
||||
let client = GoogleApi::from_function(
|
||||
SecretManagerServiceClient::new,
|
||||
"https://secretmanager.googleapis.com",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -3,6 +3,7 @@
|
||||
//! Implementations provide storage/backends for secrets and a common
|
||||
//! interface used by the CLI.
|
||||
pub mod aws_secrets_manager;
|
||||
pub mod gcp_secret_manager;
|
||||
mod git_sync;
|
||||
pub mod local;
|
||||
|
||||
@@ -11,6 +12,8 @@ use anyhow::{Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use validator::{Validate, ValidationErrors};
|
||||
use aws_secrets_manager::AwsSecretsManagerProvider;
|
||||
use gcp_secret_manager::GcpSecretManagerProvider;
|
||||
|
||||
/// A secret storage backend capable of CRUD, with optional
|
||||
/// update, listing, and sync support.
|
||||
@@ -43,7 +46,6 @@ pub trait SecretProvider: Send + Sync {
|
||||
/// Registry of built-in providers.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
|
||||
#[serde(deny_unknown_fields, tag = "type", rename_all = "snake_case")]
|
||||
//TODO test that this works with the AWS config
|
||||
pub enum SupportedProvider {
|
||||
Local {
|
||||
#[serde(flatten)]
|
||||
@@ -51,7 +53,11 @@ pub enum SupportedProvider {
|
||||
},
|
||||
AwsSecretsManager {
|
||||
#[serde(flatten)]
|
||||
provider_def: aws_secrets_manager::AwsSecretsManagerProvider,
|
||||
provider_def: AwsSecretsManagerProvider,
|
||||
},
|
||||
GcpSecretManager {
|
||||
#[serde(flatten)]
|
||||
provider_def: GcpSecretManagerProvider,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -60,6 +66,7 @@ impl Validate for SupportedProvider {
|
||||
match self {
|
||||
SupportedProvider::Local { provider_def } => provider_def.validate(),
|
||||
SupportedProvider::AwsSecretsManager { provider_def } => provider_def.validate(),
|
||||
SupportedProvider::GcpSecretManager { provider_def } => provider_def.validate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,6 +84,7 @@ impl Display for SupportedProvider {
|
||||
match self {
|
||||
SupportedProvider::Local { .. } => write!(f, "local"),
|
||||
SupportedProvider::AwsSecretsManager { .. } => write!(f, "aws_secrets_manager"),
|
||||
SupportedProvider::GcpSecretManager { .. } => write!(f, "gcp_secret_manager"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user