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