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