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#[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 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 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 return None;
162 }
163 };
164
165 Some(match code.as_str() {
166 "AccessDeniedException"
168 | "UnauthorizedOperation"
169 | "InvalidUserException"
170 | "UnrecognizedClientException" => ErrorData::RemoteAccessDenied {
171 resource_type: "AWS Secrets Manager Secret".into(),
172 resource_name: resource.into(),
173 },
174 "ThrottlingException" | "RequestLimitExceeded" => {
176 ErrorData::RateLimitExceeded { message }
177 }
178 "InternalServiceException" | "ServiceUnavailableException" => {
180 ErrorData::RemoteServiceUnavailable { message }
181 }
182 "ResourceNotFoundException" => ErrorData::RemoteResourceNotFound {
184 resource_type: "AWS Secrets Manager Secret".into(),
185 resource_name: resource.into(),
186 },
187 "ResourceExistsException" => ErrorData::RemoteResourceConflict {
189 message,
190 resource_type: "AWS Secrets Manager Secret".into(),
191 resource_name: resource.into(),
192 },
193 "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 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 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 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#[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#[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 #[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 #[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#[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 #[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 #[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}