secret_store 0.1.1

A unified, async secret store interface for Azure Key Vault, AWS Secrets Manager, GCP Secret Manager, and generic HTTP endpoints
Documentation
//! AWS Secrets Manager SDK abstraction and real adapter.

use async_trait::async_trait;
use aws_sdk_secretsmanager::Client;

use super::types::map_aws_error;
use crate::common::{Error, Result};

// ─────────────────────────────────────────────────────────────────────────────
// SDK abstraction trait
// ─────────────────────────────────────────────────────────────────────────────

/// Low-level AWS Secrets Manager operations.
///
/// The real implementation wraps the AWS SDK [`Client`]; unit tests inject a
/// `MockAwsSmOps` generated by `mockall`.
#[cfg_attr(test, mockall::automock)]
#[async_trait]
pub trait AwsSmOps: Send + Sync {
    /// Minimal label used by `Display` (e.g. `"AwsSecretsManager(region=us-east-1)"`).
    fn display_name(&self) -> String;
    /// Verbose info used by `Debug` (region, endpoint override, provider tag).
    fn debug_info(&self) -> String;

    async fn get(&self, name: &str) -> Result<String>;
    /// Creates a new secret (will fail if it already exists).
    async fn create(&self, name: &str, value: &str) -> Result<()>;
    /// Updates the value of an existing secret.
    async fn put(&self, name: &str, value: &str) -> Result<()>;
    async fn delete(&self, name: &str) -> Result<()>;
    async fn list(&self, prefix: Option<String>) -> Result<Vec<String>>;
}

// ─────────────────────────────────────────────────────────────────────────────
// Real SDK adapter
// ─────────────────────────────────────────────────────────────────────────────

/// Wraps the real `aws_sdk_secretsmanager::Client`.
pub(super) struct AwsSdkClient {
    pub client: Client,
}

#[async_trait]
impl AwsSmOps for AwsSdkClient {
    fn display_name(&self) -> String {
        let region = self
            .client
            .config()
            .region()
            .map(|r| r.as_ref().to_owned())
            .unwrap_or_else(|| "unknown".to_owned());
        format!("AwsSecretsManager(region={})", region)
    }

    fn debug_info(&self) -> String {
        let cfg = self.client.config();
        let region = cfg.region().map(|r| r.as_ref()).unwrap_or("unknown");
        format!("region={region}, provider=AwsSecretsManager")
    }

    async fn get(&self, name: &str) -> Result<String> {
        self.client
            .get_secret_value()
            .secret_id(name)
            .send()
            .await
            .map_err(|e| map_aws_error(name, e.into_service_error()))
            .map(|r| r.secret_string().unwrap_or_default().to_owned())
    }

    async fn create(&self, name: &str, value: &str) -> Result<()> {
        self.client
            .create_secret()
            .name(name)
            .secret_string(value)
            .send()
            .await
            .map(|_| ())
            .map_err(|e| map_aws_error(name, e.into_service_error()))
    }

    async fn put(&self, name: &str, value: &str) -> Result<()> {
        self.client
            .put_secret_value()
            .secret_id(name)
            .secret_string(value)
            .send()
            .await
            .map(|_| ())
            .map_err(|e| map_aws_error(name, e.into_service_error()))
    }

    async fn delete(&self, name: &str) -> Result<()> {
        self.client
            .delete_secret()
            .secret_id(name)
            .send()
            .await
            .map(|_| ())
            .map_err(|e| map_aws_error(name, e.into_service_error()))
    }

    async fn list(&self, prefix: Option<String>) -> Result<Vec<String>> {
        let mut names = Vec::new();
        let mut paginator = self.client.list_secrets().into_paginator().send();
        while let Some(page) = paginator.next().await {
            let page = page.map_err(|e| Error::Generic {
                store: "AwsSecretsManager",
                source: Box::new(e.into_service_error()),
            })?;
            for entry in page.secret_list() {
                if let Some(n) = entry.name() {
                    if prefix.as_deref().is_none_or(|p| n.starts_with(p)) {
                        names.push(n.to_owned());
                    }
                }
            }
        }
        Ok(names)
    }
}