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                let mut entry = json!({
996                    "ARN": s.arn,
997                    "Name": s.name,
998                    "CreatedDate": s.created_at.timestamp_millis() as f64 / 1000.0,
999                    "LastChangedDate": s.last_changed_at.timestamp_millis() as f64 / 1000.0,
1000                });
1001
1002                if !s.versions.is_empty() {
1003                    let mut version_ids_to_stages: serde_json::Map<String, Value> =
1004                        serde_json::Map::new();
1005                    for (vid, version) in &s.versions {
1006                        version_ids_to_stages.insert(vid.clone(), json!(version.stages));
1007                    }
1008                    entry["SecretVersionsToStages"] = Value::Object(version_ids_to_stages);
1009                }
1010
1011                if let Some(ref desc) = s.description {
1012                    if !desc.is_empty() {
1013                        entry["Description"] = json!(desc);
1014                    }
1015                }
1016
1017                if s.tags_ever_set || !s.tags.is_empty() {
1018                    entry["Tags"] = json!(tags_to_json(&s.tags));
1019                }
1020
1021                if let Some(ref kms) = s.kms_key_id {
1022                    entry["KmsKeyId"] = json!(kms);
1023                }
1024                if s.deleted {
1025                    entry["DeletedDate"] = json!(s
1026                        .deletion_date
1027                        .map(|d| d.timestamp_millis() as f64 / 1000.0));
1028                }
1029                entry
1030            })
1031            .collect();
1032
1033        let has_more = start_idx + max_results < secrets.len();
1034        let mut response = json!({
1035            "SecretList": page,
1036        });
1037        if has_more {
1038            if let Some(next) = secrets.get(start_idx + max_results) {
1039                response["NextToken"] = json!(next.name);
1040            }
1041        }
1042
1043        Ok(AwsResponse::ok_json(response))
1044    }
1045
1046    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1047        let body = req.json_body();
1048        let secret_id = require_secret_id(&body)?;
1049
1050        let new_tags = parse_tags(&body["Tags"]);
1051
1052        let mut accounts = self.state.write();
1053        let state = accounts.get_or_create(&req.account_id);
1054        let secret = self.find_secret_mut(state, &secret_id)?;
1055
1056        if !new_tags.is_empty() {
1057            secret.tags_ever_set = true;
1058        }
1059        for (k, v) in new_tags {
1060            // Update existing tag or add new one
1061            if let Some(existing) = secret.tags.iter_mut().find(|(ek, _)| *ek == k) {
1062                existing.1 = v;
1063            } else {
1064                secret.tags.push((k, v));
1065            }
1066        }
1067
1068        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1069    }
1070
1071    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1072        let body = req.json_body();
1073        let secret_id = require_secret_id(&body)?;
1074
1075        let tag_keys: Vec<String> = body["TagKeys"]
1076            .as_array()
1077            .map(|arr| {
1078                arr.iter()
1079                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
1080                    .collect()
1081            })
1082            .unwrap_or_default();
1083
1084        let mut accounts = self.state.write();
1085        let state = accounts.get_or_create(&req.account_id);
1086        let secret = self.find_secret_mut(state, &secret_id)?;
1087
1088        secret.tags.retain(|(k, _)| !tag_keys.contains(k));
1089
1090        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1091    }
1092
1093    fn list_secret_version_ids(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1094        let body = req.json_body();
1095        let secret_id = require_secret_id(&body)?;
1096        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
1097
1098        let accounts = self.state.read();
1099        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1100        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1101        let secret = self.find_secret_ref(state, &secret_id)?;
1102
1103        let versions: Vec<Value> = secret
1104            .versions
1105            .values()
1106            .map(|v| {
1107                json!({
1108                    "VersionId": v.version_id,
1109                    "VersionStages": v.stages,
1110                    "CreatedDate": v.created_at.timestamp_millis() as f64 / 1000.0,
1111                })
1112            })
1113            .collect();
1114
1115        let response = json!({
1116            "ARN": secret.arn,
1117            "Name": secret.name,
1118            "Versions": versions,
1119        });
1120
1121        Ok(AwsResponse::ok_json(response))
1122    }
1123
1124    fn get_random_password(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1125        let body = req.json_body();
1126        let length = body["PasswordLength"].as_i64().unwrap_or(32) as usize;
1127
1128        if length < 4 {
1129            return Err(AwsServiceError::aws_error(
1130                StatusCode::BAD_REQUEST,
1131                "InvalidParameterException",
1132                "InvalidParameterException",
1133            ));
1134        }
1135        if length > 4096 {
1136            return Err(AwsServiceError::aws_error(
1137                StatusCode::BAD_REQUEST,
1138                "InvalidParameterValue",
1139                "InvalidParameterValue",
1140            ));
1141        }
1142
1143        let exclude_lowercase = body["ExcludeLowercase"].as_bool().unwrap_or(false);
1144        let exclude_uppercase = body["ExcludeUppercase"].as_bool().unwrap_or(false);
1145        let exclude_numbers = body["ExcludeNumbers"].as_bool().unwrap_or(false);
1146        let exclude_punctuation = body["ExcludePunctuation"].as_bool().unwrap_or(false);
1147        let include_space = body["IncludeSpace"].as_bool().unwrap_or(false);
1148        let require_each = body["RequireEachIncludedType"].as_bool().unwrap_or(true);
1149        validate_optional_string_length(
1150            "excludeCharacters",
1151            body["ExcludeCharacters"].as_str(),
1152            0,
1153            4096,
1154        )?;
1155        let exclude_chars = body["ExcludeCharacters"].as_str().unwrap_or("").to_string();
1156
1157        let lowercase = "abcdefghijklmnopqrstuvwxyz";
1158        let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1159        let digits = "0123456789";
1160        let punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
1161
1162        let mut char_pool = String::new();
1163        let mut required_chars: Vec<String> = Vec::new();
1164
1165        if !exclude_lowercase {
1166            let filtered: String = lowercase
1167                .chars()
1168                .filter(|c| !exclude_chars.contains(*c))
1169                .collect();
1170            if !filtered.is_empty() {
1171                required_chars.push(filtered.clone());
1172                char_pool.push_str(&filtered);
1173            }
1174        }
1175        if !exclude_uppercase {
1176            let filtered: String = uppercase
1177                .chars()
1178                .filter(|c| !exclude_chars.contains(*c))
1179                .collect();
1180            if !filtered.is_empty() {
1181                required_chars.push(filtered.clone());
1182                char_pool.push_str(&filtered);
1183            }
1184        }
1185        if !exclude_numbers {
1186            let filtered: String = digits
1187                .chars()
1188                .filter(|c| !exclude_chars.contains(*c))
1189                .collect();
1190            if !filtered.is_empty() {
1191                required_chars.push(filtered.clone());
1192                char_pool.push_str(&filtered);
1193            }
1194        }
1195        if !exclude_punctuation {
1196            let filtered: String = punctuation
1197                .chars()
1198                .filter(|c| !exclude_chars.contains(*c))
1199                .collect();
1200            if !filtered.is_empty() {
1201                required_chars.push(filtered.clone());
1202                char_pool.push_str(&filtered);
1203            }
1204        }
1205        if include_space && !exclude_chars.contains(' ') {
1206            char_pool.push(' ');
1207        }
1208
1209        if char_pool.is_empty() {
1210            return Err(AwsServiceError::aws_error(
1211                StatusCode::BAD_REQUEST,
1212                "InvalidParameterException",
1213                "InvalidParameterException",
1214            ));
1215        }
1216
1217        let pool_bytes: Vec<char> = char_pool.chars().collect();
1218        let mut password = String::with_capacity(length);
1219
1220        // Use simple random generation
1221        if require_each {
1222            // First, ensure at least one character from each required category
1223            for category in &required_chars {
1224                let chars: Vec<char> = category.chars().collect();
1225                let idx = simple_random() % chars.len();
1226                password.push(chars[idx]);
1227            }
1228            if include_space && !exclude_chars.contains(' ') {
1229                password.push(' ');
1230            }
1231        }
1232
1233        // Fill the rest randomly
1234        while password.len() < length {
1235            let idx = simple_random() % pool_bytes.len();
1236            password.push(pool_bytes[idx]);
1237        }
1238
1239        // Shuffle the password (Fisher-Yates)
1240        let mut chars: Vec<char> = password.chars().collect();
1241        for i in (1..chars.len()).rev() {
1242            let j = simple_random() % (i + 1);
1243            chars.swap(i, j);
1244        }
1245        let password: String = chars.into_iter().take(length).collect();
1246
1247        let response = json!({
1248            "RandomPassword": password,
1249        });
1250
1251        Ok(AwsResponse::ok_json(response))
1252    }
1253
1254    fn rotate_secret(
1255        &self,
1256        req: &AwsRequest,
1257    ) -> Result<(AwsResponse, Option<RotationInvocation>), AwsServiceError> {
1258        let body = req.json_body();
1259        let secret_id = require_secret_id(&body)?;
1260
1261        // Validate ClientRequestToken
1262        if let Some(token) = body["ClientRequestToken"].as_str() {
1263            if token.len() < 32 || token.len() > 64 {
1264                return Err(AwsServiceError::aws_error(
1265                    StatusCode::BAD_REQUEST,
1266                    "InvalidParameterException",
1267                    "ClientRequestToken must be 32-64 characters long.",
1268                ));
1269            }
1270        }
1271
1272        // Validate RotationLambdaARN
1273        if let Some(arn) = body["RotationLambdaARN"].as_str() {
1274            if arn.len() > 2048 {
1275                return Err(AwsServiceError::aws_error(
1276                    StatusCode::BAD_REQUEST,
1277                    "InvalidParameterException",
1278                    "RotationLambdaARN length must be less than or equal to 2048.",
1279                ));
1280            }
1281        }
1282
1283        // Validate RotationRules
1284        if let Some(rules) = body["RotationRules"].as_object() {
1285            if let Some(days) = rules.get("AutomaticallyAfterDays").and_then(|v| v.as_i64()) {
1286                if !(1..=1000).contains(&days) {
1287                    return Err(AwsServiceError::aws_error(
1288                        StatusCode::BAD_REQUEST,
1289                        "InvalidParameterException",
1290                        "RotationRules.AutomaticallyAfterDays must be within 1-1000.",
1291                    ));
1292                }
1293            }
1294        }
1295
1296        let mut accounts = self.state.write();
1297        let state = accounts.get_or_create(&req.account_id);
1298        let secret = self.find_secret_mut(state, &secret_id)?;
1299
1300        if secret.deleted {
1301            return Err(AwsServiceError::aws_error(
1302                StatusCode::BAD_REQUEST,
1303                "InvalidRequestException",
1304                "You can't perform this operation on the secret because it was marked for deletion.",
1305            ));
1306        }
1307
1308        // Set rotation config
1309        if let Some(lambda_arn) = body["RotationLambdaARN"].as_str() {
1310            secret.rotation_lambda_arn = Some(lambda_arn.to_string());
1311        }
1312
1313        if let Some(rules) = body["RotationRules"].as_object() {
1314            let days = rules.get("AutomaticallyAfterDays").and_then(|v| v.as_i64());
1315            secret.rotation_rules = Some(RotationRules {
1316                automatically_after_days: days,
1317            });
1318        }
1319
1320        secret.rotation_enabled = Some(true);
1321        let now = Utc::now();
1322        secret.last_rotated_at = Some(now);
1323        secret.last_changed_at = now;
1324
1325        let version_id = body["ClientRequestToken"]
1326            .as_str()
1327            .map(|s| s.to_string())
1328            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
1329
1330        let has_lambda =
1331            body["RotationLambdaARN"].as_str().is_some() || secret.rotation_lambda_arn.is_some();
1332        let lambda_arn = secret.rotation_lambda_arn.clone();
1333
1334        // If the secret has a value, perform rotation
1335        let mut invocation = None;
1336        if let Some(current_vid) = secret.current_version_id.clone() {
1337            let current_value = secret.versions.get(&current_vid).cloned();
1338
1339            if let Some(cv) = current_value {
1340                if has_lambda {
1341                    // With Lambda: do NOT pre-create the AWSPENDING version. The
1342                    // rotation Lambda is responsible for putting the new value via
1343                    // PutSecretValue with VersionStages=[AWSPENDING] during the
1344                    // createSecret step (matching real AWS Secrets Manager behavior).
1345
1346                    // Schedule Lambda invocation
1347                    if let Some(ref arn) = lambda_arn {
1348                        invocation = Some(RotationInvocation {
1349                            lambda_arn: arn.clone(),
1350                            secret_id: secret.arn.clone(),
1351                            client_request_token: version_id.clone(),
1352                        });
1353                    }
1354                } else {
1355                    // Without Lambda: simple rotation - new version becomes AWSCURRENT
1356                    // Move old version to AWSPREVIOUS
1357                    if let Some(old_v) = secret.versions.get_mut(&current_vid) {
1358                        old_v.stages.retain(|s| s != "AWSCURRENT");
1359                        if !old_v.stages.contains(&"AWSPREVIOUS".to_string()) {
1360                            old_v.stages.push("AWSPREVIOUS".to_string());
1361                        }
1362                    }
1363                    let version = SecretVersion {
1364                        version_id: version_id.clone(),
1365                        secret_string: cv.secret_string.clone(),
1366                        secret_binary: cv.secret_binary.clone(),
1367                        stages: vec!["AWSCURRENT".to_string()],
1368                        created_at: now,
1369                    };
1370                    secret.versions.insert(version_id.clone(), version);
1371                    secret.current_version_id = Some(version_id.clone());
1372                }
1373            }
1374        }
1375
1376        let response = json!({
1377            "ARN": secret.arn,
1378            "Name": secret.name,
1379            "VersionId": version_id,
1380        });
1381
1382        Ok((AwsResponse::ok_json(response), invocation))
1383    }
1384
1385    fn cancel_rotate_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1386        let body = req.json_body();
1387        let secret_id = require_secret_id(&body)?;
1388
1389        let mut accounts = self.state.write();
1390        let state = accounts.get_or_create(&req.account_id);
1391        let secret = self.find_secret_mut(state, &secret_id)?;
1392
1393        if secret.deleted {
1394            return Err(AwsServiceError::aws_error(
1395                StatusCode::BAD_REQUEST,
1396                "InvalidRequestException",
1397                "You can't perform this operation on the secret because it was marked for deletion.",
1398            ));
1399        }
1400
1401        if secret.rotation_enabled != Some(true) {
1402            return Err(AwsServiceError::aws_error(
1403                StatusCode::BAD_REQUEST,
1404                "InvalidRequestException",
1405                "You can't cancel rotation for a secret that does not have rotation enabled.",
1406            ));
1407        }
1408
1409        secret.rotation_enabled = Some(false);
1410
1411        let response = json!({
1412            "ARN": secret.arn,
1413            "Name": secret.name,
1414        });
1415
1416        Ok(AwsResponse::ok_json(response))
1417    }
1418
1419    fn update_secret_version_stage(
1420        &self,
1421        req: &AwsRequest,
1422    ) -> Result<AwsResponse, AwsServiceError> {
1423        let body = req.json_body();
1424        let secret_id = require_secret_id(&body)?;
1425        let version_stage = body["VersionStage"]
1426            .as_str()
1427            .ok_or_else(|| {
1428                AwsServiceError::aws_error(
1429                    StatusCode::BAD_REQUEST,
1430                    "InvalidParameterException",
1431                    "VersionStage is required",
1432                )
1433            })?
1434            .to_string();
1435        validate_string_length("versionStage", &version_stage, 1, 256)?;
1436        validate_optional_string_length(
1437            "removeFromVersionId",
1438            body["RemoveFromVersionId"].as_str(),
1439            32,
1440            64,
1441        )?;
1442        validate_optional_string_length(
1443            "moveToVersionId",
1444            body["MoveToVersionId"].as_str(),
1445            32,
1446            64,
1447        )?;
1448
1449        let move_to = body["MoveToVersionId"].as_str().map(|s| s.to_string());
1450        let remove_from = body["RemoveFromVersionId"].as_str().map(|s| s.to_string());
1451
1452        let mut accounts = self.state.write();
1453        let state = accounts.get_or_create(&req.account_id);
1454        let secret = self.find_secret_mut(state, &secret_id)?;
1455
1456        // Validate: if moving AWSCURRENT, must specify RemoveFromVersionId
1457        if version_stage == "AWSCURRENT" && move_to.is_some() && remove_from.is_none() {
1458            // Find the version that currently has AWSCURRENT
1459            let current_holder = secret
1460                .versions
1461                .iter()
1462                .find(|(_, v)| v.stages.contains(&"AWSCURRENT".to_string()))
1463                .map(|(id, _)| id.clone());
1464
1465            if let Some(current_vid) = current_holder {
1466                return Err(AwsServiceError::aws_error(
1467                    StatusCode::BAD_REQUEST,
1468                    "InvalidParameterException",
1469                    format!(
1470                        "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."
1471                    ),
1472                ));
1473            }
1474        }
1475
1476        // Remove stage from specified version
1477        if let Some(ref remove_vid) = remove_from {
1478            if let Some(version) = secret.versions.get_mut(remove_vid) {
1479                version.stages.retain(|s| s != &version_stage);
1480                // If moving AWSCURRENT away, add AWSPREVIOUS and remove from others
1481                if version_stage == "AWSCURRENT" {
1482                    // Remove AWSPREVIOUS from all other versions first
1483                    for (id, v) in secret.versions.iter_mut() {
1484                        if id != remove_vid {
1485                            v.stages.retain(|s| s != "AWSPREVIOUS");
1486                        }
1487                    }
1488                    // Now add AWSPREVIOUS to the version losing AWSCURRENT
1489                    if let Some(v) = secret.versions.get_mut(remove_vid) {
1490                        if !v.stages.contains(&"AWSPREVIOUS".to_string()) {
1491                            v.stages.push("AWSPREVIOUS".to_string());
1492                        }
1493                    }
1494                }
1495            }
1496        }
1497
1498        // Add stage to specified version
1499        if let Some(ref move_vid) = move_to {
1500            if let Some(version) = secret.versions.get_mut(move_vid) {
1501                if !version.stages.contains(&version_stage) {
1502                    version.stages.push(version_stage.clone());
1503                }
1504            }
1505            // Update current_version_id if we moved AWSCURRENT
1506            if version_stage == "AWSCURRENT" {
1507                secret.current_version_id = Some(move_vid.clone());
1508            }
1509        }
1510
1511        let response = json!({
1512            "ARN": secret.arn,
1513            "Name": secret.name,
1514        });
1515
1516        Ok(AwsResponse::ok_json(response))
1517    }
1518
1519    fn batch_get_secret_value(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1520        let body = req.json_body();
1521        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
1522        let secret_id_list = body["SecretIdList"].as_array();
1523        let filters = body["Filters"].as_array();
1524        let max_results = body.get("MaxResults").and_then(|v| v.as_i64());
1525
1526        // Validate: can't use both SecretIdList and Filters
1527        if secret_id_list.is_some() && filters.is_some() {
1528            return Err(AwsServiceError::aws_error(
1529                StatusCode::BAD_REQUEST,
1530                "InvalidParameterException",
1531                "Either 'SecretIdList' or 'Filters' must be provided, but not both.",
1532            ));
1533        }
1534
1535        // Validate: MaxResults requires Filters
1536        if max_results.is_some() && filters.is_none() {
1537            return Err(AwsServiceError::aws_error(
1538                StatusCode::BAD_REQUEST,
1539                "InvalidParameterException",
1540                "'Filters' not specified. 'Filters' must also be specified when 'MaxResults' is provided.",
1541            ));
1542        }
1543
1544        let accounts = self.state.read();
1545        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1546        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1547        let mut secret_values: Vec<Value> = Vec::new();
1548        let mut errors: Vec<Value> = Vec::new();
1549
1550        if let Some(id_list) = secret_id_list {
1551            for id_val in id_list {
1552                let sid = id_val.as_str().unwrap_or("");
1553                match self.find_secret_ref(state, sid) {
1554                    Ok(secret) => {
1555                        if secret.deleted {
1556                            errors.push(json!({
1557                                "SecretId": sid,
1558                                "ErrorCode": "InvalidRequestException",
1559                                "Message": "Secret is currently marked deleted. Secret can be recovered with RestoreSecret. Secret is currently marked deleted.",
1560                            }));
1561                        } else if let Some(ref current_vid) = secret.current_version_id {
1562                            if let Some(version) = secret.versions.get(current_vid) {
1563                                let mut entry = json!({
1564                                    "ARN": secret.arn,
1565                                    "Name": secret.name,
1566                                    "VersionId": version.version_id,
1567                                    "VersionStages": version.stages,
1568                                    "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
1569                                });
1570                                if let Some(ref s) = version.secret_string {
1571                                    entry["SecretString"] = json!(s);
1572                                }
1573                                if let Some(ref b) = version.secret_binary {
1574                                    entry["SecretBinary"] = json!(base64_encode(b));
1575                                }
1576                                secret_values.push(entry);
1577                            } else {
1578                                errors.push(json!({
1579                                    "SecretId": sid,
1580                                    "ErrorCode": "ResourceNotFoundException",
1581                                    "Message": "Secrets Manager can't find the specified secret.",
1582                                }));
1583                            }
1584                        } else {
1585                            errors.push(json!({
1586                                "SecretId": sid,
1587                                "ErrorCode": "ResourceNotFoundException",
1588                                "Message": "Secrets Manager can't find the specified secret.",
1589                            }));
1590                        }
1591                    }
1592                    Err(_) => {
1593                        errors.push(json!({
1594                            "SecretId": sid,
1595                            "ErrorCode": "ResourceNotFoundException",
1596                            "Message": "Secrets Manager can't find the specified secret.",
1597                        }));
1598                    }
1599                }
1600            }
1601        } else if let Some(filters) = filters {
1602            // Get secrets matching filters
1603            let matching: Vec<&Secret> = state
1604                .secrets
1605                .values()
1606                .filter(|s| {
1607                    if s.deleted {
1608                        return false;
1609                    }
1610                    for filter in filters {
1611                        let key = filter["Key"].as_str().unwrap_or("");
1612                        let values: Vec<&str> = filter["Values"]
1613                            .as_array()
1614                            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
1615                            .unwrap_or_default();
1616                        let matches = match key {
1617                            "name" => filter_name(s, &values),
1618                            "description" => filter_description(s, &values),
1619                            "tag-key" => filter_tag_key(s, &values),
1620                            "tag-value" => filter_tag_value(s, &values),
1621                            "all" => filter_all(s, &values),
1622                            _ => true,
1623                        };
1624                        if !matches {
1625                            return false;
1626                        }
1627                    }
1628                    true
1629                })
1630                .collect();
1631
1632            let limit = max_results.unwrap_or(100) as usize;
1633            let mut no_value_found = false;
1634            let mut matching = matching;
1635            matching.sort_by(|a, b| a.name.cmp(&b.name));
1636
1637            for secret in matching.iter().take(limit) {
1638                if let Some(ref current_vid) = secret.current_version_id {
1639                    if let Some(version) = secret.versions.get(current_vid) {
1640                        let mut entry = json!({
1641                            "ARN": secret.arn,
1642                            "Name": secret.name,
1643                            "VersionId": version.version_id,
1644                            "VersionStages": version.stages,
1645                            "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
1646                        });
1647                        if let Some(ref s) = version.secret_string {
1648                            entry["SecretString"] = json!(s);
1649                        }
1650                        if let Some(ref b) = version.secret_binary {
1651                            entry["SecretBinary"] = json!(base64_encode(b));
1652                        }
1653                        secret_values.push(entry);
1654                    } else {
1655                        no_value_found = true;
1656                    }
1657                } else {
1658                    no_value_found = true;
1659                }
1660            }
1661
1662            if no_value_found && secret_values.is_empty() {
1663                return Err(AwsServiceError::aws_error(
1664                    StatusCode::NOT_FOUND,
1665                    "ResourceNotFoundException",
1666                    "Secrets Manager can't find the specified secret.",
1667                ));
1668            }
1669        }
1670
1671        let mut response = json!({
1672            "SecretValues": secret_values,
1673            "Errors": errors,
1674        });
1675
1676        // Remove empty arrays
1677        if errors.is_empty() {
1678            response.as_object_mut().unwrap().remove("Errors");
1679        }
1680
1681        Ok(AwsResponse::ok_json(response))
1682    }
1683
1684    fn get_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1685        let body = req.json_body();
1686        let secret_id = require_secret_id(&body)?;
1687
1688        let accounts = self.state.read();
1689        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1690        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1691        let secret = self.find_secret_ref(state, &secret_id)?;
1692
1693        let mut response = json!({
1694            "ARN": secret.arn,
1695            "Name": secret.name,
1696        });
1697
1698        if let Some(ref policy) = secret.resource_policy {
1699            response["ResourcePolicy"] = json!(policy);
1700        }
1701
1702        Ok(AwsResponse::ok_json(response))
1703    }
1704
1705    fn validate_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1706        let body = req.json_body();
1707        validate_optional_string_length("secretId", body["SecretId"].as_str(), 1, 2048)?;
1708        validate_required("ResourcePolicy", &body["ResourcePolicy"])?;
1709        let policy_str = body["ResourcePolicy"].as_str().ok_or_else(|| {
1710            AwsServiceError::aws_error(
1711                StatusCode::BAD_REQUEST,
1712                "InvalidParameterException",
1713                "ResourcePolicy must be a string",
1714            )
1715        })?;
1716        validate_string_length("resourcePolicy", policy_str, 1, 20480)?;
1717
1718        // If SecretId is provided, verify the secret exists
1719        if let Some(secret_id) = body["SecretId"].as_str() {
1720            let accounts = self.state.read();
1721            let empty = SecretsManagerState::new(&req.account_id, &req.region);
1722            let state = accounts.get(&req.account_id).unwrap_or(&empty);
1723            self.find_secret_key(state, secret_id)?;
1724        }
1725
1726        let response = json!({
1727            "PolicyValidationPassed": true,
1728            "ValidationErrors": [],
1729        });
1730        Ok(AwsResponse::ok_json(response))
1731    }
1732
1733    fn put_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1734        let body = req.json_body();
1735        let secret_id = require_secret_id(&body)?;
1736        validate_required("ResourcePolicy", &body["ResourcePolicy"])?;
1737        validate_optional_string_length(
1738            "resourcePolicy",
1739            body["ResourcePolicy"].as_str(),
1740            1,
1741            20480,
1742        )?;
1743        let policy = body["ResourcePolicy"].as_str().map(|s| s.to_string());
1744
1745        let mut accounts = self.state.write();
1746        let state = accounts.get_or_create(&req.account_id);
1747        let secret = self.find_secret_mut(state, &secret_id)?;
1748        secret.resource_policy = policy;
1749
1750        let response = json!({
1751            "ARN": secret.arn,
1752            "Name": secret.name,
1753        });
1754
1755        Ok(AwsResponse::ok_json(response))
1756    }
1757
1758    fn delete_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1759        let body = req.json_body();
1760        let secret_id = require_secret_id(&body)?;
1761
1762        let mut accounts = self.state.write();
1763        let state = accounts.get_or_create(&req.account_id);
1764        let secret = self.find_secret_mut(state, &secret_id)?;
1765        secret.resource_policy = None;
1766
1767        let response = json!({
1768            "ARN": secret.arn,
1769            "Name": secret.name,
1770        });
1771
1772        Ok(AwsResponse::ok_json(response))
1773    }
1774
1775    fn replicate_secret_to_regions(
1776        &self,
1777        req: &AwsRequest,
1778    ) -> Result<AwsResponse, AwsServiceError> {
1779        let body = req.json_body();
1780        let secret_id = require_secret_id(&body)?;
1781
1782        let accounts = self.state.read();
1783        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1784        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1785        let secret = self.find_secret_ref(state, &secret_id)?;
1786
1787        let response = json!({
1788            "ARN": secret.arn,
1789            "ReplicationStatus": [],
1790        });
1791        Ok(AwsResponse::ok_json(response))
1792    }
1793
1794    fn remove_regions_from_replication(
1795        &self,
1796        req: &AwsRequest,
1797    ) -> Result<AwsResponse, AwsServiceError> {
1798        let body = req.json_body();
1799        let secret_id = require_secret_id(&body)?;
1800
1801        let accounts = self.state.read();
1802        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1803        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1804        let secret = self.find_secret_ref(state, &secret_id)?;
1805
1806        let response = json!({
1807            "ARN": secret.arn,
1808            "ReplicationStatus": [],
1809        });
1810        Ok(AwsResponse::ok_json(response))
1811    }
1812
1813    fn stop_replication_to_replica(
1814        &self,
1815        req: &AwsRequest,
1816    ) -> Result<AwsResponse, AwsServiceError> {
1817        let body = req.json_body();
1818        let secret_id = require_secret_id(&body)?;
1819
1820        let accounts = self.state.read();
1821        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1822        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1823        let secret = self.find_secret_ref(state, &secret_id)?;
1824
1825        let response = json!({
1826            "ARN": secret.arn,
1827        });
1828        Ok(AwsResponse::ok_json(response))
1829    }
1830
1831    /// Find a secret by name, full ARN, or partial ARN (mutable).
1832    fn find_secret_mut<'a>(
1833        &self,
1834        state: &'a mut crate::state::SecretsManagerState,
1835        secret_id: &str,
1836    ) -> Result<&'a mut Secret, AwsServiceError> {
1837        let key = self.find_secret_key(state, secret_id)?;
1838        Ok(state.secrets.get_mut(&key).unwrap())
1839    }
1840
1841    fn find_secret_key(
1842        &self,
1843        state: &crate::state::SecretsManagerState,
1844        secret_id: &str,
1845    ) -> Result<String, AwsServiceError> {
1846        if state.secrets.contains_key(secret_id) {
1847            return Ok(secret_id.to_string());
1848        }
1849
1850        for secret in state.secrets.values() {
1851            if secret.arn == secret_id {
1852                return Ok(secret.name.clone());
1853            }
1854        }
1855
1856        if secret_id.starts_with("arn:aws:secretsmanager:") {
1857            for secret in state.secrets.values() {
1858                if secret.arn.starts_with(secret_id) {
1859                    return Ok(secret.name.clone());
1860                }
1861            }
1862        }
1863
1864        Err(AwsServiceError::aws_error(
1865            StatusCode::NOT_FOUND,
1866            "ResourceNotFoundException",
1867            "Secrets Manager can't find the specified secret.",
1868        ))
1869    }
1870
1871    /// Find a secret by name, full ARN, or partial ARN (immutable).
1872    fn find_secret_ref<'a>(
1873        &self,
1874        state: &'a crate::state::SecretsManagerState,
1875        secret_id: &str,
1876    ) -> Result<&'a Secret, AwsServiceError> {
1877        if let Some(secret) = state.secrets.get(secret_id) {
1878            return Ok(secret);
1879        }
1880
1881        // Search by full ARN
1882        for secret in state.secrets.values() {
1883            if secret.arn == secret_id {
1884                return Ok(secret);
1885            }
1886        }
1887
1888        // Search by partial ARN
1889        if secret_id.starts_with("arn:aws:secretsmanager:") {
1890            for secret in state.secrets.values() {
1891                if secret.arn.starts_with(secret_id) {
1892                    return Ok(secret);
1893                }
1894            }
1895        }
1896
1897        Err(AwsServiceError::aws_error(
1898            StatusCode::NOT_FOUND,
1899            "ResourceNotFoundException",
1900            "Secrets Manager can't find the specified secret.",
1901        ))
1902    }
1903}
1904
1905/// Parsed + validated inputs for `CreateSecret`.
1906struct CreateSecretInput {
1907    name: String,
1908    client_request_token: Option<String>,
1909    description: Option<String>,
1910    kms_key_id: Option<String>,
1911    secret_string: Option<String>,
1912    secret_binary: Option<Vec<u8>>,
1913    tags: Vec<(String, String)>,
1914}
1915
1916impl CreateSecretInput {
1917    fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
1918        validate_required("Name", &body["Name"])?;
1919        let name = body["Name"]
1920            .as_str()
1921            .ok_or_else(|| {
1922                AwsServiceError::aws_error(
1923                    StatusCode::BAD_REQUEST,
1924                    "InvalidParameterException",
1925                    "Name is required",
1926                )
1927            })?
1928            .to_string();
1929        validate_string_length("name", &name, 1, 512)?;
1930        validate_optional_string_length(
1931            "clientRequestToken",
1932            body["ClientRequestToken"].as_str(),
1933            32,
1934            64,
1935        )?;
1936        validate_optional_string_length("description", body["Description"].as_str(), 0, 2048)?;
1937        validate_optional_string_length("kmsKeyId", body["KmsKeyId"].as_str(), 0, 2048)?;
1938        validate_optional_string_length("secretString", body["SecretString"].as_str(), 1, 65536)?;
1939
1940        Ok(Self {
1941            name,
1942            client_request_token: body["ClientRequestToken"].as_str().map(|s| s.to_string()),
1943            description: body["Description"].as_str().map(|s| s.to_string()),
1944            kms_key_id: body["KmsKeyId"].as_str().map(|s| s.to_string()),
1945            secret_string: body["SecretString"].as_str().map(|s| s.to_string()),
1946            secret_binary: body["SecretBinary"].as_str().and_then(base64_decode),
1947            tags: parse_tags(&body["Tags"]),
1948        })
1949    }
1950}
1951
1952#[async_trait]
1953impl AwsService for SecretsManagerService {
1954    fn service_name(&self) -> &str {
1955        "secretsmanager"
1956    }
1957
1958    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1959        let mutates = is_mutating_action(req.action.as_str());
1960        let result = match req.action.as_str() {
1961            "CreateSecret" => self.create_secret(&req),
1962            "GetSecretValue" => self.get_secret_value(&req),
1963            "PutSecretValue" => self.put_secret_value(&req),
1964            "UpdateSecret" => self.update_secret(&req),
1965            "DeleteSecret" => self.delete_secret(&req),
1966            "RestoreSecret" => self.restore_secret(&req),
1967            "DescribeSecret" => self.describe_secret(&req),
1968            "ListSecrets" => self.list_secrets(&req),
1969            "TagResource" => self.tag_resource(&req),
1970            "UntagResource" => self.untag_resource(&req),
1971            "ListSecretVersionIds" => self.list_secret_version_ids(&req),
1972            "GetRandomPassword" => self.get_random_password(&req),
1973            "RotateSecret" => {
1974                let (response, invocation) = self.rotate_secret(&req)?;
1975                if let Some(inv) = invocation {
1976                    if let Some(ref bus) = self.delivery_bus {
1977                        let bus = bus.clone();
1978                        // AWS invokes the rotation Lambda asynchronously for each step.
1979                        tokio::spawn(async move {
1980                            for step in &["createSecret", "setSecret", "testSecret", "finishSecret"]
1981                            {
1982                                let payload = serde_json::json!({
1983                                    "SecretId": inv.secret_id,
1984                                    "ClientRequestToken": inv.client_request_token,
1985                                    "Step": step,
1986                                });
1987                                let payload_str = payload.to_string();
1988                                match bus.invoke_lambda(&inv.lambda_arn, &payload_str).await {
1989                                    Some(Ok(_)) => {}
1990                                    Some(Err(e)) => {
1991                                        tracing::warn!(
1992                                            step = step,
1993                                            error = %e,
1994                                            "rotation Lambda invocation failed"
1995                                        );
1996                                    }
1997                                    None => {
1998                                        tracing::warn!(
1999                                            lambda_arn = %inv.lambda_arn,
2000                                            step = step,
2001                                            "rotation Lambda delivery not configured; \
2002                                             Lambda invocation skipped"
2003                                        );
2004                                        break;
2005                                    }
2006                                }
2007                            }
2008                        });
2009                    }
2010                }
2011                Ok(response)
2012            }
2013            "CancelRotateSecret" => self.cancel_rotate_secret(&req),
2014            "UpdateSecretVersionStage" => self.update_secret_version_stage(&req),
2015            "BatchGetSecretValue" => self.batch_get_secret_value(&req),
2016            "GetResourcePolicy" => self.get_resource_policy(&req),
2017            "PutResourcePolicy" => self.put_resource_policy(&req),
2018            "DeleteResourcePolicy" => self.delete_resource_policy(&req),
2019            "ValidateResourcePolicy" => self.validate_resource_policy(&req),
2020            "ReplicateSecretToRegions" => self.replicate_secret_to_regions(&req),
2021            "RemoveRegionsFromReplication" => self.remove_regions_from_replication(&req),
2022            "StopReplicationToReplica" => self.stop_replication_to_replica(&req),
2023            _ => Err(AwsServiceError::action_not_implemented(
2024                "secretsmanager",
2025                &req.action,
2026            )),
2027        };
2028        if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
2029            self.save_snapshot().await;
2030        }
2031        result
2032    }
2033
2034    fn supported_actions(&self) -> &[&str] {
2035        &[
2036            "CreateSecret",
2037            "GetSecretValue",
2038            "PutSecretValue",
2039            "UpdateSecret",
2040            "DeleteSecret",
2041            "RestoreSecret",
2042            "DescribeSecret",
2043            "ListSecrets",
2044            "TagResource",
2045            "UntagResource",
2046            "ListSecretVersionIds",
2047            "GetRandomPassword",
2048            "RotateSecret",
2049            "CancelRotateSecret",
2050            "UpdateSecretVersionStage",
2051            "BatchGetSecretValue",
2052            "GetResourcePolicy",
2053            "PutResourcePolicy",
2054            "DeleteResourcePolicy",
2055            "ValidateResourcePolicy",
2056            "ReplicateSecretToRegions",
2057            "RemoveRegionsFromReplication",
2058            "StopReplicationToReplica",
2059        ]
2060    }
2061}
2062
2063#[path = "service_helpers.rs"]
2064mod service_helpers;
2065pub(crate) use service_helpers::*;
2066
2067/// Extract the owning account-id from an `arn:aws:secretsmanager:...:ACCOUNT:secret:...`
2068/// secret id. Returns `caller_account` when the input is a bare name
2069/// or a same-account ARN.
2070fn secret_owner_account(secret_id: &str, caller_account: &str) -> String {
2071    if !secret_id.starts_with("arn:aws:secretsmanager:") {
2072        return caller_account.to_string();
2073    }
2074    let parts: Vec<&str> = secret_id.splitn(7, ':').collect();
2075    if parts.len() < 5 {
2076        return caller_account.to_string();
2077    }
2078    let account = parts[4];
2079    if account.is_empty() {
2080        caller_account.to_string()
2081    } else {
2082        account.to_string()
2083    }
2084}
2085
2086/// Evaluate a Secrets Manager resource policy against a cross-account
2087/// caller. Empty policy implicitly denies (real AWS behaviour). Uses
2088/// the shared IAM evaluator so the same policy semantics apply
2089/// service-wide.
2090fn resource_policy_allows(policy_doc: &str, caller_account: &str, secret_arn: &str) -> bool {
2091    if policy_doc.is_empty() {
2092        return false;
2093    }
2094    use fakecloud_core::auth::{Principal, PrincipalType};
2095    use fakecloud_iam::evaluator::{evaluate, EvalRequest, PolicyDocument};
2096    let doc = PolicyDocument::parse(policy_doc);
2097    let principal_arn = format!("arn:aws:iam::{caller_account}:root");
2098    let principal = Principal {
2099        arn: principal_arn.clone(),
2100        user_id: principal_arn.clone(),
2101        account_id: caller_account.to_string(),
2102        principal_type: PrincipalType::User,
2103        source_identity: None,
2104        tags: None,
2105    };
2106    let req = EvalRequest {
2107        principal: &principal,
2108        action: "secretsmanager:GetSecretValue".to_string(),
2109        resource: secret_arn.to_string(),
2110        context: Default::default(),
2111    };
2112    matches!(
2113        evaluate(&[doc], &req),
2114        fakecloud_iam::evaluator::Decision::Allow
2115    )
2116}
2117
2118#[cfg(test)]
2119#[path = "service_tests.rs"]
2120mod tests;