Skip to main content

fakecloud_secretsmanager/
service.rs

1use std::collections::{BTreeMap, HashMap};
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use chrono::Utc;
6use http::StatusCode;
7use serde_json::{json, Value};
8
9use tokio::sync::Mutex as AsyncMutex;
10
11use fakecloud_aws::arn::Arn;
12use fakecloud_core::delivery::DeliveryBus;
13use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
14use fakecloud_core::validation::*;
15use fakecloud_persistence::SnapshotStore;
16
17use crate::state::{
18    RotationRules, Secret, SecretVersion, SecretsManagerSnapshot, SecretsManagerState,
19    SharedSecretsManagerState, SECRETSMANAGER_SNAPSHOT_SCHEMA_VERSION,
20};
21
22/// Information needed to invoke the rotation Lambda after releasing state lock.
23struct RotationInvocation {
24    lambda_arn: String,
25    secret_id: String,
26    client_request_token: String,
27}
28
29/// Result of an idempotency check against an existing
30/// `ClientRequestToken` / version id.
31pub(crate) enum VersionIdempotency {
32    /// The version id isn't in the secret yet — this is a fresh write.
33    NotFound,
34    /// The version id exists and stores the exact same payload we're
35    /// about to write — callers should return the existing version as
36    /// a successful no-op response.
37    Match,
38    /// The version id exists but stores a different payload — AWS
39    /// surfaces this as a `ResourceExistsException`.
40    Conflict,
41}
42
43pub struct SecretsManagerService {
44    state: SharedSecretsManagerState,
45    delivery_bus: Option<Arc<DeliveryBus>>,
46    snapshot_store: Option<Arc<dyn SnapshotStore>>,
47    snapshot_lock: Arc<AsyncMutex<()>>,
48    kms_hook: Option<Arc<dyn fakecloud_core::delivery::KmsHook>>,
49}
50
51impl SecretsManagerService {
52    pub fn new(state: SharedSecretsManagerState) -> Self {
53        Self {
54            state,
55            delivery_bus: None,
56            snapshot_store: None,
57            snapshot_lock: Arc::new(AsyncMutex::new(())),
58            kms_hook: None,
59        }
60    }
61
62    pub fn with_delivery(mut self, delivery_bus: Arc<DeliveryBus>) -> Self {
63        self.delivery_bus = Some(delivery_bus);
64        self
65    }
66
67    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
68        self.snapshot_store = Some(store);
69        self
70    }
71
72    pub fn with_kms_hook(mut self, hook: Arc<dyn fakecloud_core::delivery::KmsHook>) -> Self {
73        self.kms_hook = Some(hook);
74        self
75    }
76
77    fn maybe_encrypt_secret_string(
78        &self,
79        account_id: &str,
80        region: &str,
81        secret_arn: &str,
82        kms_key_id: Option<&str>,
83        plaintext: Option<String>,
84    ) -> Option<String> {
85        let pt = plaintext?;
86        let (Some(hook), Some(key)) = (&self.kms_hook, kms_key_id) else {
87            return Some(pt);
88        };
89        let key = if key.is_empty() {
90            "aws/secretsmanager"
91        } else {
92            key
93        };
94        let mut ctx = HashMap::new();
95        ctx.insert(
96            "aws:secretsmanager:secretArn".to_string(),
97            secret_arn.to_string(),
98        );
99        match hook.encrypt(
100            account_id,
101            region,
102            key,
103            pt.as_bytes(),
104            "secretsmanager.amazonaws.com",
105            ctx,
106        ) {
107            Ok(ciphertext) => Some(ciphertext),
108            Err(err) => {
109                tracing::warn!(
110                    secret_arn = %secret_arn,
111                    error = %err,
112                    "KMS encrypt failed for secret; storing plaintext"
113                );
114                Some(pt)
115            }
116        }
117    }
118
119    fn maybe_decrypt_secret_string(
120        &self,
121        account_id: &str,
122        secret_arn: &str,
123        kms_key_id: Option<&str>,
124        stored: Option<&str>,
125    ) -> Option<String> {
126        let stored = stored?;
127        let (Some(hook), Some(_)) = (&self.kms_hook, kms_key_id) else {
128            return Some(stored.to_string());
129        };
130        let mut ctx = HashMap::new();
131        ctx.insert(
132            "aws:secretsmanager:secretArn".to_string(),
133            secret_arn.to_string(),
134        );
135        match hook.decrypt(account_id, stored, "secretsmanager.amazonaws.com", ctx) {
136            Ok(bytes) => Some(String::from_utf8_lossy(&bytes).to_string()),
137            Err(_) => Some(stored.to_string()),
138        }
139    }
140
141    /// Persist current state as a snapshot. Held across the
142    /// clone-serialize-write sequence to prevent stale-last writes,
143    /// with serde + file I/O offloaded to the blocking pool.
144    async fn save_snapshot(&self) {
145        save_secretsmanager_snapshot(
146            &self.state,
147            self.snapshot_store.clone(),
148            &self.snapshot_lock,
149        )
150        .await;
151    }
152
153    /// Build a hook that persists the current Secrets Manager state when
154    /// invoked, or `None` in memory mode (no snapshot store). The
155    /// CloudFormation provisioner mutates `state` directly and uses this to
156    /// write a CFN-provisioned secret through to disk, the same way a direct
157    /// mutating API call would.
158    pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
159        let store = self.snapshot_store.clone()?;
160        let state = self.state.clone();
161        let lock = self.snapshot_lock.clone();
162        Some(Arc::new(move || {
163            let state = state.clone();
164            let store = store.clone();
165            let lock = lock.clone();
166            Box::pin(async move {
167                save_secretsmanager_snapshot(&state, Some(store), &lock).await;
168            })
169        }))
170    }
171
172    fn create_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
173        let input = CreateSecretInput::from_body(&req.json_body())?;
174        let has_value = input.secret_string.is_some() || input.secret_binary.is_some();
175
176        let mut accounts = self.state.write();
177        let state = accounts.get_or_create(&req.account_id);
178
179        if let Some(existing) = state.secrets.get(&input.name) {
180            if let Some(ref token) = input.client_request_token {
181                let existing_plaintext = existing.versions.get(token).and_then(|v| {
182                    self.maybe_decrypt_secret_string(
183                        &req.account_id,
184                        &existing.arn,
185                        existing.kms_key_id.as_deref(),
186                        v.secret_string.as_deref(),
187                    )
188                });
189                match check_secret_version_idempotency(
190                    &existing.versions,
191                    token,
192                    existing_plaintext,
193                    &input.secret_string,
194                    &input.secret_binary,
195                ) {
196                    VersionIdempotency::Match => {
197                        let mut response = json!({
198                            "ARN": existing.arn,
199                            "Name": existing.name,
200                            "VersionId": token,
201                        });
202                        if !has_value {
203                            response.as_object_mut().unwrap().remove("VersionId");
204                        }
205                        return Ok(AwsResponse::ok_json(response));
206                    }
207                    VersionIdempotency::Conflict => {
208                        return Err(AwsServiceError::aws_error(
209                            StatusCode::BAD_REQUEST,
210                            "ResourceExistsException",
211                            format!(
212                                "You can't use ClientRequestToken {token} because that value is already in use for a version of secret {}.",
213                                existing.arn
214                            ),
215                        ));
216                    }
217                    VersionIdempotency::NotFound => {}
218                }
219            }
220            return Err(AwsServiceError::aws_error(
221                StatusCode::BAD_REQUEST,
222                "ResourceExistsException",
223                format!(
224                    "The operation failed because the secret {} already exists.",
225                    input.name
226                ),
227            ));
228        }
229
230        let arn = format!(
231            "arn:aws:secretsmanager:{}:{}:secret:{}-{}",
232            req.region,
233            req.account_id,
234            input.name,
235            &uuid::Uuid::new_v4().to_string()[..6]
236        );
237
238        let now = Utc::now();
239
240        let (versions, current_version_id, version_id_for_response) = if has_value {
241            let vid = input
242                .client_request_token
243                .clone()
244                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
245            let stored_string = self.maybe_encrypt_secret_string(
246                &req.account_id,
247                &req.region,
248                &arn,
249                input.kms_key_id.as_deref(),
250                input.secret_string,
251            );
252            let version = SecretVersion {
253                version_id: vid.clone(),
254                secret_string: stored_string,
255                secret_binary: input.secret_binary,
256                stages: vec!["AWSCURRENT".to_string()],
257                created_at: now,
258            };
259            let mut versions = std::collections::BTreeMap::new();
260            versions.insert(vid.clone(), version);
261            (versions, Some(vid.clone()), Some(vid))
262        } else {
263            (std::collections::BTreeMap::new(), None, None)
264        };
265
266        let tags_ever_set = !input.tags.is_empty();
267        let secret = Secret {
268            name: input.name.clone(),
269            arn: arn.clone(),
270            description: input.description,
271            kms_key_id: input.kms_key_id,
272            versions,
273            current_version_id,
274            tags: input.tags,
275            tags_ever_set,
276            deleted: false,
277            deletion_date: None,
278            created_at: now,
279            last_changed_at: now,
280            last_accessed_at: None,
281            rotation_enabled: None,
282            rotation_lambda_arn: None,
283            rotation_rules: None,
284            last_rotated_at: None,
285            resource_policy: None,
286        };
287
288        state.secrets.insert(input.name.clone(), secret);
289
290        let mut response = json!({
291            "ARN": arn,
292            "Name": input.name,
293        });
294        if let Some(vid) = version_id_for_response {
295            response["VersionId"] = json!(vid);
296        }
297
298        Ok(AwsResponse::ok_json(response))
299    }
300
301    fn get_secret_value(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
302        let body = req.json_body();
303        let secret_id = require_secret_id(&body)?;
304        validate_optional_string_length("versionId", body["VersionId"].as_str(), 32, 64)?;
305        validate_optional_string_length("versionStage", body["VersionStage"].as_str(), 1, 256)?;
306
307        // Resolve owning account from an ARN form. Cross-account
308        // GetSecretValue then evaluates `secret.resource_policy` via
309        // the IAM evaluator before returning the value.
310        let owner_account = secret_owner_account(&secret_id, &req.account_id);
311        let mut accounts = self.state.write();
312        let state = accounts.get_or_create(&owner_account);
313        let secret = self.find_secret_mut(state, &secret_id)?;
314        if owner_account != req.account_id {
315            let policy_doc = secret.resource_policy.as_deref().unwrap_or("");
316            let secret_arn = secret.arn.clone();
317            if !resource_policy_allows(policy_doc, &req.account_id, &secret_arn) {
318                return Err(AwsServiceError::aws_error(
319                    StatusCode::FORBIDDEN,
320                    "AccessDeniedException",
321                    "User is not authorized to perform: secretsmanager:GetSecretValue on the requested resource",
322                ));
323            }
324        }
325
326        if secret.deleted {
327            return Err(AwsServiceError::aws_error(
328                StatusCode::BAD_REQUEST,
329                "InvalidRequestException",
330                "You can't perform this operation on the secret because it was marked for deletion.",
331            ));
332        }
333
334        let requested_stage = body["VersionStage"].as_str().unwrap_or("AWSCURRENT");
335
336        // Determine which version to return
337        let version_id = body["VersionId"]
338            .as_str()
339            .map(|s| s.to_string())
340            .or_else(|| {
341                secret
342                    .versions
343                    .iter()
344                    .find(|(_, v)| v.stages.contains(&requested_stage.to_string()))
345                    .map(|(id, _)| id.clone())
346            });
347
348        let version_id = match version_id {
349            Some(vid) => vid,
350            None => {
351                // No versions exist
352                return Err(AwsServiceError::aws_error(
353                    StatusCode::NOT_FOUND,
354                    "ResourceNotFoundException",
355                    format!(
356                        "Secrets Manager can't find the specified secret value for staging label: {requested_stage}"
357                    ),
358                ));
359            }
360        };
361
362        let version = secret.versions.get(&version_id).ok_or_else(|| {
363            AwsServiceError::aws_error(
364                StatusCode::NOT_FOUND,
365                "ResourceNotFoundException",
366                format!(
367                    "Secrets Manager can't find the specified secret value for VersionId: {version_id}"
368                ),
369            )
370        })?;
371
372        // If VersionStage is specified with VersionId, verify they match
373        if body["VersionId"].as_str().is_some() {
374            if let Some(stage) = body["VersionStage"].as_str() {
375                if !version.stages.contains(&stage.to_string()) {
376                    return Err(AwsServiceError::aws_error(
377                        StatusCode::NOT_FOUND,
378                        "ResourceNotFoundException",
379                        "You provided a VersionStage that is not associated to the provided VersionId.",
380                    ));
381                }
382            }
383        }
384
385        // Only set last_accessed_at on successful retrieval
386        secret.last_accessed_at = Some(Utc::now());
387
388        let mut response = json!({
389            "ARN": secret.arn,
390            "Name": secret.name,
391            "VersionId": version.version_id,
392            "VersionStages": version.stages,
393            "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
394        });
395
396        let kms_for_decrypt = secret.kms_key_id.clone();
397        let arn_for_decrypt = secret.arn.clone();
398        if let Some(ref s) = version.secret_string {
399            let plaintext = self
400                .maybe_decrypt_secret_string(
401                    &req.account_id,
402                    &arn_for_decrypt,
403                    kms_for_decrypt.as_deref(),
404                    Some(s.as_str()),
405                )
406                .unwrap_or_else(|| s.clone());
407            response["SecretString"] = json!(plaintext);
408        }
409        if let Some(ref b) = version.secret_binary {
410            response["SecretBinary"] = json!(base64_encode(b));
411        }
412
413        Ok(AwsResponse::ok_json(response))
414    }
415
416    fn put_secret_value(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
417        let body = req.json_body();
418        let secret_id = require_secret_id(&body)?;
419        validate_optional_string_length(
420            "clientRequestToken",
421            body["ClientRequestToken"].as_str(),
422            32,
423            64,
424        )?;
425        validate_optional_string_length("secretString", body["SecretString"].as_str(), 1, 65536)?;
426
427        let secret_string = body["SecretString"].as_str().map(|s| s.to_string());
428        let secret_binary = body["SecretBinary"].as_str().and_then(base64_decode);
429
430        // Validate that either SecretString or SecretBinary is provided
431        if secret_string.is_none() && secret_binary.is_none() {
432            return Err(AwsServiceError::aws_error(
433                StatusCode::BAD_REQUEST,
434                "InvalidRequestException",
435                "You must provide either SecretString or SecretBinary.",
436            ));
437        }
438
439        let mut accounts = self.state.write();
440        let state = accounts.get_or_create(&req.account_id);
441        let secret = match self.find_secret_mut(state, &secret_id) {
442            Ok(s) => s,
443            Err(_) => {
444                return Err(AwsServiceError::aws_error(
445                    StatusCode::NOT_FOUND,
446                    "ResourceNotFoundException",
447                    "Secrets Manager can't find the specified secret.",
448                ));
449            }
450        };
451
452        if secret.deleted {
453            return Err(AwsServiceError::aws_error(
454                StatusCode::BAD_REQUEST,
455                "InvalidRequestException",
456                "You can't perform this operation on the secret because it was marked for deletion.",
457            ));
458        }
459
460        let now = Utc::now();
461        let version_id = body["ClientRequestToken"]
462            .as_str()
463            .map(|s| s.to_string())
464            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
465
466        let existing_plaintext = secret.versions.get(&version_id).and_then(|v| {
467            self.maybe_decrypt_secret_string(
468                &req.account_id,
469                &secret.arn,
470                secret.kms_key_id.as_deref(),
471                v.secret_string.as_deref(),
472            )
473        });
474        match check_secret_version_idempotency(
475            &secret.versions,
476            &version_id,
477            existing_plaintext,
478            &secret_string,
479            &secret_binary,
480        ) {
481            VersionIdempotency::Match => {
482                let existing_stages = secret.versions[&version_id].stages.clone();
483                return Ok(AwsResponse::ok_json(json!({
484                    "ARN": secret.arn,
485                    "Name": secret.name,
486                    "VersionId": version_id,
487                    "VersionStages": existing_stages,
488                })));
489            }
490            VersionIdempotency::Conflict => {
491                return Err(AwsServiceError::aws_error(
492                    StatusCode::BAD_REQUEST,
493                    "ResourceExistsException",
494                    format!(
495                        "You can't use ClientRequestToken {version_id} because that value is already in use for a version of secret {}.",
496                        secret.arn
497                    ),
498                ));
499            }
500            VersionIdempotency::NotFound => {}
501        }
502
503        let mut version_stages: Vec<String> = body["VersionStages"]
504            .as_array()
505            .map(|arr| {
506                arr.iter()
507                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
508                    .collect()
509            })
510            .unwrap_or_else(|| vec!["AWSCURRENT".to_string()]);
511
512        // If this is the first version with a value, add AWSCURRENT to stages
513        let has_current = secret
514            .versions
515            .values()
516            .any(|v| v.stages.contains(&"AWSCURRENT".to_string()));
517        if !has_current && !version_stages.contains(&"AWSCURRENT".to_string()) {
518            version_stages.push("AWSCURRENT".to_string());
519        }
520
521        // Move AWSCURRENT from old version to AWSPREVIOUS if new version has AWSCURRENT
522        if version_stages.contains(&"AWSCURRENT".to_string()) {
523            if let Some(ref old_vid) = secret.current_version_id.clone() {
524                if let Some(old_version) = secret.versions.get_mut(old_vid) {
525                    old_version.stages.retain(|s| s != "AWSCURRENT");
526                    if !old_version.stages.contains(&"AWSPREVIOUS".to_string()) {
527                        old_version.stages.push("AWSPREVIOUS".to_string());
528                    }
529                }
530                // Remove AWSPREVIOUS from any other version
531                for (id, v) in secret.versions.iter_mut() {
532                    if id != old_vid {
533                        v.stages.retain(|s| s != "AWSPREVIOUS");
534                    }
535                }
536            }
537            secret.current_version_id = Some(version_id.clone());
538        }
539
540        // Remove custom stages from other versions that have them
541        for stage in &version_stages {
542            if stage == "AWSCURRENT" || stage == "AWSPREVIOUS" {
543                continue;
544            }
545            for v in secret.versions.values_mut() {
546                v.stages.retain(|s| s != stage);
547            }
548        }
549
550        // Remove versions with no stages
551        secret.versions.retain(|_, v| !v.stages.is_empty());
552
553        let kms_key_for_enc = secret.kms_key_id.clone();
554        let arn_for_enc = secret.arn.clone();
555        let stored_secret_string = self.maybe_encrypt_secret_string(
556            &req.account_id,
557            &req.region,
558            &arn_for_enc,
559            kms_key_for_enc.as_deref(),
560            secret_string,
561        );
562        let version = SecretVersion {
563            version_id: version_id.clone(),
564            secret_string: stored_secret_string,
565            secret_binary,
566            stages: version_stages.clone(),
567            created_at: now,
568        };
569
570        secret.versions.insert(version_id.clone(), version);
571        secret.last_changed_at = now;
572
573        let response = json!({
574            "ARN": secret.arn,
575            "Name": secret.name,
576            "VersionId": version_id,
577            "VersionStages": version_stages,
578        });
579
580        Ok(AwsResponse::ok_json(response))
581    }
582
583    fn update_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
584        let body = req.json_body();
585        let secret_id = require_secret_id(&body)?;
586        validate_optional_string_length(
587            "clientRequestToken",
588            body["ClientRequestToken"].as_str(),
589            32,
590            64,
591        )?;
592        validate_optional_string_length("description", body["Description"].as_str(), 0, 2048)?;
593        validate_optional_string_length("kmsKeyId", body["KmsKeyId"].as_str(), 0, 2048)?;
594        validate_optional_string_length("secretString", body["SecretString"].as_str(), 1, 65536)?;
595
596        let mut accounts = self.state.write();
597        let state = accounts.get_or_create(&req.account_id);
598        let secret = match self.find_secret_mut(state, &secret_id) {
599            Ok(s) => s,
600            Err(_) => {
601                return Err(AwsServiceError::aws_error(
602                    StatusCode::NOT_FOUND,
603                    "ResourceNotFoundException",
604                    "Secrets Manager can't find the specified secret.",
605                ));
606            }
607        };
608
609        if secret.deleted {
610            return Err(AwsServiceError::aws_error(
611                StatusCode::BAD_REQUEST,
612                "InvalidRequestException",
613                "You can't perform this operation on the secret because it was marked for deletion.",
614            ));
615        }
616
617        if let Some(desc) = body["Description"].as_str() {
618            secret.description = Some(desc.to_string());
619        }
620        if let Some(kms) = body["KmsKeyId"].as_str() {
621            secret.kms_key_id = Some(kms.to_string());
622        }
623
624        // If SecretString or SecretBinary is provided, create a new version
625        let secret_string = body["SecretString"].as_str().map(|s| s.to_string());
626        let secret_binary = body["SecretBinary"].as_str().and_then(base64_decode);
627
628        let version_id = if secret_string.is_some() || secret_binary.is_some() {
629            let vid = body["ClientRequestToken"]
630                .as_str()
631                .map(|s| s.to_string())
632                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
633
634            let existing_plaintext = secret.versions.get(&vid).and_then(|v| {
635                self.maybe_decrypt_secret_string(
636                    &req.account_id,
637                    &secret.arn,
638                    secret.kms_key_id.as_deref(),
639                    v.secret_string.as_deref(),
640                )
641            });
642            match check_secret_version_idempotency(
643                &secret.versions,
644                &vid,
645                existing_plaintext,
646                &secret_string,
647                &secret_binary,
648            ) {
649                VersionIdempotency::Match => {
650                    return Ok(AwsResponse::ok_json(json!({
651                        "ARN": secret.arn,
652                        "Name": secret.name,
653                        "VersionId": vid,
654                    })));
655                }
656                VersionIdempotency::Conflict => {
657                    return Err(AwsServiceError::aws_error(
658                        StatusCode::BAD_REQUEST,
659                        "ResourceExistsException",
660                        format!(
661                            "You can't use ClientRequestToken {vid} because that value is already in use for a version of secret {}.",
662                            secret.arn
663                        ),
664                    ));
665                }
666                VersionIdempotency::NotFound => {}
667            }
668
669            let now = Utc::now();
670
671            // Move AWSCURRENT -> AWSPREVIOUS on old version
672            if let Some(ref old_vid) = secret.current_version_id.clone() {
673                if let Some(old_v) = secret.versions.get_mut(old_vid) {
674                    old_v.stages.retain(|s| s != "AWSCURRENT");
675                    if !old_v.stages.contains(&"AWSPREVIOUS".to_string()) {
676                        old_v.stages.push("AWSPREVIOUS".to_string());
677                    }
678                }
679            }
680
681            let version = SecretVersion {
682                version_id: vid.clone(),
683                secret_string,
684                secret_binary,
685                stages: vec!["AWSCURRENT".to_string()],
686                created_at: now,
687            };
688            secret.versions.insert(vid.clone(), version);
689            secret.current_version_id = Some(vid.clone());
690            secret.last_changed_at = now;
691            Some(vid)
692        } else {
693            secret.last_changed_at = Utc::now();
694            None
695        };
696
697        let mut response = json!({
698            "ARN": secret.arn,
699            "Name": secret.name,
700        });
701        if let Some(vid) = version_id {
702            response["VersionId"] = json!(vid);
703        }
704
705        Ok(AwsResponse::ok_json(response))
706    }
707
708    fn delete_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
709        let body = req.json_body();
710        let secret_id = require_secret_id(&body)?;
711
712        let force_delete = body["ForceDeleteWithoutRecovery"]
713            .as_bool()
714            .unwrap_or(false);
715        let recovery_window = body.get("RecoveryWindowInDays").and_then(|v| v.as_i64());
716
717        // Validate recovery window range first (AWS validates this before the conflict check)
718        if let Some(days) = recovery_window {
719            if !(7..=30).contains(&days) {
720                return Err(AwsServiceError::aws_error(
721                    StatusCode::BAD_REQUEST,
722                    "InvalidParameterException",
723                    "An error occurred (InvalidParameterException) when calling the DeleteSecret operation: RecoveryWindowInDays value must be between 7 and 30 days (inclusive).",
724                ));
725            }
726        }
727
728        // Validate: can't use both force delete and recovery window
729        if force_delete && recovery_window.is_some() {
730            return Err(AwsServiceError::aws_error(
731                StatusCode::BAD_REQUEST,
732                "InvalidParameterException",
733                "An error occurred (InvalidParameterException) when calling the DeleteSecret operation: You can't use ForceDeleteWithoutRecovery in conjunction with RecoveryWindowInDays.",
734            ));
735        }
736
737        let mut accounts = self.state.write();
738        let state = accounts.get_or_create(&req.account_id);
739
740        if force_delete {
741            // Force delete: if secret doesn't exist, create a fake response
742            match self.find_secret_mut(state, &secret_id) {
743                Ok(secret) => {
744                    let arn = secret.arn.clone();
745                    let name = secret.name.clone();
746                    let deletion_date = Utc::now();
747                    state.secrets.remove(&name);
748                    let response = json!({
749                        "ARN": arn,
750                        "Name": name,
751                        "DeletionDate": deletion_date.timestamp_millis() as f64 / 1000.0,
752                    });
753                    return Ok(AwsResponse::ok_json(response));
754                }
755                Err(_) => {
756                    // For force delete of non-existent secret, AWS returns success
757                    let arn = format!(
758                        "arn:aws:secretsmanager:{}:{}:secret:{}-{}",
759                        req.region,
760                        req.account_id,
761                        secret_id,
762                        &uuid::Uuid::new_v4().to_string()[..6]
763                    );
764                    let deletion_date = Utc::now();
765                    let response = json!({
766                        "ARN": arn,
767                        "Name": secret_id,
768                        "DeletionDate": deletion_date.timestamp_millis() as f64 / 1000.0,
769                    });
770                    return Ok(AwsResponse::ok_json(response));
771                }
772            }
773        }
774
775        let secret = self.find_secret_mut(state, &secret_id)?;
776
777        if secret.deleted {
778            return Err(AwsServiceError::aws_error(
779                StatusCode::BAD_REQUEST,
780                "InvalidRequestException",
781                "You can't perform this operation on the secret because it was already scheduled for deletion.",
782            ));
783        }
784
785        let now = Utc::now();
786        let days = recovery_window.unwrap_or(30);
787        let deletion_date = now + chrono::Duration::days(days);
788        secret.deleted = true;
789        secret.deletion_date = Some(deletion_date);
790
791        let response = json!({
792            "ARN": secret.arn,
793            "Name": secret.name,
794            "DeletionDate": deletion_date.timestamp_millis() as f64 / 1000.0,
795        });
796
797        Ok(AwsResponse::ok_json(response))
798    }
799
800    fn restore_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
801        let body = req.json_body();
802        let secret_id = require_secret_id(&body)?;
803
804        let mut accounts = self.state.write();
805        let state = accounts.get_or_create(&req.account_id);
806        let secret = self.find_secret_mut(state, &secret_id)?;
807
808        // AWS allows restoring a secret that is not deleted (no-op)
809        secret.deleted = false;
810        secret.deletion_date = None;
811
812        let response = json!({
813            "ARN": secret.arn,
814            "Name": secret.name,
815        });
816
817        Ok(AwsResponse::ok_json(response))
818    }
819
820    fn describe_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
821        let body = req.json_body();
822        let secret_id = require_secret_id(&body)?;
823
824        let accounts = self.state.read();
825        let empty = SecretsManagerState::new(&req.account_id, &req.region);
826        let state = accounts.get(&req.account_id).unwrap_or(&empty);
827        let secret = self.find_secret_ref(state, &secret_id)?;
828
829        let mut response = json!({
830            "ARN": secret.arn,
831            "Name": secret.name,
832            "CreatedDate": secret.created_at.timestamp_millis() as f64 / 1000.0,
833            "LastChangedDate": secret.last_changed_at.timestamp_millis() as f64 / 1000.0,
834        });
835
836        if !secret.versions.is_empty() {
837            let mut version_ids_to_stages: serde_json::Map<String, Value> = serde_json::Map::new();
838            for (vid, version) in &secret.versions {
839                version_ids_to_stages.insert(vid.clone(), json!(version.stages));
840            }
841            response["VersionIdsToStages"] = Value::Object(version_ids_to_stages);
842        }
843
844        if let Some(ref desc) = secret.description {
845            if !desc.is_empty() {
846                response["Description"] = json!(desc);
847            }
848        }
849
850        if secret.tags_ever_set || !secret.tags.is_empty() {
851            response["Tags"] = json!(tags_to_json(&secret.tags));
852        }
853
854        if let Some(ref kms) = secret.kms_key_id {
855            response["KmsKeyId"] = json!(kms);
856        }
857        if secret.deleted {
858            response["DeletedDate"] = json!(secret
859                .deletion_date
860                .map(|d| d.timestamp_millis() as f64 / 1000.0));
861        }
862        if let Some(rotation_enabled) = secret.rotation_enabled {
863            response["RotationEnabled"] = json!(rotation_enabled);
864        }
865        if let Some(ref lambda_arn) = secret.rotation_lambda_arn {
866            response["RotationLambdaARN"] = json!(lambda_arn);
867        }
868        if let Some(ref rules) = secret.rotation_rules {
869            let mut rules_json = json!({});
870            if let Some(days) = rules.automatically_after_days {
871                rules_json["AutomaticallyAfterDays"] = json!(days);
872            }
873            response["RotationRules"] = rules_json;
874        }
875        if let Some(last_rotated) = secret.last_rotated_at {
876            response["LastRotatedDate"] = json!(last_rotated.timestamp_millis() as f64 / 1000.0);
877        }
878        // Calculate NextRotationDate if rotation is enabled
879        if secret.rotation_enabled == Some(true) {
880            if let Some(ref rules) = secret.rotation_rules {
881                if let Some(days) = rules.automatically_after_days {
882                    let base = secret.last_rotated_at.unwrap_or(secret.created_at);
883                    let next = base + chrono::Duration::days(days);
884                    response["NextRotationDate"] = json!(next.timestamp_millis() as f64 / 1000.0);
885                }
886            }
887        }
888
889        Ok(AwsResponse::ok_json(response))
890    }
891
892    fn list_secrets(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
893        let body = req.json_body();
894        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
895        validate_optional_range_i64("maxResults", body["MaxResults"].as_i64(), 1, 100)?;
896        validate_optional_enum("sortBy", body["SortBy"].as_str(), &["name", "created-date"])?;
897        validate_optional_enum("sortOrder", body["SortOrder"].as_str(), &["asc", "desc"])?;
898        let max_results = body["MaxResults"].as_i64().unwrap_or(100) as usize;
899        let next_token = body["NextToken"].as_str();
900        let filters = body["Filters"].as_array();
901        let include_deleted = body["IncludePlannedDeletion"].as_bool().unwrap_or(false);
902
903        // Validate filters
904        if let Some(filters) = filters {
905            for filter in filters {
906                let key = filter["Key"].as_str().unwrap_or("");
907                let values = filter["Values"].as_array();
908
909                if key.is_empty() {
910                    return Err(AwsServiceError::aws_error(
911                        StatusCode::BAD_REQUEST,
912                        "InvalidParameterException",
913                        "Invalid filter key",
914                    ));
915                }
916
917                let valid_keys = [
918                    "all",
919                    "name",
920                    "tag-key",
921                    "description",
922                    "tag-value",
923                    "owning-service",
924                    "primary-region",
925                ];
926                if !valid_keys.contains(&key) {
927                    return Err(AwsServiceError::aws_error(
928                        StatusCode::BAD_REQUEST,
929                        "ValidationException",
930                        format!(
931                            "1 validation error detected: Value '{}' at 'filters.1.member.key' failed to satisfy constraint: Member must satisfy enum value set: [all, name, tag-key, description, tag-value]",
932                            key
933                        ),
934                    ));
935                }
936
937                if values.is_none() || values.unwrap().is_empty() {
938                    return Err(AwsServiceError::aws_error(
939                        StatusCode::BAD_REQUEST,
940                        "InvalidParameterException",
941                        format!("Invalid filter values for key: {key}"),
942                    ));
943                }
944            }
945        }
946
947        let accounts = self.state.read();
948        let empty = SecretsManagerState::new(&req.account_id, &req.region);
949        let state = accounts.get(&req.account_id).unwrap_or(&empty);
950
951        let mut secrets: Vec<&Secret> = state
952            .secrets
953            .values()
954            .filter(|s| {
955                // Exclude deleted unless IncludePlannedDeletion
956                if s.deleted && !include_deleted {
957                    return false;
958                }
959
960                if let Some(filters) = filters {
961                    for filter in filters {
962                        let key = filter["Key"].as_str().unwrap_or("");
963                        let values: Vec<&str> = filter["Values"]
964                            .as_array()
965                            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
966                            .unwrap_or_default();
967
968                        let matches = match key {
969                            "name" => filter_name(s, &values),
970                            "description" => filter_description(s, &values),
971                            "tag-key" => filter_tag_key(s, &values),
972                            "tag-value" => filter_tag_value(s, &values),
973                            "all" => filter_all(s, &values),
974                            "owning-service" => false,
975                            "primary-region" => false,
976                            _ => true,
977                        };
978
979                        if !matches {
980                            return false;
981                        }
982                    }
983                }
984                true
985            })
986            .collect();
987        secrets.sort_by_key(|a| a.created_at);
988
989        // Simple pagination with name-based token
990        let start_idx = if let Some(token) = next_token {
991            secrets.iter().position(|s| s.name == token).unwrap_or(0)
992        } else {
993            0
994        };
995
996        let page: Vec<Value> = secrets
997            .iter()
998            .skip(start_idx)
999            .take(max_results)
1000            .map(|s| {
1001                // AWS always echoes `Description` and `SecretVersionsToStages`
1002                // on every SecretListEntry. The documented `@examples` for
1003                // ListSecrets relies on the fields being present even when
1004                // empty, and SDK consumers index into them unconditionally.
1005                let mut version_ids_to_stages: serde_json::Map<String, Value> =
1006                    serde_json::Map::new();
1007                for (vid, version) in &s.versions {
1008                    version_ids_to_stages.insert(vid.clone(), json!(version.stages));
1009                }
1010                let mut entry = json!({
1011                    "ARN": s.arn,
1012                    "Name": s.name,
1013                    "CreatedDate": s.created_at.timestamp_millis() as f64 / 1000.0,
1014                    "LastChangedDate": s.last_changed_at.timestamp_millis() as f64 / 1000.0,
1015                    "Description": s.description.clone().unwrap_or_default(),
1016                    "SecretVersionsToStages": Value::Object(version_ids_to_stages),
1017                });
1018
1019                if s.tags_ever_set || !s.tags.is_empty() {
1020                    entry["Tags"] = json!(tags_to_json(&s.tags));
1021                }
1022
1023                if let Some(ref kms) = s.kms_key_id {
1024                    entry["KmsKeyId"] = json!(kms);
1025                }
1026                if s.deleted {
1027                    entry["DeletedDate"] = json!(s
1028                        .deletion_date
1029                        .map(|d| d.timestamp_millis() as f64 / 1000.0));
1030                }
1031                entry
1032            })
1033            .collect();
1034
1035        let has_more = start_idx + max_results < secrets.len();
1036        let mut response = json!({
1037            "SecretList": page,
1038        });
1039        if has_more {
1040            if let Some(next) = secrets.get(start_idx + max_results) {
1041                response["NextToken"] = json!(next.name);
1042            }
1043        }
1044
1045        Ok(AwsResponse::ok_json(response))
1046    }
1047
1048    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1049        let body = req.json_body();
1050        let secret_id = require_secret_id(&body)?;
1051
1052        let new_tags = parse_tags(&body["Tags"]);
1053
1054        let mut accounts = self.state.write();
1055        let state = accounts.get_or_create(&req.account_id);
1056        let secret = self.find_secret_mut(state, &secret_id)?;
1057
1058        if !new_tags.is_empty() {
1059            secret.tags_ever_set = true;
1060        }
1061        for (k, v) in new_tags {
1062            // Update existing tag or add new one
1063            if let Some(existing) = secret.tags.iter_mut().find(|(ek, _)| *ek == k) {
1064                existing.1 = v;
1065            } else {
1066                secret.tags.push((k, v));
1067            }
1068        }
1069
1070        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1071    }
1072
1073    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1074        let body = req.json_body();
1075        let secret_id = require_secret_id(&body)?;
1076
1077        let tag_keys: Vec<String> = body["TagKeys"]
1078            .as_array()
1079            .map(|arr| {
1080                arr.iter()
1081                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
1082                    .collect()
1083            })
1084            .unwrap_or_default();
1085
1086        let mut accounts = self.state.write();
1087        let state = accounts.get_or_create(&req.account_id);
1088        let secret = self.find_secret_mut(state, &secret_id)?;
1089
1090        secret.tags.retain(|(k, _)| !tag_keys.contains(k));
1091
1092        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1093    }
1094
1095    fn list_secret_version_ids(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1096        let body = req.json_body();
1097        let secret_id = require_secret_id(&body)?;
1098        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
1099
1100        let accounts = self.state.read();
1101        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1102        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1103        let secret = self.find_secret_ref(state, &secret_id)?;
1104
1105        let versions: Vec<Value> = secret
1106            .versions
1107            .values()
1108            .map(|v| {
1109                json!({
1110                    "VersionId": v.version_id,
1111                    "VersionStages": v.stages,
1112                    "CreatedDate": v.created_at.timestamp_millis() as f64 / 1000.0,
1113                })
1114            })
1115            .collect();
1116
1117        let response = json!({
1118            "ARN": secret.arn,
1119            "Name": secret.name,
1120            "Versions": versions,
1121        });
1122
1123        Ok(AwsResponse::ok_json(response))
1124    }
1125
1126    fn get_random_password(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1127        let body = req.json_body();
1128        let length = body["PasswordLength"].as_i64().unwrap_or(32) as usize;
1129
1130        if length < 4 {
1131            return Err(AwsServiceError::aws_error(
1132                StatusCode::BAD_REQUEST,
1133                "InvalidParameterException",
1134                "InvalidParameterException",
1135            ));
1136        }
1137        if length > 4096 {
1138            return Err(AwsServiceError::aws_error(
1139                StatusCode::BAD_REQUEST,
1140                "InvalidParameterValue",
1141                "InvalidParameterValue",
1142            ));
1143        }
1144
1145        let exclude_lowercase = body["ExcludeLowercase"].as_bool().unwrap_or(false);
1146        let exclude_uppercase = body["ExcludeUppercase"].as_bool().unwrap_or(false);
1147        let exclude_numbers = body["ExcludeNumbers"].as_bool().unwrap_or(false);
1148        let exclude_punctuation = body["ExcludePunctuation"].as_bool().unwrap_or(false);
1149        let include_space = body["IncludeSpace"].as_bool().unwrap_or(false);
1150        let require_each = body["RequireEachIncludedType"].as_bool().unwrap_or(true);
1151        validate_optional_string_length(
1152            "excludeCharacters",
1153            body["ExcludeCharacters"].as_str(),
1154            0,
1155            4096,
1156        )?;
1157        let exclude_chars = body["ExcludeCharacters"].as_str().unwrap_or("").to_string();
1158
1159        let lowercase = "abcdefghijklmnopqrstuvwxyz";
1160        let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1161        let digits = "0123456789";
1162        let punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
1163
1164        let mut char_pool = String::new();
1165        let mut required_chars: Vec<String> = Vec::new();
1166
1167        if !exclude_lowercase {
1168            let filtered: String = lowercase
1169                .chars()
1170                .filter(|c| !exclude_chars.contains(*c))
1171                .collect();
1172            if !filtered.is_empty() {
1173                required_chars.push(filtered.clone());
1174                char_pool.push_str(&filtered);
1175            }
1176        }
1177        if !exclude_uppercase {
1178            let filtered: String = uppercase
1179                .chars()
1180                .filter(|c| !exclude_chars.contains(*c))
1181                .collect();
1182            if !filtered.is_empty() {
1183                required_chars.push(filtered.clone());
1184                char_pool.push_str(&filtered);
1185            }
1186        }
1187        if !exclude_numbers {
1188            let filtered: String = digits
1189                .chars()
1190                .filter(|c| !exclude_chars.contains(*c))
1191                .collect();
1192            if !filtered.is_empty() {
1193                required_chars.push(filtered.clone());
1194                char_pool.push_str(&filtered);
1195            }
1196        }
1197        if !exclude_punctuation {
1198            let filtered: String = punctuation
1199                .chars()
1200                .filter(|c| !exclude_chars.contains(*c))
1201                .collect();
1202            if !filtered.is_empty() {
1203                required_chars.push(filtered.clone());
1204                char_pool.push_str(&filtered);
1205            }
1206        }
1207        if include_space && !exclude_chars.contains(' ') {
1208            char_pool.push(' ');
1209        }
1210
1211        if char_pool.is_empty() {
1212            return Err(AwsServiceError::aws_error(
1213                StatusCode::BAD_REQUEST,
1214                "InvalidParameterException",
1215                "InvalidParameterException",
1216            ));
1217        }
1218
1219        let pool_bytes: Vec<char> = char_pool.chars().collect();
1220        let mut password = String::with_capacity(length);
1221
1222        // Use simple random generation
1223        if require_each {
1224            // First, ensure at least one character from each required category
1225            for category in &required_chars {
1226                let chars: Vec<char> = category.chars().collect();
1227                let idx = simple_random() % chars.len();
1228                password.push(chars[idx]);
1229            }
1230            if include_space && !exclude_chars.contains(' ') {
1231                password.push(' ');
1232            }
1233        }
1234
1235        // Fill the rest randomly
1236        while password.len() < length {
1237            let idx = simple_random() % pool_bytes.len();
1238            password.push(pool_bytes[idx]);
1239        }
1240
1241        // Shuffle the password (Fisher-Yates)
1242        let mut chars: Vec<char> = password.chars().collect();
1243        for i in (1..chars.len()).rev() {
1244            let j = simple_random() % (i + 1);
1245            chars.swap(i, j);
1246        }
1247        let password: String = chars.into_iter().take(length).collect();
1248
1249        let response = json!({
1250            "RandomPassword": password,
1251        });
1252
1253        Ok(AwsResponse::ok_json(response))
1254    }
1255
1256    fn rotate_secret(
1257        &self,
1258        req: &AwsRequest,
1259    ) -> Result<(AwsResponse, Option<RotationInvocation>), AwsServiceError> {
1260        let body = req.json_body();
1261        let secret_id = require_secret_id(&body)?;
1262
1263        // Validate ClientRequestToken
1264        if let Some(token) = body["ClientRequestToken"].as_str() {
1265            if token.len() < 32 || token.len() > 64 {
1266                return Err(AwsServiceError::aws_error(
1267                    StatusCode::BAD_REQUEST,
1268                    "InvalidParameterException",
1269                    "ClientRequestToken must be 32-64 characters long.",
1270                ));
1271            }
1272        }
1273
1274        // Validate RotationLambdaARN
1275        if let Some(arn) = body["RotationLambdaARN"].as_str() {
1276            if arn.len() > 2048 {
1277                return Err(AwsServiceError::aws_error(
1278                    StatusCode::BAD_REQUEST,
1279                    "InvalidParameterException",
1280                    "RotationLambdaARN length must be less than or equal to 2048.",
1281                ));
1282            }
1283        }
1284
1285        // Validate RotationRules
1286        if let Some(rules) = body["RotationRules"].as_object() {
1287            if let Some(days) = rules.get("AutomaticallyAfterDays").and_then(|v| v.as_i64()) {
1288                if !(1..=1000).contains(&days) {
1289                    return Err(AwsServiceError::aws_error(
1290                        StatusCode::BAD_REQUEST,
1291                        "InvalidParameterException",
1292                        "RotationRules.AutomaticallyAfterDays must be within 1-1000.",
1293                    ));
1294                }
1295            }
1296        }
1297
1298        let mut accounts = self.state.write();
1299        let state = accounts.get_or_create(&req.account_id);
1300        let secret = self.find_secret_mut(state, &secret_id)?;
1301
1302        if secret.deleted {
1303            return Err(AwsServiceError::aws_error(
1304                StatusCode::BAD_REQUEST,
1305                "InvalidRequestException",
1306                "You can't perform this operation on the secret because it was marked for deletion.",
1307            ));
1308        }
1309
1310        // Set rotation config
1311        if let Some(lambda_arn) = body["RotationLambdaARN"].as_str() {
1312            secret.rotation_lambda_arn = Some(lambda_arn.to_string());
1313        }
1314
1315        if let Some(rules) = body["RotationRules"].as_object() {
1316            let days = rules.get("AutomaticallyAfterDays").and_then(|v| v.as_i64());
1317            secret.rotation_rules = Some(RotationRules {
1318                automatically_after_days: days,
1319            });
1320        }
1321
1322        secret.rotation_enabled = Some(true);
1323        let now = Utc::now();
1324        secret.last_rotated_at = Some(now);
1325        secret.last_changed_at = now;
1326
1327        let version_id = body["ClientRequestToken"]
1328            .as_str()
1329            .map(|s| s.to_string())
1330            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
1331
1332        let has_lambda =
1333            body["RotationLambdaARN"].as_str().is_some() || secret.rotation_lambda_arn.is_some();
1334        let lambda_arn = secret.rotation_lambda_arn.clone();
1335
1336        // If the secret has a value, perform rotation
1337        let mut invocation = None;
1338        if let Some(current_vid) = secret.current_version_id.clone() {
1339            let current_value = secret.versions.get(&current_vid).cloned();
1340
1341            if let Some(cv) = current_value {
1342                if has_lambda {
1343                    // With Lambda: do NOT pre-create the AWSPENDING version. The
1344                    // rotation Lambda is responsible for putting the new value via
1345                    // PutSecretValue with VersionStages=[AWSPENDING] during the
1346                    // createSecret step (matching real AWS Secrets Manager behavior).
1347
1348                    // Schedule Lambda invocation
1349                    if let Some(ref arn) = lambda_arn {
1350                        invocation = Some(RotationInvocation {
1351                            lambda_arn: arn.clone(),
1352                            secret_id: secret.arn.clone(),
1353                            client_request_token: version_id.clone(),
1354                        });
1355                    }
1356                } else {
1357                    // Without Lambda: simple rotation - new version becomes AWSCURRENT
1358                    // Move old version to AWSPREVIOUS
1359                    if let Some(old_v) = secret.versions.get_mut(&current_vid) {
1360                        old_v.stages.retain(|s| s != "AWSCURRENT");
1361                        if !old_v.stages.contains(&"AWSPREVIOUS".to_string()) {
1362                            old_v.stages.push("AWSPREVIOUS".to_string());
1363                        }
1364                    }
1365                    let version = SecretVersion {
1366                        version_id: version_id.clone(),
1367                        secret_string: cv.secret_string.clone(),
1368                        secret_binary: cv.secret_binary.clone(),
1369                        stages: vec!["AWSCURRENT".to_string()],
1370                        created_at: now,
1371                    };
1372                    secret.versions.insert(version_id.clone(), version);
1373                    secret.current_version_id = Some(version_id.clone());
1374                }
1375            }
1376        }
1377
1378        let response = json!({
1379            "ARN": secret.arn,
1380            "Name": secret.name,
1381            "VersionId": version_id,
1382        });
1383
1384        Ok((AwsResponse::ok_json(response), invocation))
1385    }
1386
1387    fn cancel_rotate_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1388        let body = req.json_body();
1389        let secret_id = require_secret_id(&body)?;
1390
1391        let mut accounts = self.state.write();
1392        let state = accounts.get_or_create(&req.account_id);
1393        let secret = self.find_secret_mut(state, &secret_id)?;
1394
1395        if secret.deleted {
1396            return Err(AwsServiceError::aws_error(
1397                StatusCode::BAD_REQUEST,
1398                "InvalidRequestException",
1399                "You can't perform this operation on the secret because it was marked for deletion.",
1400            ));
1401        }
1402
1403        if secret.rotation_enabled != Some(true) {
1404            return Err(AwsServiceError::aws_error(
1405                StatusCode::BAD_REQUEST,
1406                "InvalidRequestException",
1407                "You can't cancel rotation for a secret that does not have rotation enabled.",
1408            ));
1409        }
1410
1411        secret.rotation_enabled = Some(false);
1412
1413        let response = json!({
1414            "ARN": secret.arn,
1415            "Name": secret.name,
1416        });
1417
1418        Ok(AwsResponse::ok_json(response))
1419    }
1420
1421    fn update_secret_version_stage(
1422        &self,
1423        req: &AwsRequest,
1424    ) -> Result<AwsResponse, AwsServiceError> {
1425        let body = req.json_body();
1426        let secret_id = require_secret_id(&body)?;
1427        let version_stage = body["VersionStage"]
1428            .as_str()
1429            .ok_or_else(|| {
1430                AwsServiceError::aws_error(
1431                    StatusCode::BAD_REQUEST,
1432                    "InvalidParameterException",
1433                    "VersionStage is required",
1434                )
1435            })?
1436            .to_string();
1437        validate_string_length("versionStage", &version_stage, 1, 256)?;
1438        validate_optional_string_length(
1439            "removeFromVersionId",
1440            body["RemoveFromVersionId"].as_str(),
1441            32,
1442            64,
1443        )?;
1444        validate_optional_string_length(
1445            "moveToVersionId",
1446            body["MoveToVersionId"].as_str(),
1447            32,
1448            64,
1449        )?;
1450
1451        let move_to = body["MoveToVersionId"].as_str().map(|s| s.to_string());
1452        let remove_from = body["RemoveFromVersionId"].as_str().map(|s| s.to_string());
1453
1454        let mut accounts = self.state.write();
1455        let state = accounts.get_or_create(&req.account_id);
1456        let secret = self.find_secret_mut(state, &secret_id)?;
1457
1458        // Validate: if moving AWSCURRENT, must specify RemoveFromVersionId
1459        if version_stage == "AWSCURRENT" && move_to.is_some() && remove_from.is_none() {
1460            // Find the version that currently has AWSCURRENT
1461            let current_holder = secret
1462                .versions
1463                .iter()
1464                .find(|(_, v)| v.stages.contains(&"AWSCURRENT".to_string()))
1465                .map(|(id, _)| id.clone());
1466
1467            if let Some(current_vid) = current_holder {
1468                return Err(AwsServiceError::aws_error(
1469                    StatusCode::BAD_REQUEST,
1470                    "InvalidParameterException",
1471                    format!(
1472                        "The parameter RemoveFromVersionId can't be empty. Staging label AWSCURRENT is currently attached to version {current_vid}, so you must explicitly reference that version in RemoveFromVersionId."
1473                    ),
1474                ));
1475            }
1476        }
1477
1478        // Remove stage from specified version
1479        if let Some(ref remove_vid) = remove_from {
1480            if let Some(version) = secret.versions.get_mut(remove_vid) {
1481                version.stages.retain(|s| s != &version_stage);
1482                // If moving AWSCURRENT away, add AWSPREVIOUS and remove from others
1483                if version_stage == "AWSCURRENT" {
1484                    // Remove AWSPREVIOUS from all other versions first
1485                    for (id, v) in secret.versions.iter_mut() {
1486                        if id != remove_vid {
1487                            v.stages.retain(|s| s != "AWSPREVIOUS");
1488                        }
1489                    }
1490                    // Now add AWSPREVIOUS to the version losing AWSCURRENT
1491                    if let Some(v) = secret.versions.get_mut(remove_vid) {
1492                        if !v.stages.contains(&"AWSPREVIOUS".to_string()) {
1493                            v.stages.push("AWSPREVIOUS".to_string());
1494                        }
1495                    }
1496                }
1497            }
1498        }
1499
1500        // Add stage to specified version
1501        if let Some(ref move_vid) = move_to {
1502            if let Some(version) = secret.versions.get_mut(move_vid) {
1503                if !version.stages.contains(&version_stage) {
1504                    version.stages.push(version_stage.clone());
1505                }
1506            }
1507            // Update current_version_id if we moved AWSCURRENT
1508            if version_stage == "AWSCURRENT" {
1509                secret.current_version_id = Some(move_vid.clone());
1510            }
1511        }
1512
1513        let response = json!({
1514            "ARN": secret.arn,
1515            "Name": secret.name,
1516        });
1517
1518        Ok(AwsResponse::ok_json(response))
1519    }
1520
1521    fn batch_get_secret_value(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1522        let body = req.json_body();
1523        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
1524        let secret_id_list = body["SecretIdList"].as_array();
1525        let filters = body["Filters"].as_array();
1526        let max_results = body.get("MaxResults").and_then(|v| v.as_i64());
1527
1528        // Validate: can't use both SecretIdList and Filters
1529        if secret_id_list.is_some() && filters.is_some() {
1530            return Err(AwsServiceError::aws_error(
1531                StatusCode::BAD_REQUEST,
1532                "InvalidParameterException",
1533                "Either 'SecretIdList' or 'Filters' must be provided, but not both.",
1534            ));
1535        }
1536
1537        // Validate: MaxResults requires Filters
1538        if max_results.is_some() && filters.is_none() {
1539            return Err(AwsServiceError::aws_error(
1540                StatusCode::BAD_REQUEST,
1541                "InvalidParameterException",
1542                "'Filters' not specified. 'Filters' must also be specified when 'MaxResults' is provided.",
1543            ));
1544        }
1545
1546        let accounts = self.state.read();
1547        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1548        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1549        let mut secret_values: Vec<Value> = Vec::new();
1550        let mut errors: Vec<Value> = Vec::new();
1551
1552        if let Some(id_list) = secret_id_list {
1553            for id_val in id_list {
1554                let sid = id_val.as_str().unwrap_or("");
1555                match self.find_secret_ref(state, sid) {
1556                    Ok(secret) => {
1557                        if secret.deleted {
1558                            errors.push(json!({
1559                                "SecretId": sid,
1560                                "ErrorCode": "InvalidRequestException",
1561                                "Message": "Secret is currently marked deleted. Secret can be recovered with RestoreSecret. Secret is currently marked deleted.",
1562                            }));
1563                        } else if let Some(ref current_vid) = secret.current_version_id {
1564                            if let Some(version) = secret.versions.get(current_vid) {
1565                                let mut entry = json!({
1566                                    "ARN": secret.arn,
1567                                    "Name": secret.name,
1568                                    "VersionId": version.version_id,
1569                                    "VersionStages": version.stages,
1570                                    "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
1571                                });
1572                                if let Some(ref s) = version.secret_string {
1573                                    entry["SecretString"] = json!(s);
1574                                }
1575                                if let Some(ref b) = version.secret_binary {
1576                                    entry["SecretBinary"] = json!(base64_encode(b));
1577                                }
1578                                secret_values.push(entry);
1579                            } else {
1580                                errors.push(json!({
1581                                    "SecretId": sid,
1582                                    "ErrorCode": "ResourceNotFoundException",
1583                                    "Message": "Secrets Manager can't find the specified secret.",
1584                                }));
1585                            }
1586                        } else {
1587                            errors.push(json!({
1588                                "SecretId": sid,
1589                                "ErrorCode": "ResourceNotFoundException",
1590                                "Message": "Secrets Manager can't find the specified secret.",
1591                            }));
1592                        }
1593                    }
1594                    Err(_) => {
1595                        errors.push(json!({
1596                            "SecretId": sid,
1597                            "ErrorCode": "ResourceNotFoundException",
1598                            "Message": "Secrets Manager can't find the specified secret.",
1599                        }));
1600                    }
1601                }
1602            }
1603        } else if let Some(filters) = filters {
1604            // Get secrets matching filters
1605            let matching: Vec<&Secret> = state
1606                .secrets
1607                .values()
1608                .filter(|s| {
1609                    if s.deleted {
1610                        return false;
1611                    }
1612                    for filter in filters {
1613                        let key = filter["Key"].as_str().unwrap_or("");
1614                        let values: Vec<&str> = filter["Values"]
1615                            .as_array()
1616                            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
1617                            .unwrap_or_default();
1618                        let matches = match key {
1619                            "name" => filter_name(s, &values),
1620                            "description" => filter_description(s, &values),
1621                            "tag-key" => filter_tag_key(s, &values),
1622                            "tag-value" => filter_tag_value(s, &values),
1623                            "all" => filter_all(s, &values),
1624                            _ => true,
1625                        };
1626                        if !matches {
1627                            return false;
1628                        }
1629                    }
1630                    true
1631                })
1632                .collect();
1633
1634            let limit = max_results.unwrap_or(100) as usize;
1635            let mut no_value_found = false;
1636            let mut matching = matching;
1637            matching.sort_by(|a, b| a.name.cmp(&b.name));
1638
1639            for secret in matching.iter().take(limit) {
1640                if let Some(ref current_vid) = secret.current_version_id {
1641                    if let Some(version) = secret.versions.get(current_vid) {
1642                        let mut entry = json!({
1643                            "ARN": secret.arn,
1644                            "Name": secret.name,
1645                            "VersionId": version.version_id,
1646                            "VersionStages": version.stages,
1647                            "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
1648                        });
1649                        if let Some(ref s) = version.secret_string {
1650                            entry["SecretString"] = json!(s);
1651                        }
1652                        if let Some(ref b) = version.secret_binary {
1653                            entry["SecretBinary"] = json!(base64_encode(b));
1654                        }
1655                        secret_values.push(entry);
1656                    } else {
1657                        no_value_found = true;
1658                    }
1659                } else {
1660                    no_value_found = true;
1661                }
1662            }
1663
1664            if no_value_found && secret_values.is_empty() {
1665                return Err(AwsServiceError::aws_error(
1666                    StatusCode::NOT_FOUND,
1667                    "ResourceNotFoundException",
1668                    "Secrets Manager can't find the specified secret.",
1669                ));
1670            }
1671        }
1672
1673        let mut response = json!({
1674            "SecretValues": secret_values,
1675            "Errors": errors,
1676        });
1677
1678        // Remove empty arrays
1679        if errors.is_empty() {
1680            response.as_object_mut().unwrap().remove("Errors");
1681        }
1682
1683        Ok(AwsResponse::ok_json(response))
1684    }
1685
1686    fn get_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1687        let body = req.json_body();
1688        let secret_id = require_secret_id(&body)?;
1689
1690        let accounts = self.state.read();
1691        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1692        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1693        let secret = self.find_secret_ref(state, &secret_id)?;
1694
1695        // Real AWS omits ResourcePolicy when none is attached; terraform
1696        // provider and the SDK choke on an empty-string policy.
1697        let mut response = json!({
1698            "ARN": secret.arn,
1699            "Name": secret.name,
1700        });
1701        if let Some(ref policy) = secret.resource_policy {
1702            response["ResourcePolicy"] = json!(policy);
1703        }
1704
1705        Ok(AwsResponse::ok_json(response))
1706    }
1707
1708    fn validate_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1709        let body = req.json_body();
1710        validate_optional_string_length("secretId", body["SecretId"].as_str(), 1, 2048)?;
1711        validate_required("ResourcePolicy", &body["ResourcePolicy"])?;
1712        let policy_str = body["ResourcePolicy"].as_str().ok_or_else(|| {
1713            AwsServiceError::aws_error(
1714                StatusCode::BAD_REQUEST,
1715                "InvalidParameterException",
1716                "ResourcePolicy must be a string",
1717            )
1718        })?;
1719        validate_string_length("resourcePolicy", policy_str, 1, 20480)?;
1720
1721        // If SecretId is provided, verify the secret exists
1722        if let Some(secret_id) = body["SecretId"].as_str() {
1723            let accounts = self.state.read();
1724            let empty = SecretsManagerState::new(&req.account_id, &req.region);
1725            let state = accounts.get(&req.account_id).unwrap_or(&empty);
1726            self.find_secret_key(state, secret_id)?;
1727        }
1728
1729        let response = json!({
1730            "PolicyValidationPassed": true,
1731            "ValidationErrors": [],
1732        });
1733        Ok(AwsResponse::ok_json(response))
1734    }
1735
1736    fn put_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1737        let body = req.json_body();
1738        let secret_id = require_secret_id(&body)?;
1739        validate_required("ResourcePolicy", &body["ResourcePolicy"])?;
1740        validate_optional_string_length(
1741            "resourcePolicy",
1742            body["ResourcePolicy"].as_str(),
1743            1,
1744            20480,
1745        )?;
1746        let policy = body["ResourcePolicy"].as_str().map(|s| s.to_string());
1747
1748        let mut accounts = self.state.write();
1749        let state = accounts.get_or_create(&req.account_id);
1750        let secret = self.find_secret_mut(state, &secret_id)?;
1751        secret.resource_policy = policy;
1752
1753        let response = json!({
1754            "ARN": secret.arn,
1755            "Name": secret.name,
1756        });
1757
1758        Ok(AwsResponse::ok_json(response))
1759    }
1760
1761    fn delete_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1762        let body = req.json_body();
1763        let secret_id = require_secret_id(&body)?;
1764
1765        let mut accounts = self.state.write();
1766        let state = accounts.get_or_create(&req.account_id);
1767        let secret = self.find_secret_mut(state, &secret_id)?;
1768        secret.resource_policy = None;
1769
1770        let response = json!({
1771            "ARN": secret.arn,
1772            "Name": secret.name,
1773        });
1774
1775        Ok(AwsResponse::ok_json(response))
1776    }
1777
1778    fn replicate_secret_to_regions(
1779        &self,
1780        req: &AwsRequest,
1781    ) -> Result<AwsResponse, AwsServiceError> {
1782        let body = req.json_body();
1783        let secret_id = require_secret_id(&body)?;
1784
1785        let accounts = self.state.read();
1786        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1787        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1788        let secret = self.find_secret_ref(state, &secret_id)?;
1789
1790        let response = json!({
1791            "ARN": secret.arn,
1792            "ReplicationStatus": [],
1793        });
1794        Ok(AwsResponse::ok_json(response))
1795    }
1796
1797    fn remove_regions_from_replication(
1798        &self,
1799        req: &AwsRequest,
1800    ) -> Result<AwsResponse, AwsServiceError> {
1801        let body = req.json_body();
1802        let secret_id = require_secret_id(&body)?;
1803
1804        let accounts = self.state.read();
1805        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1806        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1807        let secret = self.find_secret_ref(state, &secret_id)?;
1808
1809        let response = json!({
1810            "ARN": secret.arn,
1811            "ReplicationStatus": [],
1812        });
1813        Ok(AwsResponse::ok_json(response))
1814    }
1815
1816    fn stop_replication_to_replica(
1817        &self,
1818        req: &AwsRequest,
1819    ) -> Result<AwsResponse, AwsServiceError> {
1820        let body = req.json_body();
1821        let secret_id = require_secret_id(&body)?;
1822
1823        let accounts = self.state.read();
1824        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1825        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1826        let secret = self.find_secret_ref(state, &secret_id)?;
1827
1828        let response = json!({
1829            "ARN": secret.arn,
1830        });
1831        Ok(AwsResponse::ok_json(response))
1832    }
1833
1834    /// Find a secret by name, full ARN, or partial ARN (mutable).
1835    fn find_secret_mut<'a>(
1836        &self,
1837        state: &'a mut crate::state::SecretsManagerState,
1838        secret_id: &str,
1839    ) -> Result<&'a mut Secret, AwsServiceError> {
1840        let key = self.find_secret_key(state, secret_id)?;
1841        Ok(state.secrets.get_mut(&key).unwrap())
1842    }
1843
1844    fn find_secret_key(
1845        &self,
1846        state: &crate::state::SecretsManagerState,
1847        secret_id: &str,
1848    ) -> Result<String, AwsServiceError> {
1849        if state.secrets.contains_key(secret_id) {
1850            return Ok(secret_id.to_string());
1851        }
1852
1853        for secret in state.secrets.values() {
1854            if secret.arn == secret_id {
1855                return Ok(secret.name.clone());
1856            }
1857        }
1858
1859        if secret_id.starts_with("arn:aws:secretsmanager:") {
1860            for secret in state.secrets.values() {
1861                if secret.arn.starts_with(secret_id) {
1862                    return Ok(secret.name.clone());
1863                }
1864            }
1865        }
1866
1867        Err(AwsServiceError::aws_error(
1868            StatusCode::NOT_FOUND,
1869            "ResourceNotFoundException",
1870            "Secrets Manager can't find the specified secret.",
1871        ))
1872    }
1873
1874    /// Find a secret by name, full ARN, or partial ARN (immutable).
1875    fn find_secret_ref<'a>(
1876        &self,
1877        state: &'a crate::state::SecretsManagerState,
1878        secret_id: &str,
1879    ) -> Result<&'a Secret, AwsServiceError> {
1880        if let Some(secret) = state.secrets.get(secret_id) {
1881            return Ok(secret);
1882        }
1883
1884        // Search by full ARN
1885        for secret in state.secrets.values() {
1886            if secret.arn == secret_id {
1887                return Ok(secret);
1888            }
1889        }
1890
1891        // Search by partial ARN
1892        if secret_id.starts_with("arn:aws:secretsmanager:") {
1893            for secret in state.secrets.values() {
1894                if secret.arn.starts_with(secret_id) {
1895                    return Ok(secret);
1896                }
1897            }
1898        }
1899
1900        Err(AwsServiceError::aws_error(
1901            StatusCode::NOT_FOUND,
1902            "ResourceNotFoundException",
1903            "Secrets Manager can't find the specified secret.",
1904        ))
1905    }
1906}
1907
1908/// Persist the current Secrets Manager state as a snapshot. Offloads the
1909/// serialization and blocking file write to the Tokio blocking pool. Noop when
1910/// `store` is `None` (memory mode). Shared by
1911/// `SecretsManagerService::save_snapshot` and
1912/// the CloudFormation provisioner's post-provision persist hook so both route
1913/// through the same serialize-and-write path.
1914pub async fn save_secretsmanager_snapshot(
1915    state: &SharedSecretsManagerState,
1916    store: Option<Arc<dyn SnapshotStore>>,
1917    lock: &AsyncMutex<()>,
1918) {
1919    let Some(store) = store else {
1920        return;
1921    };
1922    let _guard = lock.lock().await;
1923    let snapshot = SecretsManagerSnapshot {
1924        schema_version: SECRETSMANAGER_SNAPSHOT_SCHEMA_VERSION,
1925        state: None,
1926        accounts: Some(state.read().clone()),
1927    };
1928    let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
1929        let bytes = serde_json::to_vec(&snapshot)
1930            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
1931        store.save(&bytes)
1932    })
1933    .await;
1934    match join {
1935        Ok(Ok(())) => {}
1936        Ok(Err(err)) => tracing::error!(%err, "failed to write secretsmanager snapshot"),
1937        Err(err) => tracing::error!(%err, "secretsmanager snapshot task panicked"),
1938    }
1939}
1940
1941/// Parsed + validated inputs for `CreateSecret`.
1942struct CreateSecretInput {
1943    name: String,
1944    client_request_token: Option<String>,
1945    description: Option<String>,
1946    kms_key_id: Option<String>,
1947    secret_string: Option<String>,
1948    secret_binary: Option<Vec<u8>>,
1949    tags: Vec<(String, String)>,
1950}
1951
1952impl CreateSecretInput {
1953    fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
1954        validate_required("Name", &body["Name"])?;
1955        let name = body["Name"]
1956            .as_str()
1957            .ok_or_else(|| {
1958                AwsServiceError::aws_error(
1959                    StatusCode::BAD_REQUEST,
1960                    "InvalidParameterException",
1961                    "Name is required",
1962                )
1963            })?
1964            .to_string();
1965        validate_string_length("name", &name, 1, 512)?;
1966        validate_optional_string_length(
1967            "clientRequestToken",
1968            body["ClientRequestToken"].as_str(),
1969            32,
1970            64,
1971        )?;
1972        validate_optional_string_length("description", body["Description"].as_str(), 0, 2048)?;
1973        validate_optional_string_length("kmsKeyId", body["KmsKeyId"].as_str(), 0, 2048)?;
1974        validate_optional_string_length("secretString", body["SecretString"].as_str(), 1, 65536)?;
1975
1976        Ok(Self {
1977            name,
1978            client_request_token: body["ClientRequestToken"].as_str().map(|s| s.to_string()),
1979            description: body["Description"].as_str().map(|s| s.to_string()),
1980            kms_key_id: body["KmsKeyId"].as_str().map(|s| s.to_string()),
1981            secret_string: body["SecretString"].as_str().map(|s| s.to_string()),
1982            secret_binary: body["SecretBinary"].as_str().and_then(base64_decode),
1983            tags: parse_tags(&body["Tags"]),
1984        })
1985    }
1986}
1987
1988#[async_trait]
1989impl AwsService for SecretsManagerService {
1990    fn service_name(&self) -> &str {
1991        "secretsmanager"
1992    }
1993
1994    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1995        let mutates = is_mutating_action(req.action.as_str());
1996        let result = match req.action.as_str() {
1997            "CreateSecret" => self.create_secret(&req),
1998            "GetSecretValue" => self.get_secret_value(&req),
1999            "PutSecretValue" => self.put_secret_value(&req),
2000            "UpdateSecret" => self.update_secret(&req),
2001            "DeleteSecret" => self.delete_secret(&req),
2002            "RestoreSecret" => self.restore_secret(&req),
2003            "DescribeSecret" => self.describe_secret(&req),
2004            "ListSecrets" => self.list_secrets(&req),
2005            "TagResource" => self.tag_resource(&req),
2006            "UntagResource" => self.untag_resource(&req),
2007            "ListSecretVersionIds" => self.list_secret_version_ids(&req),
2008            "GetRandomPassword" => self.get_random_password(&req),
2009            "RotateSecret" => {
2010                let (response, invocation) = self.rotate_secret(&req)?;
2011                if let Some(inv) = invocation {
2012                    if let Some(ref bus) = self.delivery_bus {
2013                        let bus = bus.clone();
2014                        // AWS invokes the rotation Lambda asynchronously for each step.
2015                        tokio::spawn(async move {
2016                            for step in &["createSecret", "setSecret", "testSecret", "finishSecret"]
2017                            {
2018                                let payload = serde_json::json!({
2019                                    "SecretId": inv.secret_id,
2020                                    "ClientRequestToken": inv.client_request_token,
2021                                    "Step": step,
2022                                });
2023                                let payload_str = payload.to_string();
2024                                match bus.invoke_lambda(&inv.lambda_arn, &payload_str).await {
2025                                    Some(Ok(_)) => {}
2026                                    Some(Err(e)) => {
2027                                        tracing::warn!(
2028                                            step = step,
2029                                            error = %e,
2030                                            "rotation Lambda invocation failed"
2031                                        );
2032                                    }
2033                                    None => {
2034                                        tracing::warn!(
2035                                            lambda_arn = %inv.lambda_arn,
2036                                            step = step,
2037                                            "rotation Lambda delivery not configured; \
2038                                             Lambda invocation skipped"
2039                                        );
2040                                        break;
2041                                    }
2042                                }
2043                            }
2044                        });
2045                    }
2046                }
2047                Ok(response)
2048            }
2049            "CancelRotateSecret" => self.cancel_rotate_secret(&req),
2050            "UpdateSecretVersionStage" => self.update_secret_version_stage(&req),
2051            "BatchGetSecretValue" => self.batch_get_secret_value(&req),
2052            "GetResourcePolicy" => self.get_resource_policy(&req),
2053            "PutResourcePolicy" => self.put_resource_policy(&req),
2054            "DeleteResourcePolicy" => self.delete_resource_policy(&req),
2055            "ValidateResourcePolicy" => self.validate_resource_policy(&req),
2056            "ReplicateSecretToRegions" => self.replicate_secret_to_regions(&req),
2057            "RemoveRegionsFromReplication" => self.remove_regions_from_replication(&req),
2058            "StopReplicationToReplica" => self.stop_replication_to_replica(&req),
2059            _ => Err(AwsServiceError::action_not_implemented(
2060                "secretsmanager",
2061                &req.action,
2062            )),
2063        };
2064        if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
2065            self.save_snapshot().await;
2066        }
2067        result.map_err(remap_validation_error)
2068    }
2069
2070    fn supported_actions(&self) -> &[&str] {
2071        &[
2072            "CreateSecret",
2073            "GetSecretValue",
2074            "PutSecretValue",
2075            "UpdateSecret",
2076            "DeleteSecret",
2077            "RestoreSecret",
2078            "DescribeSecret",
2079            "ListSecrets",
2080            "TagResource",
2081            "UntagResource",
2082            "ListSecretVersionIds",
2083            "GetRandomPassword",
2084            "RotateSecret",
2085            "CancelRotateSecret",
2086            "UpdateSecretVersionStage",
2087            "BatchGetSecretValue",
2088            "GetResourcePolicy",
2089            "PutResourcePolicy",
2090            "DeleteResourcePolicy",
2091            "ValidateResourcePolicy",
2092            "ReplicateSecretToRegions",
2093            "RemoveRegionsFromReplication",
2094            "StopReplicationToReplica",
2095        ]
2096    }
2097}
2098
2099#[path = "service_helpers.rs"]
2100mod service_helpers;
2101pub(crate) use service_helpers::*;
2102
2103/// The shared `fakecloud_core::validation` helpers raise
2104/// `ValidationException`, but the Secrets Manager Smithy model does not
2105/// declare `ValidationException` on any operation — its declared input
2106/// error is `InvalidParameterException`. Translate at the dispatcher
2107/// boundary so the wire-level error code matches real AWS without
2108/// duplicating every validator.
2109fn remap_validation_error(err: AwsServiceError) -> AwsServiceError {
2110    match err {
2111        AwsServiceError::AwsError {
2112            status,
2113            code,
2114            message,
2115            extra_fields,
2116            headers,
2117        } if code == "ValidationException" => AwsServiceError::AwsError {
2118            status,
2119            code: "InvalidParameterException".to_string(),
2120            message,
2121            extra_fields,
2122            headers,
2123        },
2124        other => other,
2125    }
2126}
2127
2128/// Extract the owning account-id from an `arn:aws:secretsmanager:...:ACCOUNT:secret:...`
2129/// secret id. Returns `caller_account` when the input is a bare name
2130/// or a same-account ARN.
2131fn secret_owner_account(secret_id: &str, caller_account: &str) -> String {
2132    if !secret_id.starts_with("arn:aws:secretsmanager:") {
2133        return caller_account.to_string();
2134    }
2135    let parts: Vec<&str> = secret_id.splitn(7, ':').collect();
2136    if parts.len() < 5 {
2137        return caller_account.to_string();
2138    }
2139    let account = parts[4];
2140    if account.is_empty() {
2141        caller_account.to_string()
2142    } else {
2143        account.to_string()
2144    }
2145}
2146
2147/// Evaluate a Secrets Manager resource policy against a cross-account
2148/// caller. Empty policy implicitly denies (real AWS behaviour). Uses
2149/// the shared IAM evaluator so the same policy semantics apply
2150/// service-wide.
2151fn resource_policy_allows(policy_doc: &str, caller_account: &str, secret_arn: &str) -> bool {
2152    if policy_doc.is_empty() {
2153        return false;
2154    }
2155    use fakecloud_core::auth::{Principal, PrincipalType};
2156    use fakecloud_iam::evaluator::{evaluate, EvalRequest, PolicyDocument};
2157    let doc = PolicyDocument::parse(policy_doc);
2158    let principal_arn = Arn::global("iam", caller_account, "root").to_string();
2159    let principal = Principal {
2160        arn: principal_arn.clone(),
2161        user_id: principal_arn.clone(),
2162        account_id: caller_account.to_string(),
2163        principal_type: PrincipalType::User,
2164        source_identity: None,
2165        tags: None,
2166    };
2167    let req = EvalRequest {
2168        principal: &principal,
2169        action: "secretsmanager:GetSecretValue".to_string(),
2170        resource: secret_arn.to_string(),
2171        context: Default::default(),
2172    };
2173    matches!(
2174        evaluate(&[doc], &req),
2175        fakecloud_iam::evaluator::Decision::Allow
2176    )
2177}
2178
2179#[cfg(test)]
2180#[path = "service_tests.rs"]
2181mod tests;