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