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            if let Some(ref duration) = rules.duration {
875                rules_json["Duration"] = json!(duration);
876            }
877            if let Some(ref expr) = rules.schedule_expression {
878                rules_json["ScheduleExpression"] = json!(expr);
879            }
880            response["RotationRules"] = rules_json;
881        }
882        if let Some(last_rotated) = secret.last_rotated_at {
883            response["LastRotatedDate"] = json!(last_rotated.timestamp_millis() as f64 / 1000.0);
884        }
885        if !secret.replica_regions.is_empty() {
886            response["ReplicationStatus"] = replication_status_json(&secret.replica_regions);
887        }
888        // Calculate NextRotationDate if rotation is enabled
889        if secret.rotation_enabled == Some(true) {
890            if let Some(ref rules) = secret.rotation_rules {
891                if let Some(days) = rules.automatically_after_days {
892                    let base = secret.last_rotated_at.unwrap_or(secret.created_at);
893                    let next = base + chrono::Duration::days(days);
894                    response["NextRotationDate"] = json!(next.timestamp_millis() as f64 / 1000.0);
895                }
896            }
897        }
898
899        Ok(AwsResponse::ok_json(response))
900    }
901
902    fn list_secrets(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
903        let body = req.json_body();
904        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
905        validate_optional_range_i64("maxResults", body["MaxResults"].as_i64(), 1, 100)?;
906        validate_optional_enum("sortBy", body["SortBy"].as_str(), &["name", "created-date"])?;
907        validate_optional_enum("sortOrder", body["SortOrder"].as_str(), &["asc", "desc"])?;
908        let max_results = body["MaxResults"].as_i64().unwrap_or(100) as usize;
909        let next_token = body["NextToken"].as_str();
910        let filters = body["Filters"].as_array();
911        let include_deleted = body["IncludePlannedDeletion"].as_bool().unwrap_or(false);
912
913        // Validate filters
914        if let Some(filters) = filters {
915            for filter in filters {
916                let key = filter["Key"].as_str().unwrap_or("");
917                let values = filter["Values"].as_array();
918
919                if key.is_empty() {
920                    return Err(AwsServiceError::aws_error(
921                        StatusCode::BAD_REQUEST,
922                        "InvalidParameterException",
923                        "Invalid filter key",
924                    ));
925                }
926
927                let valid_keys = [
928                    "all",
929                    "name",
930                    "tag-key",
931                    "description",
932                    "tag-value",
933                    "owning-service",
934                    "primary-region",
935                ];
936                if !valid_keys.contains(&key) {
937                    return Err(AwsServiceError::aws_error(
938                        StatusCode::BAD_REQUEST,
939                        "ValidationException",
940                        format!(
941                            "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]",
942                            key
943                        ),
944                    ));
945                }
946
947                if values.is_none() || values.unwrap().is_empty() {
948                    return Err(AwsServiceError::aws_error(
949                        StatusCode::BAD_REQUEST,
950                        "InvalidParameterException",
951                        format!("Invalid filter values for key: {key}"),
952                    ));
953                }
954            }
955        }
956
957        let accounts = self.state.read();
958        let empty = SecretsManagerState::new(&req.account_id, &req.region);
959        let state = accounts.get(&req.account_id).unwrap_or(&empty);
960
961        let mut secrets: Vec<&Secret> = state
962            .secrets
963            .values()
964            .filter(|s| {
965                // Exclude deleted unless IncludePlannedDeletion
966                if s.deleted && !include_deleted {
967                    return false;
968                }
969
970                if let Some(filters) = filters {
971                    for filter in filters {
972                        let key = filter["Key"].as_str().unwrap_or("");
973                        let values: Vec<&str> = filter["Values"]
974                            .as_array()
975                            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
976                            .unwrap_or_default();
977
978                        let matches = match key {
979                            "name" => filter_name(s, &values),
980                            "description" => filter_description(s, &values),
981                            "tag-key" => filter_tag_key(s, &values),
982                            "tag-value" => filter_tag_value(s, &values),
983                            "all" => filter_all(s, &values),
984                            "owning-service" => false,
985                            "primary-region" => false,
986                            _ => true,
987                        };
988
989                        if !matches {
990                            return false;
991                        }
992                    }
993                }
994                true
995            })
996            .collect();
997        secrets.sort_by_key(|a| a.created_at);
998
999        // Simple pagination with name-based token
1000        let start_idx = if let Some(token) = next_token {
1001            secrets.iter().position(|s| s.name == token).unwrap_or(0)
1002        } else {
1003            0
1004        };
1005
1006        let page: Vec<Value> = secrets
1007            .iter()
1008            .skip(start_idx)
1009            .take(max_results)
1010            .map(|s| {
1011                // AWS always echoes `Description` and `SecretVersionsToStages`
1012                // on every SecretListEntry. The documented `@examples` for
1013                // ListSecrets relies on the fields being present even when
1014                // empty, and SDK consumers index into them unconditionally.
1015                let mut version_ids_to_stages: serde_json::Map<String, Value> =
1016                    serde_json::Map::new();
1017                for (vid, version) in &s.versions {
1018                    version_ids_to_stages.insert(vid.clone(), json!(version.stages));
1019                }
1020                let mut entry = json!({
1021                    "ARN": s.arn,
1022                    "Name": s.name,
1023                    "CreatedDate": s.created_at.timestamp_millis() as f64 / 1000.0,
1024                    "LastChangedDate": s.last_changed_at.timestamp_millis() as f64 / 1000.0,
1025                    "Description": s.description.clone().unwrap_or_default(),
1026                    "SecretVersionsToStages": Value::Object(version_ids_to_stages),
1027                });
1028
1029                if s.tags_ever_set || !s.tags.is_empty() {
1030                    entry["Tags"] = json!(tags_to_json(&s.tags));
1031                }
1032
1033                if let Some(ref kms) = s.kms_key_id {
1034                    entry["KmsKeyId"] = json!(kms);
1035                }
1036                if s.deleted {
1037                    entry["DeletedDate"] = json!(s
1038                        .deletion_date
1039                        .map(|d| d.timestamp_millis() as f64 / 1000.0));
1040                }
1041                entry
1042            })
1043            .collect();
1044
1045        let has_more = start_idx + max_results < secrets.len();
1046        let mut response = json!({
1047            "SecretList": page,
1048        });
1049        if has_more {
1050            if let Some(next) = secrets.get(start_idx + max_results) {
1051                response["NextToken"] = json!(next.name);
1052            }
1053        }
1054
1055        Ok(AwsResponse::ok_json(response))
1056    }
1057
1058    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1059        let body = req.json_body();
1060        let secret_id = require_secret_id(&body)?;
1061
1062        let new_tags = parse_tags(&body["Tags"]);
1063
1064        let mut accounts = self.state.write();
1065        let state = accounts.get_or_create(&req.account_id);
1066        let secret = self.find_secret_mut(state, &secret_id)?;
1067
1068        if !new_tags.is_empty() {
1069            secret.tags_ever_set = true;
1070        }
1071        for (k, v) in new_tags {
1072            // Update existing tag or add new one
1073            if let Some(existing) = secret.tags.iter_mut().find(|(ek, _)| *ek == k) {
1074                existing.1 = v;
1075            } else {
1076                secret.tags.push((k, v));
1077            }
1078        }
1079
1080        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1081    }
1082
1083    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1084        let body = req.json_body();
1085        let secret_id = require_secret_id(&body)?;
1086
1087        let tag_keys: Vec<String> = body["TagKeys"]
1088            .as_array()
1089            .map(|arr| {
1090                arr.iter()
1091                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
1092                    .collect()
1093            })
1094            .unwrap_or_default();
1095
1096        let mut accounts = self.state.write();
1097        let state = accounts.get_or_create(&req.account_id);
1098        let secret = self.find_secret_mut(state, &secret_id)?;
1099
1100        secret.tags.retain(|(k, _)| !tag_keys.contains(k));
1101
1102        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1103    }
1104
1105    fn list_secret_version_ids(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1106        let body = req.json_body();
1107        let secret_id = require_secret_id(&body)?;
1108        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
1109        validate_optional_range_i64("maxResults", body["MaxResults"].as_i64(), 1, 100)?;
1110        let max_results = body["MaxResults"].as_i64().unwrap_or(100) as usize;
1111        let next_token = body["NextToken"].as_str();
1112
1113        let accounts = self.state.read();
1114        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1115        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1116        let secret = self.find_secret_ref(state, &secret_id)?;
1117
1118        // Stable order so the NextToken (a version id) resumes deterministically:
1119        // newest first by CreatedDate, version id as a tiebreaker.
1120        let mut versions: Vec<&_> = secret.versions.values().collect();
1121        versions.sort_by(|a, b| {
1122            b.created_at
1123                .cmp(&a.created_at)
1124                .then_with(|| a.version_id.cmp(&b.version_id))
1125        });
1126
1127        let start_idx = if let Some(token) = next_token {
1128            versions
1129                .iter()
1130                .position(|v| v.version_id == token)
1131                .unwrap_or(versions.len())
1132        } else {
1133            0
1134        };
1135
1136        let page: Vec<Value> = versions
1137            .iter()
1138            .skip(start_idx)
1139            .take(max_results)
1140            .map(|v| {
1141                json!({
1142                    "VersionId": v.version_id,
1143                    "VersionStages": v.stages,
1144                    "CreatedDate": v.created_at.timestamp_millis() as f64 / 1000.0,
1145                })
1146            })
1147            .collect();
1148
1149        let mut response = json!({
1150            "ARN": secret.arn,
1151            "Name": secret.name,
1152            "Versions": page,
1153        });
1154        if start_idx + max_results < versions.len() {
1155            if let Some(next) = versions.get(start_idx + max_results) {
1156                response["NextToken"] = json!(next.version_id);
1157            }
1158        }
1159
1160        Ok(AwsResponse::ok_json(response))
1161    }
1162
1163    fn get_random_password(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1164        let body = req.json_body();
1165        let length = body["PasswordLength"].as_i64().unwrap_or(32) as usize;
1166
1167        if length < 4 {
1168            return Err(AwsServiceError::aws_error(
1169                StatusCode::BAD_REQUEST,
1170                "InvalidParameterException",
1171                "InvalidParameterException",
1172            ));
1173        }
1174        if length > 4096 {
1175            return Err(AwsServiceError::aws_error(
1176                StatusCode::BAD_REQUEST,
1177                "InvalidParameterValue",
1178                "InvalidParameterValue",
1179            ));
1180        }
1181
1182        let exclude_lowercase = body["ExcludeLowercase"].as_bool().unwrap_or(false);
1183        let exclude_uppercase = body["ExcludeUppercase"].as_bool().unwrap_or(false);
1184        let exclude_numbers = body["ExcludeNumbers"].as_bool().unwrap_or(false);
1185        let exclude_punctuation = body["ExcludePunctuation"].as_bool().unwrap_or(false);
1186        let include_space = body["IncludeSpace"].as_bool().unwrap_or(false);
1187        let require_each = body["RequireEachIncludedType"].as_bool().unwrap_or(true);
1188        validate_optional_string_length(
1189            "excludeCharacters",
1190            body["ExcludeCharacters"].as_str(),
1191            0,
1192            4096,
1193        )?;
1194        let exclude_chars = body["ExcludeCharacters"].as_str().unwrap_or("").to_string();
1195
1196        let lowercase = "abcdefghijklmnopqrstuvwxyz";
1197        let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1198        let digits = "0123456789";
1199        let punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
1200
1201        let mut char_pool = String::new();
1202        let mut required_chars: Vec<String> = Vec::new();
1203
1204        if !exclude_lowercase {
1205            let filtered: String = lowercase
1206                .chars()
1207                .filter(|c| !exclude_chars.contains(*c))
1208                .collect();
1209            if !filtered.is_empty() {
1210                required_chars.push(filtered.clone());
1211                char_pool.push_str(&filtered);
1212            }
1213        }
1214        if !exclude_uppercase {
1215            let filtered: String = uppercase
1216                .chars()
1217                .filter(|c| !exclude_chars.contains(*c))
1218                .collect();
1219            if !filtered.is_empty() {
1220                required_chars.push(filtered.clone());
1221                char_pool.push_str(&filtered);
1222            }
1223        }
1224        if !exclude_numbers {
1225            let filtered: String = digits
1226                .chars()
1227                .filter(|c| !exclude_chars.contains(*c))
1228                .collect();
1229            if !filtered.is_empty() {
1230                required_chars.push(filtered.clone());
1231                char_pool.push_str(&filtered);
1232            }
1233        }
1234        if !exclude_punctuation {
1235            let filtered: String = punctuation
1236                .chars()
1237                .filter(|c| !exclude_chars.contains(*c))
1238                .collect();
1239            if !filtered.is_empty() {
1240                required_chars.push(filtered.clone());
1241                char_pool.push_str(&filtered);
1242            }
1243        }
1244        if include_space && !exclude_chars.contains(' ') {
1245            char_pool.push(' ');
1246        }
1247
1248        if char_pool.is_empty() {
1249            return Err(AwsServiceError::aws_error(
1250                StatusCode::BAD_REQUEST,
1251                "InvalidParameterException",
1252                "InvalidParameterException",
1253            ));
1254        }
1255
1256        let pool_bytes: Vec<char> = char_pool.chars().collect();
1257        let mut password = String::with_capacity(length);
1258
1259        // Use simple random generation
1260        if require_each {
1261            // First, ensure at least one character from each required category
1262            for category in &required_chars {
1263                let chars: Vec<char> = category.chars().collect();
1264                let idx = simple_random() % chars.len();
1265                password.push(chars[idx]);
1266            }
1267            if include_space && !exclude_chars.contains(' ') {
1268                password.push(' ');
1269            }
1270        }
1271
1272        // Fill the rest randomly
1273        while password.len() < length {
1274            let idx = simple_random() % pool_bytes.len();
1275            password.push(pool_bytes[idx]);
1276        }
1277
1278        // Shuffle the password (Fisher-Yates)
1279        let mut chars: Vec<char> = password.chars().collect();
1280        for i in (1..chars.len()).rev() {
1281            let j = simple_random() % (i + 1);
1282            chars.swap(i, j);
1283        }
1284        let password: String = chars.into_iter().take(length).collect();
1285
1286        let response = json!({
1287            "RandomPassword": password,
1288        });
1289
1290        Ok(AwsResponse::ok_json(response))
1291    }
1292
1293    fn rotate_secret(
1294        &self,
1295        req: &AwsRequest,
1296    ) -> Result<(AwsResponse, Option<RotationInvocation>), AwsServiceError> {
1297        let body = req.json_body();
1298        let secret_id = require_secret_id(&body)?;
1299
1300        // Validate ClientRequestToken
1301        if let Some(token) = body["ClientRequestToken"].as_str() {
1302            if token.len() < 32 || token.len() > 64 {
1303                return Err(AwsServiceError::aws_error(
1304                    StatusCode::BAD_REQUEST,
1305                    "InvalidParameterException",
1306                    "ClientRequestToken must be 32-64 characters long.",
1307                ));
1308            }
1309        }
1310
1311        // Validate RotationLambdaARN
1312        if let Some(arn) = body["RotationLambdaARN"].as_str() {
1313            if arn.len() > 2048 {
1314                return Err(AwsServiceError::aws_error(
1315                    StatusCode::BAD_REQUEST,
1316                    "InvalidParameterException",
1317                    "RotationLambdaARN length must be less than or equal to 2048.",
1318                ));
1319            }
1320        }
1321
1322        // Validate RotationRules
1323        if let Some(rules) = body["RotationRules"].as_object() {
1324            if let Some(days) = rules.get("AutomaticallyAfterDays").and_then(|v| v.as_i64()) {
1325                if !(1..=1000).contains(&days) {
1326                    return Err(AwsServiceError::aws_error(
1327                        StatusCode::BAD_REQUEST,
1328                        "InvalidParameterException",
1329                        "RotationRules.AutomaticallyAfterDays must be within 1-1000.",
1330                    ));
1331                }
1332            }
1333        }
1334
1335        let mut accounts = self.state.write();
1336        let state = accounts.get_or_create(&req.account_id);
1337        let secret = self.find_secret_mut(state, &secret_id)?;
1338
1339        if secret.deleted {
1340            return Err(AwsServiceError::aws_error(
1341                StatusCode::BAD_REQUEST,
1342                "InvalidRequestException",
1343                "You can't perform this operation on the secret because it was marked for deletion.",
1344            ));
1345        }
1346
1347        // Set rotation config
1348        if let Some(lambda_arn) = body["RotationLambdaARN"].as_str() {
1349            secret.rotation_lambda_arn = Some(lambda_arn.to_string());
1350        }
1351
1352        if let Some(rules) = body["RotationRules"].as_object() {
1353            let days = rules.get("AutomaticallyAfterDays").and_then(|v| v.as_i64());
1354            secret.rotation_rules = Some(RotationRules {
1355                automatically_after_days: days,
1356                duration: rules
1357                    .get("Duration")
1358                    .and_then(|v| v.as_str())
1359                    .map(String::from),
1360                schedule_expression: rules
1361                    .get("ScheduleExpression")
1362                    .and_then(|v| v.as_str())
1363                    .map(String::from),
1364            });
1365        }
1366
1367        secret.rotation_enabled = Some(true);
1368        let now = Utc::now();
1369        secret.last_rotated_at = Some(now);
1370        secret.last_changed_at = now;
1371
1372        let version_id = body["ClientRequestToken"]
1373            .as_str()
1374            .map(|s| s.to_string())
1375            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
1376
1377        let has_lambda =
1378            body["RotationLambdaARN"].as_str().is_some() || secret.rotation_lambda_arn.is_some();
1379        let lambda_arn = secret.rotation_lambda_arn.clone();
1380
1381        // If the secret has a value, perform rotation
1382        let mut invocation = None;
1383        if let Some(current_vid) = secret.current_version_id.clone() {
1384            let current_value = secret.versions.get(&current_vid).cloned();
1385
1386            if let Some(cv) = current_value {
1387                if has_lambda {
1388                    // With Lambda: do NOT pre-create the AWSPENDING version. The
1389                    // rotation Lambda is responsible for putting the new value via
1390                    // PutSecretValue with VersionStages=[AWSPENDING] during the
1391                    // createSecret step (matching real AWS Secrets Manager behavior).
1392
1393                    // Schedule Lambda invocation
1394                    if let Some(ref arn) = lambda_arn {
1395                        invocation = Some(RotationInvocation {
1396                            lambda_arn: arn.clone(),
1397                            secret_id: secret.arn.clone(),
1398                            client_request_token: version_id.clone(),
1399                        });
1400                    }
1401                } else {
1402                    // Without Lambda: simple rotation - new version becomes AWSCURRENT
1403                    // Move old version to AWSPREVIOUS
1404                    if let Some(old_v) = secret.versions.get_mut(&current_vid) {
1405                        old_v.stages.retain(|s| s != "AWSCURRENT");
1406                        if !old_v.stages.contains(&"AWSPREVIOUS".to_string()) {
1407                            old_v.stages.push("AWSPREVIOUS".to_string());
1408                        }
1409                    }
1410                    let version = SecretVersion {
1411                        version_id: version_id.clone(),
1412                        secret_string: cv.secret_string.clone(),
1413                        secret_binary: cv.secret_binary.clone(),
1414                        stages: vec!["AWSCURRENT".to_string()],
1415                        created_at: now,
1416                    };
1417                    secret.versions.insert(version_id.clone(), version);
1418                    secret.current_version_id = Some(version_id.clone());
1419                }
1420            }
1421        }
1422
1423        let response = json!({
1424            "ARN": secret.arn,
1425            "Name": secret.name,
1426            "VersionId": version_id,
1427        });
1428
1429        Ok((AwsResponse::ok_json(response), invocation))
1430    }
1431
1432    fn cancel_rotate_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1433        let body = req.json_body();
1434        let secret_id = require_secret_id(&body)?;
1435
1436        let mut accounts = self.state.write();
1437        let state = accounts.get_or_create(&req.account_id);
1438        let secret = self.find_secret_mut(state, &secret_id)?;
1439
1440        if secret.deleted {
1441            return Err(AwsServiceError::aws_error(
1442                StatusCode::BAD_REQUEST,
1443                "InvalidRequestException",
1444                "You can't perform this operation on the secret because it was marked for deletion.",
1445            ));
1446        }
1447
1448        if secret.rotation_enabled != Some(true) {
1449            return Err(AwsServiceError::aws_error(
1450                StatusCode::BAD_REQUEST,
1451                "InvalidRequestException",
1452                "You can't cancel rotation for a secret that does not have rotation enabled.",
1453            ));
1454        }
1455
1456        secret.rotation_enabled = Some(false);
1457
1458        let response = json!({
1459            "ARN": secret.arn,
1460            "Name": secret.name,
1461        });
1462
1463        Ok(AwsResponse::ok_json(response))
1464    }
1465
1466    fn update_secret_version_stage(
1467        &self,
1468        req: &AwsRequest,
1469    ) -> Result<AwsResponse, AwsServiceError> {
1470        let body = req.json_body();
1471        let secret_id = require_secret_id(&body)?;
1472        let version_stage = body["VersionStage"]
1473            .as_str()
1474            .ok_or_else(|| {
1475                AwsServiceError::aws_error(
1476                    StatusCode::BAD_REQUEST,
1477                    "InvalidParameterException",
1478                    "VersionStage is required",
1479                )
1480            })?
1481            .to_string();
1482        validate_string_length("versionStage", &version_stage, 1, 256)?;
1483        validate_optional_string_length(
1484            "removeFromVersionId",
1485            body["RemoveFromVersionId"].as_str(),
1486            32,
1487            64,
1488        )?;
1489        validate_optional_string_length(
1490            "moveToVersionId",
1491            body["MoveToVersionId"].as_str(),
1492            32,
1493            64,
1494        )?;
1495
1496        let move_to = body["MoveToVersionId"].as_str().map(|s| s.to_string());
1497        let remove_from = body["RemoveFromVersionId"].as_str().map(|s| s.to_string());
1498
1499        let mut accounts = self.state.write();
1500        let state = accounts.get_or_create(&req.account_id);
1501        let secret = self.find_secret_mut(state, &secret_id)?;
1502
1503        // Validate: if moving AWSCURRENT, must specify RemoveFromVersionId
1504        if version_stage == "AWSCURRENT" && move_to.is_some() && remove_from.is_none() {
1505            // Find the version that currently has AWSCURRENT
1506            let current_holder = secret
1507                .versions
1508                .iter()
1509                .find(|(_, v)| v.stages.contains(&"AWSCURRENT".to_string()))
1510                .map(|(id, _)| id.clone());
1511
1512            if let Some(current_vid) = current_holder {
1513                return Err(AwsServiceError::aws_error(
1514                    StatusCode::BAD_REQUEST,
1515                    "InvalidParameterException",
1516                    format!(
1517                        "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."
1518                    ),
1519                ));
1520            }
1521        }
1522
1523        // Remove stage from specified version
1524        if let Some(ref remove_vid) = remove_from {
1525            if let Some(version) = secret.versions.get_mut(remove_vid) {
1526                version.stages.retain(|s| s != &version_stage);
1527                // If moving AWSCURRENT away, add AWSPREVIOUS and remove from others
1528                if version_stage == "AWSCURRENT" {
1529                    // Remove AWSPREVIOUS from all other versions first
1530                    for (id, v) in secret.versions.iter_mut() {
1531                        if id != remove_vid {
1532                            v.stages.retain(|s| s != "AWSPREVIOUS");
1533                        }
1534                    }
1535                    // Now add AWSPREVIOUS to the version losing AWSCURRENT
1536                    if let Some(v) = secret.versions.get_mut(remove_vid) {
1537                        if !v.stages.contains(&"AWSPREVIOUS".to_string()) {
1538                            v.stages.push("AWSPREVIOUS".to_string());
1539                        }
1540                    }
1541                }
1542            }
1543        }
1544
1545        // Add stage to specified version
1546        if let Some(ref move_vid) = move_to {
1547            if let Some(version) = secret.versions.get_mut(move_vid) {
1548                if !version.stages.contains(&version_stage) {
1549                    version.stages.push(version_stage.clone());
1550                }
1551            }
1552            // Update current_version_id if we moved AWSCURRENT
1553            if version_stage == "AWSCURRENT" {
1554                secret.current_version_id = Some(move_vid.clone());
1555            }
1556        }
1557
1558        let response = json!({
1559            "ARN": secret.arn,
1560            "Name": secret.name,
1561        });
1562
1563        Ok(AwsResponse::ok_json(response))
1564    }
1565
1566    fn batch_get_secret_value(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1567        let body = req.json_body();
1568        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
1569        let secret_id_list = body["SecretIdList"].as_array();
1570        let filters = body["Filters"].as_array();
1571        let max_results = body.get("MaxResults").and_then(|v| v.as_i64());
1572
1573        // Validate: can't use both SecretIdList and Filters
1574        if secret_id_list.is_some() && filters.is_some() {
1575            return Err(AwsServiceError::aws_error(
1576                StatusCode::BAD_REQUEST,
1577                "InvalidParameterException",
1578                "Either 'SecretIdList' or 'Filters' must be provided, but not both.",
1579            ));
1580        }
1581
1582        // Validate: MaxResults requires Filters
1583        if max_results.is_some() && filters.is_none() {
1584            return Err(AwsServiceError::aws_error(
1585                StatusCode::BAD_REQUEST,
1586                "InvalidParameterException",
1587                "'Filters' not specified. 'Filters' must also be specified when 'MaxResults' is provided.",
1588            ));
1589        }
1590
1591        let accounts = self.state.read();
1592        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1593        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1594        let mut secret_values: Vec<Value> = Vec::new();
1595        let mut errors: Vec<Value> = Vec::new();
1596
1597        if let Some(id_list) = secret_id_list {
1598            for id_val in id_list {
1599                let sid = id_val.as_str().unwrap_or("");
1600                match self.find_secret_ref(state, sid) {
1601                    Ok(secret) => {
1602                        if secret.deleted {
1603                            errors.push(json!({
1604                                "SecretId": sid,
1605                                "ErrorCode": "InvalidRequestException",
1606                                "Message": "Secret is currently marked deleted. Secret can be recovered with RestoreSecret. Secret is currently marked deleted.",
1607                            }));
1608                        } else if let Some(ref current_vid) = secret.current_version_id {
1609                            if let Some(version) = secret.versions.get(current_vid) {
1610                                let mut entry = json!({
1611                                    "ARN": secret.arn,
1612                                    "Name": secret.name,
1613                                    "VersionId": version.version_id,
1614                                    "VersionStages": version.stages,
1615                                    "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
1616                                });
1617                                if let Some(ref s) = version.secret_string {
1618                                    // Decrypt the same way GetSecretValue does;
1619                                    // pushing the raw stored value leaked
1620                                    // ciphertext when a KMS hook + KmsKeyId were
1621                                    // configured (bug-audit 2026-06-20, 1.10).
1622                                    let plaintext = self
1623                                        .maybe_decrypt_secret_string(
1624                                            &req.account_id,
1625                                            &secret.arn,
1626                                            secret.kms_key_id.as_deref(),
1627                                            Some(s.as_str()),
1628                                        )
1629                                        .unwrap_or_else(|| s.clone());
1630                                    entry["SecretString"] = json!(plaintext);
1631                                }
1632                                if let Some(ref b) = version.secret_binary {
1633                                    entry["SecretBinary"] = json!(base64_encode(b));
1634                                }
1635                                secret_values.push(entry);
1636                            } else {
1637                                errors.push(json!({
1638                                    "SecretId": sid,
1639                                    "ErrorCode": "ResourceNotFoundException",
1640                                    "Message": "Secrets Manager can't find the specified secret.",
1641                                }));
1642                            }
1643                        } else {
1644                            errors.push(json!({
1645                                "SecretId": sid,
1646                                "ErrorCode": "ResourceNotFoundException",
1647                                "Message": "Secrets Manager can't find the specified secret.",
1648                            }));
1649                        }
1650                    }
1651                    Err(_) => {
1652                        errors.push(json!({
1653                            "SecretId": sid,
1654                            "ErrorCode": "ResourceNotFoundException",
1655                            "Message": "Secrets Manager can't find the specified secret.",
1656                        }));
1657                    }
1658                }
1659            }
1660        } else if let Some(filters) = filters {
1661            // Get secrets matching filters
1662            let matching: Vec<&Secret> = state
1663                .secrets
1664                .values()
1665                .filter(|s| {
1666                    if s.deleted {
1667                        return false;
1668                    }
1669                    for filter in filters {
1670                        let key = filter["Key"].as_str().unwrap_or("");
1671                        let values: Vec<&str> = filter["Values"]
1672                            .as_array()
1673                            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
1674                            .unwrap_or_default();
1675                        let matches = match key {
1676                            "name" => filter_name(s, &values),
1677                            "description" => filter_description(s, &values),
1678                            "tag-key" => filter_tag_key(s, &values),
1679                            "tag-value" => filter_tag_value(s, &values),
1680                            "all" => filter_all(s, &values),
1681                            _ => true,
1682                        };
1683                        if !matches {
1684                            return false;
1685                        }
1686                    }
1687                    true
1688                })
1689                .collect();
1690
1691            let limit = max_results.unwrap_or(100) as usize;
1692            let mut no_value_found = false;
1693            let mut matching = matching;
1694            matching.sort_by(|a, b| a.name.cmp(&b.name));
1695
1696            for secret in matching.iter().take(limit) {
1697                if let Some(ref current_vid) = secret.current_version_id {
1698                    if let Some(version) = secret.versions.get(current_vid) {
1699                        let mut entry = json!({
1700                            "ARN": secret.arn,
1701                            "Name": secret.name,
1702                            "VersionId": version.version_id,
1703                            "VersionStages": version.stages,
1704                            "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
1705                        });
1706                        if let Some(ref s) = version.secret_string {
1707                            // Decrypt like GetSecretValue; the raw stored value
1708                            // is ciphertext under a configured KMS hook (1.10).
1709                            let plaintext = self
1710                                .maybe_decrypt_secret_string(
1711                                    &req.account_id,
1712                                    &secret.arn,
1713                                    secret.kms_key_id.as_deref(),
1714                                    Some(s.as_str()),
1715                                )
1716                                .unwrap_or_else(|| s.clone());
1717                            entry["SecretString"] = json!(plaintext);
1718                        }
1719                        if let Some(ref b) = version.secret_binary {
1720                            entry["SecretBinary"] = json!(base64_encode(b));
1721                        }
1722                        secret_values.push(entry);
1723                    } else {
1724                        no_value_found = true;
1725                    }
1726                } else {
1727                    no_value_found = true;
1728                }
1729            }
1730
1731            if no_value_found && secret_values.is_empty() {
1732                return Err(AwsServiceError::aws_error(
1733                    StatusCode::NOT_FOUND,
1734                    "ResourceNotFoundException",
1735                    "Secrets Manager can't find the specified secret.",
1736                ));
1737            }
1738        }
1739
1740        let mut response = json!({
1741            "SecretValues": secret_values,
1742            "Errors": errors,
1743        });
1744
1745        // Remove empty arrays
1746        if errors.is_empty() {
1747            response.as_object_mut().unwrap().remove("Errors");
1748        }
1749
1750        Ok(AwsResponse::ok_json(response))
1751    }
1752
1753    fn get_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1754        let body = req.json_body();
1755        let secret_id = require_secret_id(&body)?;
1756
1757        let accounts = self.state.read();
1758        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1759        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1760        let secret = self.find_secret_ref(state, &secret_id)?;
1761
1762        // Real AWS omits ResourcePolicy when none is attached; terraform
1763        // provider and the SDK choke on an empty-string policy.
1764        let mut response = json!({
1765            "ARN": secret.arn,
1766            "Name": secret.name,
1767        });
1768        if let Some(ref policy) = secret.resource_policy {
1769            response["ResourcePolicy"] = json!(policy);
1770        }
1771
1772        Ok(AwsResponse::ok_json(response))
1773    }
1774
1775    fn validate_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1776        let body = req.json_body();
1777        validate_optional_string_length("secretId", body["SecretId"].as_str(), 1, 2048)?;
1778        validate_required("ResourcePolicy", &body["ResourcePolicy"])?;
1779        let policy_str = body["ResourcePolicy"].as_str().ok_or_else(|| {
1780            AwsServiceError::aws_error(
1781                StatusCode::BAD_REQUEST,
1782                "InvalidParameterException",
1783                "ResourcePolicy must be a string",
1784            )
1785        })?;
1786        validate_string_length("resourcePolicy", policy_str, 1, 20480)?;
1787
1788        // If SecretId is provided, verify the secret exists
1789        if let Some(secret_id) = body["SecretId"].as_str() {
1790            let accounts = self.state.read();
1791            let empty = SecretsManagerState::new(&req.account_id, &req.region);
1792            let state = accounts.get(&req.account_id).unwrap_or(&empty);
1793            self.find_secret_key(state, secret_id)?;
1794        }
1795
1796        let response = json!({
1797            "PolicyValidationPassed": true,
1798            "ValidationErrors": [],
1799        });
1800        Ok(AwsResponse::ok_json(response))
1801    }
1802
1803    fn put_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1804        let body = req.json_body();
1805        let secret_id = require_secret_id(&body)?;
1806        validate_required("ResourcePolicy", &body["ResourcePolicy"])?;
1807        validate_optional_string_length(
1808            "resourcePolicy",
1809            body["ResourcePolicy"].as_str(),
1810            1,
1811            20480,
1812        )?;
1813        let policy = body["ResourcePolicy"].as_str().map(|s| s.to_string());
1814
1815        let mut accounts = self.state.write();
1816        let state = accounts.get_or_create(&req.account_id);
1817        let secret = self.find_secret_mut(state, &secret_id)?;
1818        secret.resource_policy = policy;
1819
1820        let response = json!({
1821            "ARN": secret.arn,
1822            "Name": secret.name,
1823        });
1824
1825        Ok(AwsResponse::ok_json(response))
1826    }
1827
1828    fn delete_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1829        let body = req.json_body();
1830        let secret_id = require_secret_id(&body)?;
1831
1832        let mut accounts = self.state.write();
1833        let state = accounts.get_or_create(&req.account_id);
1834        let secret = self.find_secret_mut(state, &secret_id)?;
1835        secret.resource_policy = None;
1836
1837        let response = json!({
1838            "ARN": secret.arn,
1839            "Name": secret.name,
1840        });
1841
1842        Ok(AwsResponse::ok_json(response))
1843    }
1844
1845    fn replicate_secret_to_regions(
1846        &self,
1847        req: &AwsRequest,
1848    ) -> Result<AwsResponse, AwsServiceError> {
1849        let body = req.json_body();
1850        let secret_id = require_secret_id(&body)?;
1851        // AddReplicaRegions[].Region — the regions to replicate into.
1852        let add_regions: Vec<String> = body["AddReplicaRegions"]
1853            .as_array()
1854            .map(|arr| {
1855                arr.iter()
1856                    .filter_map(|r| r["Region"].as_str().map(String::from))
1857                    .collect()
1858            })
1859            .unwrap_or_default();
1860
1861        let mut accounts = self.state.write();
1862        let state = accounts.get_or_create(&req.account_id);
1863        let secret = self.find_secret_mut(state, &secret_id)?;
1864        for region in add_regions {
1865            if !secret.replica_regions.contains(&region) {
1866                secret.replica_regions.push(region);
1867            }
1868        }
1869        let response = json!({
1870            "ARN": secret.arn,
1871            "ReplicationStatus": replication_status_json(&secret.replica_regions),
1872        });
1873        Ok(AwsResponse::ok_json(response))
1874    }
1875
1876    fn remove_regions_from_replication(
1877        &self,
1878        req: &AwsRequest,
1879    ) -> Result<AwsResponse, AwsServiceError> {
1880        let body = req.json_body();
1881        let secret_id = require_secret_id(&body)?;
1882        let remove_regions: Vec<String> = body["RemoveReplicaRegions"]
1883            .as_array()
1884            .map(|arr| {
1885                arr.iter()
1886                    .filter_map(|r| r.as_str().map(String::from))
1887                    .collect()
1888            })
1889            .unwrap_or_default();
1890
1891        let mut accounts = self.state.write();
1892        let state = accounts.get_or_create(&req.account_id);
1893        let secret = self.find_secret_mut(state, &secret_id)?;
1894        secret
1895            .replica_regions
1896            .retain(|r| !remove_regions.contains(r));
1897        let response = json!({
1898            "ARN": secret.arn,
1899            "ReplicationStatus": replication_status_json(&secret.replica_regions),
1900        });
1901        Ok(AwsResponse::ok_json(response))
1902    }
1903
1904    fn stop_replication_to_replica(
1905        &self,
1906        req: &AwsRequest,
1907    ) -> Result<AwsResponse, AwsServiceError> {
1908        let body = req.json_body();
1909        let secret_id = require_secret_id(&body)?;
1910
1911        let accounts = self.state.read();
1912        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1913        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1914        let secret = self.find_secret_ref(state, &secret_id)?;
1915
1916        let response = json!({
1917            "ARN": secret.arn,
1918        });
1919        Ok(AwsResponse::ok_json(response))
1920    }
1921
1922    /// Find a secret by name, full ARN, or partial ARN (mutable).
1923    fn find_secret_mut<'a>(
1924        &self,
1925        state: &'a mut crate::state::SecretsManagerState,
1926        secret_id: &str,
1927    ) -> Result<&'a mut Secret, AwsServiceError> {
1928        let key = self.find_secret_key(state, secret_id)?;
1929        Ok(state.secrets.get_mut(&key).unwrap())
1930    }
1931
1932    fn find_secret_key(
1933        &self,
1934        state: &crate::state::SecretsManagerState,
1935        secret_id: &str,
1936    ) -> Result<String, AwsServiceError> {
1937        if state.secrets.contains_key(secret_id) {
1938            return Ok(secret_id.to_string());
1939        }
1940
1941        for secret in state.secrets.values() {
1942            if secret.arn == secret_id {
1943                return Ok(secret.name.clone());
1944            }
1945        }
1946
1947        if secret_id.starts_with("arn:aws:secretsmanager:") {
1948            for secret in state.secrets.values() {
1949                if secret.arn.starts_with(secret_id) {
1950                    return Ok(secret.name.clone());
1951                }
1952            }
1953        }
1954
1955        Err(AwsServiceError::aws_error(
1956            StatusCode::NOT_FOUND,
1957            "ResourceNotFoundException",
1958            "Secrets Manager can't find the specified secret.",
1959        ))
1960    }
1961
1962    /// Find a secret by name, full ARN, or partial ARN (immutable).
1963    fn find_secret_ref<'a>(
1964        &self,
1965        state: &'a crate::state::SecretsManagerState,
1966        secret_id: &str,
1967    ) -> Result<&'a Secret, AwsServiceError> {
1968        if let Some(secret) = state.secrets.get(secret_id) {
1969            return Ok(secret);
1970        }
1971
1972        // Search by full ARN
1973        for secret in state.secrets.values() {
1974            if secret.arn == secret_id {
1975                return Ok(secret);
1976            }
1977        }
1978
1979        // Search by partial ARN
1980        if secret_id.starts_with("arn:aws:secretsmanager:") {
1981            for secret in state.secrets.values() {
1982                if secret.arn.starts_with(secret_id) {
1983                    return Ok(secret);
1984                }
1985            }
1986        }
1987
1988        Err(AwsServiceError::aws_error(
1989            StatusCode::NOT_FOUND,
1990            "ResourceNotFoundException",
1991            "Secrets Manager can't find the specified secret.",
1992        ))
1993    }
1994}
1995
1996/// Persist the current Secrets Manager state as a snapshot. Offloads the
1997/// serialization and blocking file write to the Tokio blocking pool. Noop when
1998/// `store` is `None` (memory mode). Shared by
1999/// `SecretsManagerService::save_snapshot` and
2000/// the CloudFormation provisioner's post-provision persist hook so both route
2001/// through the same serialize-and-write path.
2002pub async fn save_secretsmanager_snapshot(
2003    state: &SharedSecretsManagerState,
2004    store: Option<Arc<dyn SnapshotStore>>,
2005    lock: &AsyncMutex<()>,
2006) {
2007    let Some(store) = store else {
2008        return;
2009    };
2010    let _guard = lock.lock().await;
2011    let snapshot = SecretsManagerSnapshot {
2012        schema_version: SECRETSMANAGER_SNAPSHOT_SCHEMA_VERSION,
2013        state: None,
2014        accounts: Some(state.read().clone()),
2015    };
2016    let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
2017        let bytes = serde_json::to_vec(&snapshot)
2018            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
2019        store.save(&bytes)
2020    })
2021    .await;
2022    match join {
2023        Ok(Ok(())) => {}
2024        Ok(Err(err)) => tracing::error!(%err, "failed to write secretsmanager snapshot"),
2025        Err(err) => tracing::error!(%err, "secretsmanager snapshot task panicked"),
2026    }
2027}
2028
2029/// Parsed + validated inputs for `CreateSecret`.
2030struct CreateSecretInput {
2031    name: String,
2032    client_request_token: Option<String>,
2033    description: Option<String>,
2034    kms_key_id: Option<String>,
2035    secret_string: Option<String>,
2036    secret_binary: Option<Vec<u8>>,
2037    tags: Vec<(String, String)>,
2038}
2039
2040impl CreateSecretInput {
2041    fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
2042        validate_required("Name", &body["Name"])?;
2043        let name = body["Name"]
2044            .as_str()
2045            .ok_or_else(|| {
2046                AwsServiceError::aws_error(
2047                    StatusCode::BAD_REQUEST,
2048                    "InvalidParameterException",
2049                    "Name is required",
2050                )
2051            })?
2052            .to_string();
2053        validate_string_length("name", &name, 1, 512)?;
2054        validate_optional_string_length(
2055            "clientRequestToken",
2056            body["ClientRequestToken"].as_str(),
2057            32,
2058            64,
2059        )?;
2060        validate_optional_string_length("description", body["Description"].as_str(), 0, 2048)?;
2061        validate_optional_string_length("kmsKeyId", body["KmsKeyId"].as_str(), 0, 2048)?;
2062        validate_optional_string_length("secretString", body["SecretString"].as_str(), 1, 65536)?;
2063
2064        Ok(Self {
2065            name,
2066            client_request_token: body["ClientRequestToken"].as_str().map(|s| s.to_string()),
2067            description: body["Description"].as_str().map(|s| s.to_string()),
2068            kms_key_id: body["KmsKeyId"].as_str().map(|s| s.to_string()),
2069            secret_string: body["SecretString"].as_str().map(|s| s.to_string()),
2070            secret_binary: body["SecretBinary"].as_str().and_then(base64_decode),
2071            tags: parse_tags(&body["Tags"]),
2072        })
2073    }
2074}
2075
2076#[async_trait]
2077impl AwsService for SecretsManagerService {
2078    fn service_name(&self) -> &str {
2079        "secretsmanager"
2080    }
2081
2082    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2083        let mutates = is_mutating_action(req.action.as_str());
2084        let result = match req.action.as_str() {
2085            "CreateSecret" => self.create_secret(&req),
2086            "GetSecretValue" => self.get_secret_value(&req),
2087            "PutSecretValue" => self.put_secret_value(&req),
2088            "UpdateSecret" => self.update_secret(&req),
2089            "DeleteSecret" => self.delete_secret(&req),
2090            "RestoreSecret" => self.restore_secret(&req),
2091            "DescribeSecret" => self.describe_secret(&req),
2092            "ListSecrets" => self.list_secrets(&req),
2093            "TagResource" => self.tag_resource(&req),
2094            "UntagResource" => self.untag_resource(&req),
2095            "ListSecretVersionIds" => self.list_secret_version_ids(&req),
2096            "GetRandomPassword" => self.get_random_password(&req),
2097            "RotateSecret" => {
2098                let (response, invocation) = self.rotate_secret(&req)?;
2099                if let Some(inv) = invocation {
2100                    if let Some(ref bus) = self.delivery_bus {
2101                        let bus = bus.clone();
2102                        // AWS invokes the rotation Lambda asynchronously for each step.
2103                        tokio::spawn(async move {
2104                            for step in &["createSecret", "setSecret", "testSecret", "finishSecret"]
2105                            {
2106                                let payload = serde_json::json!({
2107                                    "SecretId": inv.secret_id,
2108                                    "ClientRequestToken": inv.client_request_token,
2109                                    "Step": step,
2110                                });
2111                                let payload_str = payload.to_string();
2112                                match bus.invoke_lambda(&inv.lambda_arn, &payload_str).await {
2113                                    Some(Ok(_)) => {}
2114                                    Some(Err(e)) => {
2115                                        tracing::warn!(
2116                                            step = step,
2117                                            error = %e,
2118                                            "rotation Lambda invocation failed"
2119                                        );
2120                                    }
2121                                    None => {
2122                                        tracing::warn!(
2123                                            lambda_arn = %inv.lambda_arn,
2124                                            step = step,
2125                                            "rotation Lambda delivery not configured; \
2126                                             Lambda invocation skipped"
2127                                        );
2128                                        break;
2129                                    }
2130                                }
2131                            }
2132                        });
2133                    }
2134                }
2135                Ok(response)
2136            }
2137            "CancelRotateSecret" => self.cancel_rotate_secret(&req),
2138            "UpdateSecretVersionStage" => self.update_secret_version_stage(&req),
2139            "BatchGetSecretValue" => self.batch_get_secret_value(&req),
2140            "GetResourcePolicy" => self.get_resource_policy(&req),
2141            "PutResourcePolicy" => self.put_resource_policy(&req),
2142            "DeleteResourcePolicy" => self.delete_resource_policy(&req),
2143            "ValidateResourcePolicy" => self.validate_resource_policy(&req),
2144            "ReplicateSecretToRegions" => self.replicate_secret_to_regions(&req),
2145            "RemoveRegionsFromReplication" => self.remove_regions_from_replication(&req),
2146            "StopReplicationToReplica" => self.stop_replication_to_replica(&req),
2147            _ => Err(AwsServiceError::action_not_implemented(
2148                "secretsmanager",
2149                &req.action,
2150            )),
2151        };
2152        if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
2153            self.save_snapshot().await;
2154        }
2155        result.map_err(remap_validation_error)
2156    }
2157
2158    fn supported_actions(&self) -> &[&str] {
2159        &[
2160            "CreateSecret",
2161            "GetSecretValue",
2162            "PutSecretValue",
2163            "UpdateSecret",
2164            "DeleteSecret",
2165            "RestoreSecret",
2166            "DescribeSecret",
2167            "ListSecrets",
2168            "TagResource",
2169            "UntagResource",
2170            "ListSecretVersionIds",
2171            "GetRandomPassword",
2172            "RotateSecret",
2173            "CancelRotateSecret",
2174            "UpdateSecretVersionStage",
2175            "BatchGetSecretValue",
2176            "GetResourcePolicy",
2177            "PutResourcePolicy",
2178            "DeleteResourcePolicy",
2179            "ValidateResourcePolicy",
2180            "ReplicateSecretToRegions",
2181            "RemoveRegionsFromReplication",
2182            "StopReplicationToReplica",
2183        ]
2184    }
2185}
2186
2187#[path = "service_helpers.rs"]
2188mod service_helpers;
2189pub(crate) use service_helpers::*;
2190
2191/// The shared `fakecloud_core::validation` helpers raise
2192/// `ValidationException`, but the Secrets Manager Smithy model does not
2193/// declare `ValidationException` on any operation — its declared input
2194/// error is `InvalidParameterException`. Translate at the dispatcher
2195/// boundary so the wire-level error code matches real AWS without
2196/// duplicating every validator.
2197/// Build the `ReplicationStatus` array from a secret's replica regions.
2198/// Each replica reports `InSync`, matching a healthy replication.
2199fn replication_status_json(regions: &[String]) -> Value {
2200    Value::Array(
2201        regions
2202            .iter()
2203            .map(|r| {
2204                json!({
2205                    "Region": r,
2206                    "Status": "InSync",
2207                    "StatusMessage": "Replication succeeded",
2208                })
2209            })
2210            .collect(),
2211    )
2212}
2213
2214fn remap_validation_error(err: AwsServiceError) -> AwsServiceError {
2215    match err {
2216        AwsServiceError::AwsError {
2217            status,
2218            code,
2219            message,
2220            extra_fields,
2221            headers,
2222        } if code == "ValidationException" => AwsServiceError::AwsError {
2223            status,
2224            code: "InvalidParameterException".to_string(),
2225            message,
2226            extra_fields,
2227            headers,
2228        },
2229        other => other,
2230    }
2231}
2232
2233/// Extract the owning account-id from an `arn:aws:secretsmanager:...:ACCOUNT:secret:...`
2234/// secret id. Returns `caller_account` when the input is a bare name
2235/// or a same-account ARN.
2236fn secret_owner_account(secret_id: &str, caller_account: &str) -> String {
2237    if !secret_id.starts_with("arn:aws:secretsmanager:") {
2238        return caller_account.to_string();
2239    }
2240    let parts: Vec<&str> = secret_id.splitn(7, ':').collect();
2241    if parts.len() < 5 {
2242        return caller_account.to_string();
2243    }
2244    let account = parts[4];
2245    if account.is_empty() {
2246        caller_account.to_string()
2247    } else {
2248        account.to_string()
2249    }
2250}
2251
2252/// Evaluate a Secrets Manager resource policy against a cross-account
2253/// caller. Empty policy implicitly denies (real AWS behaviour). Uses
2254/// the shared IAM evaluator so the same policy semantics apply
2255/// service-wide.
2256fn resource_policy_allows(policy_doc: &str, caller_account: &str, secret_arn: &str) -> bool {
2257    if policy_doc.is_empty() {
2258        return false;
2259    }
2260    use fakecloud_core::auth::{Principal, PrincipalType};
2261    use fakecloud_iam::evaluator::{evaluate, EvalRequest, PolicyDocument};
2262    let doc = PolicyDocument::parse(policy_doc);
2263    let principal_arn = Arn::global("iam", caller_account, "root").to_string();
2264    let principal = Principal {
2265        arn: principal_arn.clone(),
2266        user_id: principal_arn.clone(),
2267        account_id: caller_account.to_string(),
2268        principal_type: PrincipalType::User,
2269        source_identity: None,
2270        tags: None,
2271    };
2272    let req = EvalRequest {
2273        principal: &principal,
2274        action: "secretsmanager:GetSecretValue".to_string(),
2275        resource: secret_arn.to_string(),
2276        context: Default::default(),
2277    };
2278    matches!(
2279        evaluate(&[doc], &req),
2280        fakecloud_iam::evaluator::Decision::Allow
2281    )
2282}
2283
2284#[cfg(test)]
2285#[path = "service_tests.rs"]
2286mod tests;