docbox_secrets/
aws.rs

1//! # AWS secret manager
2//!
3//! Secret manager backed by [AWS secrets manager](https://docs.aws.amazon.com/secretsmanager/).
4//! Inherits the loaded [SdkConfig] and all configuration provided to it.
5//!
6//! # Environment Variables
7//!
8//! * `DOCBOX_SECRETS_ENDPOINT` - URL to use when using a custom secrets manager endpoint
9//! * `DOCBOX_SECRETS_ACCESS_KEY_ID` - Access key ID when using a custom secrets manager endpoint
10//! * `DOCBOX_SECRETS_ACCESS_KEY_SECRET` - Access key secret when using a custom secrets manager endpoint
11//!
12use crate::{Secret, SecretManagerError, SecretManagerImpl, SetSecretOutcome};
13use aws_config::SdkConfig;
14use aws_sdk_secretsmanager::{
15    config::{Credentials, SharedCredentialsProvider},
16    error::SdkError,
17    operation::{
18        create_secret::CreateSecretError, delete_secret::DeleteSecretError,
19        get_secret_value::GetSecretValueError, update_secret::UpdateSecretError,
20    },
21};
22use serde::{Deserialize, Serialize};
23use std::fmt::Debug;
24use thiserror::Error;
25
26type SecretsManagerClient = aws_sdk_secretsmanager::Client;
27
28/// Config for the JSON secret manager
29#[derive(Debug, Default, Clone, Deserialize, Serialize)]
30#[serde(default)]
31pub struct AwsSecretManagerConfig {
32    /// Endpoint to use for requests
33    pub endpoint: AwsSecretsEndpoint,
34}
35
36impl AwsSecretManagerConfig {
37    /// Load a [AwsSecretManagerConfig] from the current environment
38    pub fn from_env() -> Result<Self, AwsSecretsManagerConfigError> {
39        let endpoint = AwsSecretsEndpoint::from_env()?;
40        Ok(Self { endpoint })
41    }
42}
43
44/// AWS secrets manager backed secrets
45#[derive(Clone)]
46pub struct AwsSecretManager {
47    client: SecretsManagerClient,
48}
49
50/// Endpoint to use for secrets manager operations
51#[derive(Default, Clone, Deserialize, Serialize)]
52#[serde(tag = "type", rename_all = "snake_case")]
53pub enum AwsSecretsEndpoint {
54    /// AWS default endpoint
55    #[default]
56    Aws,
57    /// Custom endpoint (Loker or other compatible)
58    Custom {
59        /// Endpoint URL
60        endpoint: String,
61        /// Access key ID to use
62        access_key_id: String,
63        /// Access key secret to use
64        access_key_secret: String,
65    },
66}
67
68/// Errors that could occur when loading the AWS configuration
69#[derive(Debug, Error)]
70pub enum AwsSecretsManagerConfigError {
71    /// Using a custom endpoint but didn't specify the access key ID
72    #[error(
73        "cannot use DOCBOX_SECRETS_ACCESS_KEY_ID without specifying DOCBOX_SECRETS_ACCESS_KEY_ID"
74    )]
75    MissingAccessKeyId,
76
77    /// Using a custom endpoint but didn't specify the access key secret
78    #[error(
79        "cannot use DOCBOX_SECRETS_ACCESS_KEY_SECRET without specifying DOCBOX_SECRETS_ACCESS_KEY_SECRET"
80    )]
81    MissingAccessKeySecret,
82}
83
84impl Debug for AwsSecretsEndpoint {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        match self {
87            Self::Aws => write!(f, "Aws"),
88            Self::Custom { endpoint, .. } => f
89                .debug_struct("Custom")
90                .field("endpoint", endpoint)
91                .finish(),
92        }
93    }
94}
95
96impl AwsSecretsEndpoint {
97    /// Load a [SecretsEndpoint] from the current environment
98    pub fn from_env() -> Result<Self, AwsSecretsManagerConfigError> {
99        match std::env::var("DOCBOX_SECRETS_ENDPOINT") {
100            // Using a custom secrets endpoint
101            Ok(endpoint_url) => {
102                let access_key_id = std::env::var("DOCBOX_SECRETS_ACCESS_KEY_ID")
103                    .map_err(|_| AwsSecretsManagerConfigError::MissingAccessKeyId)?;
104                let access_key_secret = std::env::var("DOCBOX_SECRETS_ACCESS_KEY_SECRET")
105                    .map_err(|_| AwsSecretsManagerConfigError::MissingAccessKeySecret)?;
106
107                Ok(AwsSecretsEndpoint::Custom {
108                    endpoint: endpoint_url,
109                    access_key_id,
110                    access_key_secret,
111                })
112            }
113            Err(_) => Ok(AwsSecretsEndpoint::Aws),
114        }
115    }
116}
117
118impl AwsSecretManager {
119    /// Create a [AwsSecretManager] from a [SdkConfig]
120    pub fn from_config(aws_config: &SdkConfig, config: AwsSecretManagerConfig) -> Self {
121        let client = match config.endpoint {
122            AwsSecretsEndpoint::Aws => SecretsManagerClient::new(aws_config),
123            AwsSecretsEndpoint::Custom {
124                endpoint,
125                access_key_id,
126                access_key_secret,
127            } => {
128                // Apply custom credentials and endpoint
129                let credentials =
130                    Credentials::new(access_key_id, access_key_secret, None, None, "docbox");
131                let aws_config = aws_config
132                    .to_builder()
133                    .endpoint_url(endpoint)
134                    .credentials_provider(SharedCredentialsProvider::new(credentials))
135                    .build();
136                SecretsManagerClient::new(&aws_config)
137            }
138        };
139
140        Self::new(client)
141    }
142
143    /// Create a [AwsSecretManager] from a [SecretsManagerClient]
144    pub fn new(client: SecretsManagerClient) -> Self {
145        Self { client }
146    }
147}
148
149/// Errors that could occur when working with AWS secret manager
150#[derive(Debug, Error)]
151pub enum AwsSecretError {
152    /// Failed to get a secret value
153    #[error("failed to get secret value")]
154    GetSecretValue(SdkError<GetSecretValueError>),
155
156    /// Failed to create a secret
157    #[error("failed to create secret")]
158    CreateSecret(SdkError<CreateSecretError>),
159
160    /// Failed to delete a secret
161    #[error("failed to delete secret")]
162    DeleteSecret(SdkError<DeleteSecretError>),
163
164    /// Failed to update a secret
165    #[error("failed to update secret")]
166    UpdateSecret(SdkError<UpdateSecretError>),
167}
168
169impl SecretManagerImpl for AwsSecretManager {
170    async fn get_secret(&self, name: &str) -> Result<Option<super::Secret>, SecretManagerError> {
171        let result = match self.client.get_secret_value().secret_id(name).send().await {
172            Ok(value) => value,
173            Err(error) => {
174                if error
175                    .as_service_error()
176                    .is_some_and(|value| value.is_resource_not_found_exception())
177                {
178                    return Ok(None);
179                }
180
181                tracing::error!(?error, "failed to get secret value");
182                return Err(AwsSecretError::GetSecretValue(error).into());
183            }
184        };
185
186        if let Some(value) = result.secret_string {
187            return Ok(Some(Secret::String(value)));
188        }
189
190        if let Some(value) = result.secret_binary {
191            return Ok(Some(Secret::Binary(value.into_inner())));
192        }
193
194        Ok(None)
195    }
196
197    async fn has_secret(&self, name: &str) -> Result<bool, SecretManagerError> {
198        self.get_secret(name).await.map(|value| value.is_some())
199    }
200
201    async fn set_secret(
202        &self,
203        name: &str,
204        value: &str,
205    ) -> Result<SetSecretOutcome, SecretManagerError> {
206        let error = match self
207            .client
208            .create_secret()
209            .secret_string(value)
210            .name(name)
211            .send()
212            .await
213        {
214            Ok(_) => return Ok(SetSecretOutcome::Created),
215            Err(err) => err,
216        };
217
218        // Handle secret already existing
219        if error
220            .as_service_error()
221            .is_some_and(|value| value.is_resource_exists_exception())
222        {
223            tracing::debug!("secret already exists, updating secret");
224
225            self.client
226                .update_secret()
227                .secret_string(value)
228                .secret_id(name)
229                .send()
230                .await
231                .map_err(|error| {
232                    tracing::error!(?error, "failed to update secret");
233                    AwsSecretError::UpdateSecret(error)
234                })?;
235
236            return Ok(SetSecretOutcome::Updated);
237        }
238
239        tracing::error!(?error, "failed to create secret");
240        Err(AwsSecretError::CreateSecret(error).into())
241    }
242
243    async fn delete_secret(&self, name: &str, force: bool) -> Result<(), SecretManagerError> {
244        let error = match self
245            .client
246            .delete_secret()
247            .secret_id(name)
248            .force_delete_without_recovery(force)
249            .send()
250            .await
251        {
252            Ok(_) => return Ok(()),
253            Err(error) => error,
254        };
255
256        // Handle secret doesn't exist
257        if error
258            .as_service_error()
259            .is_some_and(|value| value.is_resource_not_found_exception())
260        {
261            tracing::debug!("secret does not exist");
262            return Ok(());
263        }
264
265        tracing::error!(?error, "failed to create secret");
266        Err(AwsSecretError::DeleteSecret(error).into())
267    }
268}