Skip to main content

alien_aws_clients/aws/
secrets_manager.rs

1use crate::aws::aws_request_utils::{AwsRequestBuilderExt, AwsSignConfig};
2use crate::aws::credential_provider::AwsCredentialProvider;
3use alien_client_core::{ErrorData, Result};
4
5use alien_error::{Context, ContextError, IntoAlienError};
6use bon::Builder;
7use reqwest::{Client, StatusCode};
8use serde::de::DeserializeOwned;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use uuid::Uuid;
12
13#[cfg(feature = "test-utils")]
14use mockall::automock;
15
16#[cfg_attr(feature = "test-utils", automock)]
17#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
18#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
19pub trait SecretsManagerApi: Send + Sync + std::fmt::Debug {
20    async fn create_secret(&self, request: CreateSecretRequest) -> Result<CreateSecretResponse>;
21    async fn update_secret(&self, request: UpdateSecretRequest) -> Result<UpdateSecretResponse>;
22    async fn delete_secret(&self, request: DeleteSecretRequest) -> Result<DeleteSecretResponse>;
23    async fn describe_secret(
24        &self,
25        request: DescribeSecretRequest,
26    ) -> Result<DescribeSecretResponse>;
27    async fn get_secret_value(
28        &self,
29        request: GetSecretValueRequest,
30    ) -> Result<GetSecretValueResponse>;
31    async fn put_secret_value(
32        &self,
33        request: PutSecretValueRequest,
34    ) -> Result<PutSecretValueResponse>;
35}
36
37// ---------------------------------------------------------------------------
38// Secrets Manager client using new request helpers.
39// ---------------------------------------------------------------------------
40#[derive(Debug, Clone)]
41pub struct SecretsManagerClient {
42    client: Client,
43    credentials: AwsCredentialProvider,
44}
45
46impl SecretsManagerClient {
47    pub fn new(client: Client, credentials: AwsCredentialProvider) -> Self {
48        Self {
49            client,
50            credentials,
51        }
52    }
53
54    fn sign_config(&self) -> AwsSignConfig {
55        AwsSignConfig {
56            service_name: "secretsmanager".into(),
57            region: self.credentials.region().to_string(),
58            credentials: self.credentials.get_credentials(),
59            signing_region: None,
60        }
61    }
62
63    fn get_base_url(&self) -> String {
64        if let Some(override_url) = self
65            .credentials
66            .get_service_endpoint_option("secretsmanager")
67        {
68            override_url.to_string()
69        } else {
70            format!(
71                "https://secretsmanager.{}.amazonaws.com",
72                self.credentials.region()
73            )
74        }
75    }
76
77    // ------------------------- internal helpers -------------------------
78
79    async fn send_json<T: DeserializeOwned + Send + 'static>(
80        &self,
81        target: &str,
82        body: String,
83        operation: &str,
84        resource: &str,
85    ) -> Result<T> {
86        self.credentials.ensure_fresh().await?;
87        let base_url = self.get_base_url();
88        let url = format!("{}/", base_url.trim_end_matches('/'));
89
90        let builder = self
91            .client
92            .post(&url)
93            .host(&format!(
94                "secretsmanager.{}.amazonaws.com",
95                self.credentials.region()
96            ))
97            .header("X-Amz-Target", target)
98            .content_type_amz_json()
99            .content_sha256(&body)
100            .body(body.clone());
101
102        let result =
103            crate::aws::aws_request_utils::sign_send_json(builder, &self.sign_config()).await;
104
105        Self::map_result(result, operation, resource, Some(&body))
106    }
107
108    fn map_result<T>(
109        result: Result<T>,
110        operation: &str,
111        resource: &str,
112        request_body: Option<&str>,
113    ) -> Result<T> {
114        match result {
115            Ok(v) => Ok(v),
116            Err(e) => {
117                if let Some(ErrorData::HttpResponseError {
118                    http_status,
119                    http_response_text: Some(ref text),
120                    ..
121                }) = &e.error
122                {
123                    let status = StatusCode::from_u16(*http_status)
124                        .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
125                    if let Some(mapped) = Self::map_secrets_manager_error(
126                        status,
127                        text,
128                        operation,
129                        resource,
130                        request_body,
131                    ) {
132                        Err(e.context(mapped))
133                    } else {
134                        // Couldn't parse Secrets Manager error, use original error
135                        Err(e)
136                    }
137                } else {
138                    Err(e)
139                }
140            }
141        }
142    }
143
144    fn map_secrets_manager_error(
145        status: StatusCode,
146        body: &str,
147        _operation: &str,
148        resource: &str,
149        request_body: Option<&str>,
150    ) -> Option<ErrorData> {
151        let parsed: std::result::Result<SecretsManagerErrorResponse, _> =
152            serde_json::from_str(body);
153        let (code, message) = match parsed {
154            Ok(e) => {
155                let c = e.error_type.unwrap_or_else(|| "UnknownErrorCode".into());
156                let m = e.message.unwrap_or_else(|| "Unknown error".into());
157                (c, m)
158            }
159            Err(_) => {
160                // If we can't parse the response, return None to use original error
161                return None;
162            }
163        };
164
165        Some(match code.as_str() {
166            // Access / auth
167            "AccessDeniedException"
168            | "UnauthorizedOperation"
169            | "InvalidUserException"
170            | "UnrecognizedClientException" => ErrorData::RemoteAccessDenied {
171                resource_type: "AWS Secrets Manager Secret".into(),
172                resource_name: resource.into(),
173            },
174            // Throttling
175            "ThrottlingException" | "RequestLimitExceeded" => {
176                ErrorData::RateLimitExceeded { message }
177            }
178            // Service unavailable
179            "InternalServiceException" | "ServiceUnavailableException" => {
180                ErrorData::RemoteServiceUnavailable { message }
181            }
182            // Resource not found
183            "ResourceNotFoundException" => ErrorData::RemoteResourceNotFound {
184                resource_type: "AWS Secrets Manager Secret".into(),
185                resource_name: resource.into(),
186            },
187            // Conflict / already exists
188            "ResourceExistsException" => ErrorData::RemoteResourceConflict {
189                message,
190                resource_type: "AWS Secrets Manager Secret".into(),
191                resource_name: resource.into(),
192            },
193            // Invalid input
194            "InvalidParameterException"
195            | "ValidationException"
196            | "MalformedPolicyDocumentException" => ErrorData::InvalidInput {
197                message,
198                field_name: None,
199            },
200            _ => match status {
201                StatusCode::NOT_FOUND => ErrorData::RemoteResourceNotFound {
202                    resource_type: "AWS Secrets Manager Secret".into(),
203                    resource_name: resource.into(),
204                },
205                StatusCode::CONFLICT => ErrorData::RemoteResourceConflict {
206                    message,
207                    resource_type: "AWS Secrets Manager Secret".into(),
208                    resource_name: resource.into(),
209                },
210                StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => ErrorData::RemoteAccessDenied {
211                    resource_type: "AWS Secrets Manager Secret".into(),
212                    resource_name: resource.into(),
213                },
214                StatusCode::TOO_MANY_REQUESTS => ErrorData::RateLimitExceeded { message },
215                StatusCode::SERVICE_UNAVAILABLE
216                | StatusCode::BAD_GATEWAY
217                | StatusCode::GATEWAY_TIMEOUT => ErrorData::RemoteServiceUnavailable { message },
218                _ => ErrorData::HttpResponseError {
219                    message: format!("Secrets Manager operation failed: {}", message),
220                    url: format!("secretsmanager.amazonaws.com"),
221                    http_status: status.as_u16(),
222                    http_response_text: Some(body.into()),
223                    http_request_text: request_body.map(|s| s.to_string()),
224                },
225            },
226        })
227    }
228}
229
230#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
231#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
232impl SecretsManagerApi for SecretsManagerClient {
233    async fn create_secret(&self, request: CreateSecretRequest) -> Result<CreateSecretResponse> {
234        // Ensure client_request_token is provided for idempotency
235        let mut request = request;
236        if request.client_request_token.is_none() {
237            request.client_request_token = Some(Uuid::new_v4().to_string());
238        }
239
240        let body = serde_json::to_string(&request).into_alien_error().context(
241            ErrorData::SerializationError {
242                message: format!(
243                    "Failed to serialize CreateSecretRequest for secret '{}'",
244                    request.name
245                ),
246            },
247        )?;
248        self.send_json(
249            "secretsmanager.CreateSecret",
250            body,
251            "CreateSecret",
252            &request.name,
253        )
254        .await
255    }
256
257    async fn update_secret(&self, request: UpdateSecretRequest) -> Result<UpdateSecretResponse> {
258        // Ensure client_request_token is provided for idempotency
259        let mut request = request;
260        if request.client_request_token.is_none() {
261            request.client_request_token = Some(Uuid::new_v4().to_string());
262        }
263
264        let body = serde_json::to_string(&request).into_alien_error().context(
265            ErrorData::SerializationError {
266                message: format!(
267                    "Failed to serialize UpdateSecretRequest for secret '{}'",
268                    request.secret_id
269                ),
270            },
271        )?;
272        self.send_json(
273            "secretsmanager.UpdateSecret",
274            body,
275            "UpdateSecret",
276            &request.secret_id,
277        )
278        .await
279    }
280
281    async fn delete_secret(&self, request: DeleteSecretRequest) -> Result<DeleteSecretResponse> {
282        let body = serde_json::to_string(&request).into_alien_error().context(
283            ErrorData::SerializationError {
284                message: format!(
285                    "Failed to serialize DeleteSecretRequest for secret '{}'",
286                    request.secret_id
287                ),
288            },
289        )?;
290        self.send_json(
291            "secretsmanager.DeleteSecret",
292            body,
293            "DeleteSecret",
294            &request.secret_id,
295        )
296        .await
297    }
298
299    async fn describe_secret(
300        &self,
301        request: DescribeSecretRequest,
302    ) -> Result<DescribeSecretResponse> {
303        let body = serde_json::to_string(&request).into_alien_error().context(
304            ErrorData::SerializationError {
305                message: format!(
306                    "Failed to serialize DescribeSecretRequest for secret '{}'",
307                    request.secret_id
308                ),
309            },
310        )?;
311        self.send_json(
312            "secretsmanager.DescribeSecret",
313            body,
314            "DescribeSecret",
315            &request.secret_id,
316        )
317        .await
318    }
319
320    async fn get_secret_value(
321        &self,
322        request: GetSecretValueRequest,
323    ) -> Result<GetSecretValueResponse> {
324        let body = serde_json::to_string(&request).into_alien_error().context(
325            ErrorData::SerializationError {
326                message: format!(
327                    "Failed to serialize GetSecretValueRequest for secret '{}'",
328                    request.secret_id
329                ),
330            },
331        )?;
332        self.send_json(
333            "secretsmanager.GetSecretValue",
334            body,
335            "GetSecretValue",
336            &request.secret_id,
337        )
338        .await
339    }
340
341    async fn put_secret_value(
342        &self,
343        request: PutSecretValueRequest,
344    ) -> Result<PutSecretValueResponse> {
345        // Ensure client_request_token is provided for idempotency
346        let mut request = request;
347        if request.client_request_token.is_none() {
348            request.client_request_token = Some(Uuid::new_v4().to_string());
349        }
350
351        let body = serde_json::to_string(&request).into_alien_error().context(
352            ErrorData::SerializationError {
353                message: format!(
354                    "Failed to serialize PutSecretValueRequest for secret '{}'",
355                    request.secret_id
356                ),
357            },
358        )?;
359        self.send_json(
360            "secretsmanager.PutSecretValue",
361            body,
362            "PutSecretValue",
363            &request.secret_id,
364        )
365        .await
366    }
367}
368
369// ---------------------------------------------------------------------------
370// Error JSON mapping structs
371// ---------------------------------------------------------------------------
372#[derive(Debug, Deserialize)]
373struct SecretsManagerErrorResponse {
374    #[serde(rename = "__type")]
375    error_type: Option<String>,
376    #[serde(rename = "message")]
377    message: Option<String>,
378}
379
380// ---------------------------------------------------------------------------
381// Request / response payloads
382// ---------------------------------------------------------------------------
383
384#[derive(Debug, Clone, Serialize, Builder)]
385#[serde(rename_all = "PascalCase")]
386pub struct CreateSecretRequest {
387    pub name: String,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub description: Option<String>,
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub secret_string: Option<String>,
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub secret_binary: Option<String>,
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub kms_key_id: Option<String>,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub tags: Option<Vec<Tag>>,
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub force_overwrite_replica_secret: Option<bool>,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub replica_regions: Option<Vec<ReplicaRegionType>>,
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub client_request_token: Option<String>,
404}
405
406#[derive(Debug, Deserialize)]
407#[serde(rename_all = "PascalCase")]
408pub struct CreateSecretResponse {
409    // AWS returns the key all-caps ("ARN"); PascalCase would expect "Arn".
410    #[serde(rename = "ARN")]
411    pub arn: Option<String>,
412    pub name: Option<String>,
413    pub version_id: Option<String>,
414    pub replica_regions: Option<Vec<ReplicaRegionType>>,
415}
416
417#[derive(Debug, Clone, Serialize, Builder)]
418#[serde(rename_all = "PascalCase")]
419pub struct UpdateSecretRequest {
420    pub secret_id: String,
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub description: Option<String>,
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub kms_key_id: Option<String>,
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub secret_string: Option<String>,
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub secret_binary: Option<String>,
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub client_request_token: Option<String>,
431}
432
433#[derive(Debug, Deserialize)]
434#[serde(rename_all = "PascalCase")]
435pub struct UpdateSecretResponse {
436    #[serde(rename = "ARN")]
437    pub arn: Option<String>,
438    pub name: Option<String>,
439    pub version_id: Option<String>,
440}
441
442#[derive(Debug, Clone, Serialize, Builder)]
443#[serde(rename_all = "PascalCase")]
444pub struct DeleteSecretRequest {
445    pub secret_id: String,
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub force_delete_without_recovery: Option<bool>,
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub recovery_window_in_days: Option<i64>,
450}
451
452#[derive(Debug, Deserialize)]
453#[serde(rename_all = "PascalCase")]
454pub struct DeleteSecretResponse {
455    #[serde(rename = "ARN")]
456    pub arn: Option<String>,
457    pub name: Option<String>,
458    pub deletion_date: Option<f64>,
459}
460
461#[derive(Debug, Clone, Serialize, Builder)]
462#[serde(rename_all = "PascalCase")]
463pub struct DescribeSecretRequest {
464    pub secret_id: String,
465}
466
467#[derive(Debug, Deserialize)]
468#[serde(rename_all = "PascalCase")]
469pub struct DescribeSecretResponse {
470    #[serde(rename = "ARN")]
471    pub arn: Option<String>,
472    pub name: Option<String>,
473    pub description: Option<String>,
474    pub kms_key_id: Option<String>,
475    pub rotation_enabled: Option<bool>,
476    // AWS spells the suffix all-caps here too ("RotationLambdaARN") — see CreateSecretResponse.
477    #[serde(rename = "RotationLambdaARN")]
478    pub rotation_lambda_arn: Option<String>,
479    pub rotation_rules: Option<RotationRulesType>,
480    pub last_rotated_date: Option<f64>,
481    pub last_changed_date: Option<f64>,
482    pub last_accessed_date: Option<f64>,
483    pub deletion_date: Option<f64>,
484    pub tags: Option<Vec<Tag>>,
485    pub version_ids_to_stages: Option<HashMap<String, Vec<String>>>,
486    pub owning_service: Option<String>,
487    pub created_date: Option<f64>,
488    pub primary_region: Option<String>,
489    pub replica_regions: Option<Vec<ReplicaRegionType>>,
490}
491
492#[derive(Debug, Clone, Serialize, Builder)]
493#[serde(rename_all = "PascalCase")]
494pub struct GetSecretValueRequest {
495    pub secret_id: String,
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub version_id: Option<String>,
498    #[serde(skip_serializing_if = "Option::is_none")]
499    pub version_stage: Option<String>,
500}
501
502#[derive(Debug, Deserialize)]
503#[serde(rename_all = "PascalCase")]
504pub struct GetSecretValueResponse {
505    #[serde(rename = "ARN")]
506    pub arn: Option<String>,
507    pub name: Option<String>,
508    pub version_id: Option<String>,
509    pub secret_binary: Option<String>,
510    pub secret_string: Option<String>,
511    pub version_stages: Option<Vec<String>>,
512    pub created_date: Option<f64>,
513}
514
515#[derive(Debug, Clone, Serialize, Builder)]
516#[serde(rename_all = "PascalCase")]
517pub struct PutSecretValueRequest {
518    pub secret_id: String,
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub secret_binary: Option<String>,
521    #[serde(skip_serializing_if = "Option::is_none")]
522    pub secret_string: Option<String>,
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub version_stages: Option<Vec<String>>,
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub client_request_token: Option<String>,
527}
528
529#[derive(Debug, Deserialize)]
530#[serde(rename_all = "PascalCase")]
531pub struct PutSecretValueResponse {
532    #[serde(rename = "ARN")]
533    pub arn: Option<String>,
534    pub name: Option<String>,
535    pub version_id: Option<String>,
536    pub version_stages: Option<Vec<String>>,
537}
538
539// ---------------------------------------------------------------------------
540// Supporting structs
541// ---------------------------------------------------------------------------
542
543#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
544#[serde(rename_all = "PascalCase")]
545pub struct Tag {
546    pub key: String,
547    pub value: String,
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
551#[serde(rename_all = "PascalCase")]
552pub struct ReplicaRegionType {
553    pub region: String,
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub kms_key_id: Option<String>,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
559#[serde(rename_all = "PascalCase")]
560pub struct RotationRulesType {
561    #[serde(skip_serializing_if = "Option::is_none")]
562    pub automatically_after_days: Option<i64>,
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568
569    // AWS returns the ARN key all-caps ("ARN"), not PascalCase ("Arn"); the rename pins that
570    // so responses populate `arn`.
571    #[test]
572    fn create_secret_response_parses_all_caps_arn() {
573        let body = r#"{"ARN":"arn:aws:secretsmanager:us-east-1:0:secret:db-abc","Name":"db"}"#;
574        let resp: CreateSecretResponse = serde_json::from_str(body).expect("parses");
575        assert_eq!(
576            resp.arn.as_deref(),
577            Some("arn:aws:secretsmanager:us-east-1:0:secret:db-abc")
578        );
579    }
580
581    #[test]
582    fn get_secret_value_response_parses_all_caps_arn() {
583        let body =
584            r#"{"ARN":"arn:aws:secretsmanager:us-east-1:0:secret:db-abc","SecretString":"pw"}"#;
585        let resp: GetSecretValueResponse = serde_json::from_str(body).expect("parses");
586        assert_eq!(
587            resp.arn.as_deref(),
588            Some("arn:aws:secretsmanager:us-east-1:0:secret:db-abc")
589        );
590    }
591
592    // A PascalCase "Arn" must NOT populate `arn`: the field is keyed to all-caps "ARN".
593    #[test]
594    fn pascalcase_arn_key_does_not_populate() {
595        let body = r#"{"Arn":"arn:aws:secretsmanager:us-east-1:0:secret:db-abc","Name":"db"}"#;
596        let resp: CreateSecretResponse = serde_json::from_str(body).expect("parses");
597        assert_eq!(resp.arn, None);
598    }
599}