Skip to main content

fakecloud_secretsmanager/
service.rs

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