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