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
19pub 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 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 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 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 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 let err = block_on(svc.handle("GetSecretValue", json!({ "SecretId": "del-secret" }), &ctx))
439 .unwrap_err();
440 assert_eq!(err.code, "InvalidRequestException");
441
442 block_on(svc.handle("RestoreSecret", json!({ "SecretId": "del-secret" }), &ctx)).unwrap();
444
445 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 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}