1use 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#[derive(Debug, Default, Clone, Deserialize, Serialize)]
30#[serde(default)]
31pub struct AwsSecretManagerConfig {
32 pub endpoint: AwsSecretsEndpoint,
34}
35
36impl AwsSecretManagerConfig {
37 pub fn from_env() -> Result<Self, AwsSecretsManagerConfigError> {
39 let endpoint = AwsSecretsEndpoint::from_env()?;
40 Ok(Self { endpoint })
41 }
42}
43
44#[derive(Clone)]
46pub struct AwsSecretManager {
47 client: SecretsManagerClient,
48}
49
50#[derive(Default, Clone, Deserialize, Serialize)]
52#[serde(tag = "type", rename_all = "snake_case")]
53pub enum AwsSecretsEndpoint {
54 #[default]
56 Aws,
57 Custom {
59 endpoint: String,
61 access_key_id: String,
63 access_key_secret: String,
65 },
66}
67
68#[derive(Debug, Error)]
70pub enum AwsSecretsManagerConfigError {
71 #[error(
73 "cannot use DOCBOX_SECRETS_ACCESS_KEY_ID without specifying DOCBOX_SECRETS_ACCESS_KEY_ID"
74 )]
75 MissingAccessKeyId,
76
77 #[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 pub fn from_env() -> Result<Self, AwsSecretsManagerConfigError> {
99 match std::env::var("DOCBOX_SECRETS_ENDPOINT") {
100 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 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 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 pub fn new(client: SecretsManagerClient) -> Self {
145 Self { client }
146 }
147}
148
149#[derive(Debug, Error)]
151pub enum AwsSecretError {
152 #[error("failed to get secret value")]
154 GetSecretValue(SdkError<GetSecretValueError>),
155
156 #[error("failed to create secret")]
158 CreateSecret(SdkError<CreateSecretError>),
159
160 #[error("failed to delete secret")]
162 DeleteSecret(SdkError<DeleteSecretError>),
163
164 #[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 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 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}