Skip to main content

awsim_secretsmanager/
lib.rs

1pub mod authz;
2pub mod error;
3mod operations;
4pub mod state;
5mod util;
6
7pub use authz::{SecretsManagerResourcePolicyLookup, SecretsManagerSecretLookup};
8
9use async_trait::async_trait;
10use awsim_core::{
11    AccountRegionStore, AwsError, LambdaInvoker, Protocol, RequestContext, ServiceHandler,
12};
13use serde_json::Value;
14use std::sync::Arc;
15use tracing::debug;
16
17use state::SecretsState;
18
19/// The Secrets Manager service handler.
20pub struct SecretsManagerService {
21    store: AccountRegionStore<SecretsState>,
22    lambda_invoker: Option<Arc<dyn LambdaInvoker>>,
23}
24
25impl SecretsManagerService {
26    pub fn new() -> Self {
27        Self {
28            store: AccountRegionStore::new(),
29            lambda_invoker: None,
30        }
31    }
32
33    /// Attach a Lambda invoker so `RotateSecret` can dispatch the
34    /// four-step rotation state machine against the customer's
35    /// rotation Lambda. When absent, `RotateSecret` falls back to the
36    /// in-process simulation (used by tests and bare deployments).
37    pub fn with_lambda_invoker(mut self, invoker: Arc<dyn LambdaInvoker>) -> Self {
38        self.lambda_invoker = Some(invoker);
39        self
40    }
41
42    pub fn store(&self) -> AccountRegionStore<SecretsState> {
43        self.store.clone()
44    }
45
46    pub fn lambda_invoker(&self) -> Option<&Arc<dyn LambdaInvoker>> {
47        self.lambda_invoker.as_ref()
48    }
49}
50
51impl Default for SecretsManagerService {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57#[async_trait]
58impl ServiceHandler for SecretsManagerService {
59    fn service_name(&self) -> &str {
60        "secretsmanager"
61    }
62
63    fn signing_name(&self) -> &str {
64        "secretsmanager"
65    }
66
67    fn protocol(&self) -> Protocol {
68        Protocol::AwsJson1_1
69    }
70
71    async fn handle(
72        &self,
73        operation: &str,
74        input: Value,
75        ctx: &RequestContext,
76    ) -> Result<Value, AwsError> {
77        debug!(operation, "SecretsManager request");
78        let state = self.store.get(&ctx.account_id, &ctx.region);
79
80        match operation {
81            "CreateSecret" => operations::secrets::create_secret(&state, &input, ctx),
82            "GetSecretValue" => operations::secrets::get_secret_value(&state, &input, ctx),
83            "PutSecretValue" => operations::secrets::put_secret_value(&state, &input, ctx),
84            "DescribeSecret" => operations::secrets::describe_secret(&state, &input, ctx),
85            "ListSecrets" => operations::secrets::list_secrets(&state, &input, ctx),
86            "UpdateSecret" => operations::secrets::update_secret(&state, &input, ctx),
87            "DeleteSecret" => operations::secrets::delete_secret(&state, &input, ctx),
88            "RestoreSecret" => operations::secrets::restore_secret(&state, &input, ctx),
89            "TagResource" => operations::secrets::tag_resource(&state, &input, ctx),
90            "UntagResource" => operations::secrets::untag_resource(&state, &input, ctx),
91            "RotateSecret" => operations::secrets::rotate_secret(
92                &state,
93                &input,
94                ctx,
95                self.lambda_invoker.as_deref(),
96            ),
97            "CancelRotateSecret" => operations::secrets::cancel_rotate_secret(&state, &input, ctx),
98            "ValidateResourcePolicy" => {
99                operations::secrets::validate_resource_policy(&state, &input, ctx)
100            }
101            "GetRandomPassword" => operations::secrets::get_random_password(&state, &input, ctx),
102            "ReplicateSecretToRegions" => {
103                operations::secrets::replicate_secret_to_regions(&state, &input, ctx)
104            }
105            "RemoveRegionsFromReplication" => {
106                operations::secrets::remove_regions_from_replication(&state, &input, ctx)
107            }
108            "StopReplicationToReplica" => {
109                operations::secrets::stop_replication_to_replica(&state, &input, ctx)
110            }
111            "ListSecretVersionIds" => {
112                operations::secrets::list_secret_version_ids(&state, &input, ctx)
113            }
114            "BatchGetSecretValue" => {
115                operations::secrets::batch_get_secret_value(&state, &input, ctx)
116            }
117            "UpdateSecretVersionStage" => {
118                operations::secrets::update_secret_version_stage(&state, &input, ctx)
119            }
120            "PutResourcePolicy" => operations::secrets::put_resource_policy(&state, &input, ctx),
121            "GetResourcePolicy" => operations::secrets::get_resource_policy(&state, &input, ctx),
122            "DeleteResourcePolicy" => {
123                operations::secrets::delete_resource_policy(&state, &input, ctx)
124            }
125            _ => Err(AwsError::unknown_operation(operation)),
126        }
127    }
128
129    fn iam_action(&self, operation: &str) -> Option<String> {
130        match operation {
131            "CreateSecret"
132            | "GetSecretValue"
133            | "PutSecretValue"
134            | "DescribeSecret"
135            | "ListSecrets"
136            | "UpdateSecret"
137            | "DeleteSecret"
138            | "RestoreSecret"
139            | "TagResource"
140            | "UntagResource"
141            | "RotateSecret"
142            | "CancelRotateSecret"
143            | "ValidateResourcePolicy"
144            | "GetRandomPassword"
145            | "ReplicateSecretToRegions"
146            | "RemoveRegionsFromReplication"
147            | "StopReplicationToReplica"
148            | "ListSecretVersionIds"
149            | "BatchGetSecretValue"
150            | "UpdateSecretVersionStage"
151            | "PutResourcePolicy"
152            | "GetResourcePolicy"
153            | "DeleteResourcePolicy" => Some(format!("secretsmanager:{operation}")),
154            _ => None,
155        }
156    }
157
158    fn iam_resource(&self, operation: &str, input: &Value, ctx: &RequestContext) -> Option<String> {
159        match operation {
160            "ListSecrets" | "GetRandomPassword" | "BatchGetSecretValue" | "CreateSecret" => {
161                Some("*".to_string())
162            }
163            _ => {
164                let secret_id = input.get("SecretId").and_then(|v| v.as_str())?;
165                if secret_id.starts_with("arn:") {
166                    Some(secret_id.to_string())
167                } else {
168                    Some(format!(
169                        "arn:aws:secretsmanager:{}:{}:secret:{}",
170                        ctx.region, ctx.account_id, secret_id
171                    ))
172                }
173            }
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use awsim_core::{RequestContext, ServiceHandler};
181    use serde_json::json;
182
183    use super::SecretsManagerService;
184
185    fn ctx() -> RequestContext {
186        RequestContext::new("secretsmanager", "us-east-1")
187    }
188
189    fn block_on<F: std::future::Future>(f: F) -> F::Output {
190        use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
191        fn noop_clone(_: *const ()) -> RawWaker {
192            noop_raw_waker()
193        }
194        fn noop(_: *const ()) {}
195        fn noop_raw_waker() -> RawWaker {
196            static VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop);
197            RawWaker::new(std::ptr::null(), &VTABLE)
198        }
199        let waker = unsafe { Waker::from_raw(noop_raw_waker()) };
200        let mut cx = Context::from_waker(&waker);
201        let mut fut = std::pin::pin!(f);
202        loop {
203            match fut.as_mut().poll(&mut cx) {
204                Poll::Ready(v) => return v,
205                Poll::Pending => {}
206            }
207        }
208    }
209
210    #[test]
211    fn test_create_secret_basic() {
212        let svc = SecretsManagerService::new();
213        let ctx = ctx();
214        let result = block_on(svc.handle(
215            "CreateSecret",
216            json!({ "Name": "my-secret", "SecretString": "s3cr3t" }),
217            &ctx,
218        ))
219        .unwrap();
220        assert!(result["ARN"].as_str().unwrap().contains("my-secret"));
221        assert_eq!(result["Name"].as_str().unwrap(), "my-secret");
222        assert!(result["VersionId"].as_str().is_some());
223    }
224
225    #[test]
226    fn test_create_secret_duplicate() {
227        let svc = SecretsManagerService::new();
228        let ctx = ctx();
229        block_on(svc.handle(
230            "CreateSecret",
231            json!({ "Name": "dup", "SecretString": "val" }),
232            &ctx,
233        ))
234        .unwrap();
235        let err = block_on(svc.handle(
236            "CreateSecret",
237            json!({ "Name": "dup", "SecretString": "val2" }),
238            &ctx,
239        ))
240        .unwrap_err();
241        assert_eq!(err.code, "ResourceExistsException");
242    }
243
244    #[test]
245    fn test_create_secret_rejects_reserved_aws_prefix() {
246        let svc = SecretsManagerService::new();
247        let ctx = ctx();
248        let err = block_on(svc.handle(
249            "CreateSecret",
250            json!({ "Name": "aws/managed", "SecretString": "hi" }),
251            &ctx,
252        ))
253        .unwrap_err();
254        assert_eq!(err.code, "InvalidRequestException");
255    }
256
257    #[test]
258    fn test_create_secret_rejects_invalid_chars() {
259        let svc = SecretsManagerService::new();
260        let ctx = ctx();
261        let err = block_on(svc.handle(
262            "CreateSecret",
263            json!({ "Name": "bad name with spaces", "SecretString": "hi" }),
264            &ctx,
265        ))
266        .unwrap_err();
267        assert_eq!(err.code, "InvalidParameterException");
268    }
269
270    #[test]
271    fn test_create_secret_no_value() {
272        let svc = SecretsManagerService::new();
273        let ctx = ctx();
274        let err =
275            block_on(svc.handle("CreateSecret", json!({ "Name": "empty" }), &ctx)).unwrap_err();
276        assert_eq!(err.code, "InvalidParameterException");
277    }
278
279    #[test]
280    fn test_get_secret_value() {
281        let svc = SecretsManagerService::new();
282        let ctx = ctx();
283        block_on(svc.handle(
284            "CreateSecret",
285            json!({ "Name": "my-secret", "SecretString": "hello" }),
286            &ctx,
287        ))
288        .unwrap();
289        let result =
290            block_on(svc.handle("GetSecretValue", json!({ "SecretId": "my-secret" }), &ctx))
291                .unwrap();
292        assert_eq!(result["SecretString"].as_str().unwrap(), "hello");
293        assert_eq!(result["Name"].as_str().unwrap(), "my-secret");
294    }
295
296    #[test]
297    fn test_get_secret_by_arn() {
298        let svc = SecretsManagerService::new();
299        let ctx = ctx();
300        let created = block_on(svc.handle(
301            "CreateSecret",
302            json!({ "Name": "arn-secret", "SecretString": "data" }),
303            &ctx,
304        ))
305        .unwrap();
306        let arn = created["ARN"].as_str().unwrap();
307        let result =
308            block_on(svc.handle("GetSecretValue", json!({ "SecretId": arn }), &ctx)).unwrap();
309        assert_eq!(result["SecretString"].as_str().unwrap(), "data");
310    }
311
312    #[test]
313    fn test_get_secret_not_found() {
314        let svc = SecretsManagerService::new();
315        let ctx = ctx();
316        let err = block_on(svc.handle("GetSecretValue", json!({ "SecretId": "ghost" }), &ctx))
317            .unwrap_err();
318        assert_eq!(err.code, "ResourceNotFoundException");
319    }
320
321    #[test]
322    fn test_put_secret_value_rotates_stages() {
323        let svc = SecretsManagerService::new();
324        let ctx = ctx();
325        block_on(svc.handle(
326            "CreateSecret",
327            json!({ "Name": "rotate-secret", "SecretString": "v1" }),
328            &ctx,
329        ))
330        .unwrap();
331
332        block_on(svc.handle(
333            "PutSecretValue",
334            json!({ "SecretId": "rotate-secret", "SecretString": "v2" }),
335            &ctx,
336        ))
337        .unwrap();
338
339        // AWSCURRENT should return v2
340        let current = block_on(svc.handle(
341            "GetSecretValue",
342            json!({ "SecretId": "rotate-secret", "VersionStage": "AWSCURRENT" }),
343            &ctx,
344        ))
345        .unwrap();
346        assert_eq!(current["SecretString"].as_str().unwrap(), "v2");
347
348        // AWSPREVIOUS should return v1
349        let prev = block_on(svc.handle(
350            "GetSecretValue",
351            json!({ "SecretId": "rotate-secret", "VersionStage": "AWSPREVIOUS" }),
352            &ctx,
353        ))
354        .unwrap();
355        assert_eq!(prev["SecretString"].as_str().unwrap(), "v1");
356    }
357
358    #[test]
359    fn test_describe_secret() {
360        let svc = SecretsManagerService::new();
361        let ctx = ctx();
362        block_on(svc.handle(
363            "CreateSecret",
364            json!({ "Name": "desc-secret", "SecretString": "x", "Description": "my desc" }),
365            &ctx,
366        ))
367        .unwrap();
368        let result =
369            block_on(svc.handle("DescribeSecret", json!({ "SecretId": "desc-secret" }), &ctx))
370                .unwrap();
371        assert_eq!(result["Name"].as_str().unwrap(), "desc-secret");
372        assert_eq!(result["Description"].as_str().unwrap(), "my desc");
373        // Value must not be present in metadata
374        assert!(result["SecretString"].is_null());
375    }
376
377    #[test]
378    fn test_list_secrets() {
379        let svc = SecretsManagerService::new();
380        let ctx = ctx();
381        block_on(svc.handle(
382            "CreateSecret",
383            json!({ "Name": "s1", "SecretString": "a" }),
384            &ctx,
385        ))
386        .unwrap();
387        block_on(svc.handle(
388            "CreateSecret",
389            json!({ "Name": "s2", "SecretString": "b" }),
390            &ctx,
391        ))
392        .unwrap();
393        let result = block_on(svc.handle("ListSecrets", json!({}), &ctx)).unwrap();
394        assert_eq!(result["SecretList"].as_array().unwrap().len(), 2);
395    }
396
397    #[test]
398    fn test_update_secret_description() {
399        let svc = SecretsManagerService::new();
400        let ctx = ctx();
401        block_on(svc.handle(
402            "CreateSecret",
403            json!({ "Name": "upd-secret", "SecretString": "val", "Description": "old" }),
404            &ctx,
405        ))
406        .unwrap();
407        block_on(svc.handle(
408            "UpdateSecret",
409            json!({ "SecretId": "upd-secret", "Description": "new" }),
410            &ctx,
411        ))
412        .unwrap();
413        let desc =
414            block_on(svc.handle("DescribeSecret", json!({ "SecretId": "upd-secret" }), &ctx))
415                .unwrap();
416        assert_eq!(desc["Description"].as_str().unwrap(), "new");
417    }
418
419    #[test]
420    fn test_delete_and_restore_secret() {
421        let svc = SecretsManagerService::new();
422        let ctx = ctx();
423        block_on(svc.handle(
424            "CreateSecret",
425            json!({ "Name": "del-secret", "SecretString": "x" }),
426            &ctx,
427        ))
428        .unwrap();
429
430        block_on(svc.handle(
431            "DeleteSecret",
432            json!({ "SecretId": "del-secret", "RecoveryWindowInDays": 7 }),
433            &ctx,
434        ))
435        .unwrap();
436
437        // GetSecretValue on a deleted secret should fail
438        let err = block_on(svc.handle("GetSecretValue", json!({ "SecretId": "del-secret" }), &ctx))
439            .unwrap_err();
440        assert_eq!(err.code, "InvalidRequestException");
441
442        // Restore it
443        block_on(svc.handle("RestoreSecret", json!({ "SecretId": "del-secret" }), &ctx)).unwrap();
444
445        // Should be accessible again
446        let val = block_on(svc.handle("GetSecretValue", json!({ "SecretId": "del-secret" }), &ctx))
447            .unwrap();
448        assert_eq!(val["SecretString"].as_str().unwrap(), "x");
449    }
450
451    #[test]
452    fn test_force_delete_secret() {
453        let svc = SecretsManagerService::new();
454        let ctx = ctx();
455        block_on(svc.handle(
456            "CreateSecret",
457            json!({ "Name": "force-del", "SecretString": "gone" }),
458            &ctx,
459        ))
460        .unwrap();
461
462        block_on(svc.handle(
463            "DeleteSecret",
464            json!({ "SecretId": "force-del", "ForceDeleteWithoutRecovery": true }),
465            &ctx,
466        ))
467        .unwrap();
468
469        // Immediately gone
470        let err = block_on(svc.handle("GetSecretValue", json!({ "SecretId": "force-del" }), &ctx))
471            .unwrap_err();
472        assert_eq!(err.code, "ResourceNotFoundException");
473    }
474
475    #[test]
476    fn test_tag_and_untag_resource() {
477        let svc = SecretsManagerService::new();
478        let ctx = ctx();
479        block_on(svc.handle(
480            "CreateSecret",
481            json!({ "Name": "tagged", "SecretString": "v" }),
482            &ctx,
483        ))
484        .unwrap();
485
486        block_on(svc.handle(
487            "TagResource",
488            json!({
489                "SecretId": "tagged",
490                "Tags": [{ "Key": "env", "Value": "prod" }, { "Key": "team", "Value": "ops" }]
491            }),
492            &ctx,
493        ))
494        .unwrap();
495
496        let desc =
497            block_on(svc.handle("DescribeSecret", json!({ "SecretId": "tagged" }), &ctx)).unwrap();
498        assert_eq!(desc["Tags"].as_array().unwrap().len(), 2);
499
500        block_on(svc.handle(
501            "UntagResource",
502            json!({ "SecretId": "tagged", "TagKeys": ["env"] }),
503            &ctx,
504        ))
505        .unwrap();
506
507        let desc2 =
508            block_on(svc.handle("DescribeSecret", json!({ "SecretId": "tagged" }), &ctx)).unwrap();
509        assert_eq!(desc2["Tags"].as_array().unwrap().len(), 1);
510    }
511
512    #[test]
513    fn test_unknown_operation() {
514        let svc = SecretsManagerService::new();
515        let ctx = ctx();
516        let err = block_on(svc.handle("FooBar", json!({}), &ctx)).unwrap_err();
517        assert_eq!(err.code, "UnknownOperationException");
518    }
519
520    #[test]
521    fn test_list_secret_version_ids() {
522        let svc = SecretsManagerService::new();
523        let ctx = ctx();
524        block_on(svc.handle(
525            "CreateSecret",
526            json!({ "Name": "versioned", "SecretString": "v1" }),
527            &ctx,
528        ))
529        .unwrap();
530        block_on(svc.handle(
531            "PutSecretValue",
532            json!({ "SecretId": "versioned", "SecretString": "v2" }),
533            &ctx,
534        ))
535        .unwrap();
536
537        let result = block_on(svc.handle(
538            "ListSecretVersionIds",
539            json!({ "SecretId": "versioned" }),
540            &ctx,
541        ))
542        .unwrap();
543        let versions = result["Versions"].as_array().unwrap();
544        assert_eq!(versions.len(), 2);
545        assert_eq!(result["Name"].as_str().unwrap(), "versioned");
546    }
547
548    #[test]
549    fn test_batch_get_secret_value() {
550        let svc = SecretsManagerService::new();
551        let ctx = ctx();
552        block_on(svc.handle(
553            "CreateSecret",
554            json!({ "Name": "s1", "SecretString": "val1" }),
555            &ctx,
556        ))
557        .unwrap();
558        block_on(svc.handle(
559            "CreateSecret",
560            json!({ "Name": "s2", "SecretString": "val2" }),
561            &ctx,
562        ))
563        .unwrap();
564
565        let result = block_on(svc.handle(
566            "BatchGetSecretValue",
567            json!({ "SecretIdList": ["s1", "s2", "nonexistent"] }),
568            &ctx,
569        ))
570        .unwrap();
571
572        let values = result["SecretValues"].as_array().unwrap();
573        let errors = result["Errors"].as_array().unwrap();
574        assert_eq!(values.len(), 2);
575        assert_eq!(errors.len(), 1);
576        assert_eq!(
577            errors[0]["ErrorCode"].as_str().unwrap(),
578            "ResourceNotFoundException"
579        );
580    }
581}