feat: GCP Secret Manager support

This commit is contained in:
2025-09-12 18:36:16 -06:00
parent ac45287336
commit bcbd755a37
6 changed files with 768 additions and 44 deletions
+201
View File
@@ -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
View File
@@ -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"),
}
}
}