use anyhow::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;
use crate::providers::SecretProvider;
use crate::providers::error::{SecretError, classify_gcp_error};
type SecretsManagerClient = GoogleApi<SecretManagerServiceClient<GoogleAuthMiddleware>>;
const PROVIDER: &str = "gcp_secret_manager";
#[skip_serializing_none]
#[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, SecretError> {
let response = self
.get_client()
.await?
.get()
.access_secret_version(AccessSecretVersionRequest {
name: format!(
"projects/{}/secrets/{}/versions/latest",
self.gcp_project_id.as_ref().unwrap(),
key
),
})
.await
.map_err(|e| classify_gcp_error(e.into(), Some(key), "get_secret"))?
.into_inner();
let payload = response.payload.ok_or_else(|| SecretError::NotFound {
key: key.to_string(),
provider: PROVIDER,
})?;
let secret_value = payload.data.ref_sensitive_value().to_vec();
let secret_string = String::from_utf8(secret_value)
.map_err(|_| SecretError::Other(anyhow!("secret value is not valid UTF-8")))?;
Ok(secret_string)
}
async fn set_secret(&self, key: &str, value: &str) -> Result<(), SecretError> {
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 {
SecretError::AlreadyExists {
key: key.to_string(),
provider: PROVIDER,
}
} else {
classify_gcp_error(e.into(), Some(key), "set_secret")
}
})?;
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
.map_err(|e| classify_gcp_error(e.into(), Some(key), "set_secret"))?;
Ok(())
}
async fn delete_secret(&self, key: &str) -> Result<(), SecretError> {
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
.map_err(|e| classify_gcp_error(e.into(), Some(key), "delete_secret"))?;
Ok(())
}
async fn update_secret(&self, key: &str, value: &str) -> Result<(), SecretError> {
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
.map_err(|e| classify_gcp_error(e.into(), Some(key), "update_secret"))?;
Ok(())
}
async fn list_secrets(&self) -> Result<Vec<String>, SecretError> {
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
.map_err(|e| classify_gcp_error(e.into(), None, "list_secrets"))?
.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, SecretError> {
let client = GoogleApi::from_function(
SecretManagerServiceClient::new,
"https://secretmanager.googleapis.com",
None,
)
.await
.map_err(|e| SecretError::AuthFailed {
provider: PROVIDER,
source: e.into(),
})?;
Ok(client)
}
}