Skip to main content

fakecloud_secretsmanager/
service.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use chrono::Utc;
6use http::StatusCode;
7use serde_json::{json, Value};
8
9use fakecloud_core::delivery::DeliveryBus;
10use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
11use fakecloud_core::validation::*;
12
13use crate::state::{RotationRules, Secret, SecretVersion, SharedSecretsManagerState};
14
15/// Information needed to invoke the rotation Lambda after releasing state lock.
16struct RotationInvocation {
17    lambda_arn: String,
18    secret_id: String,
19    client_request_token: String,
20}
21
22/// Result of an idempotency check against an existing
23/// `ClientRequestToken` / version id.
24enum VersionIdempotency {
25    /// The version id isn't in the secret yet — this is a fresh write.
26    NotFound,
27    /// The version id exists and stores the exact same payload we're
28    /// about to write — callers should return the existing version as
29    /// a successful no-op response.
30    Match,
31    /// The version id exists but stores a different payload — AWS
32    /// surfaces this as a `ResourceExistsException`.
33    Conflict,
34}
35
36/// Classify whether a proposed write collides with an existing
37/// version. AWS uses `ClientRequestToken` as a client-side idempotency
38/// key, so a repeat write of the exact same payload is a success but a
39/// repeat with a different payload is a `ResourceExistsException`.
40fn check_secret_version_idempotency(
41    versions: &HashMap<String, SecretVersion>,
42    version_id: &str,
43    secret_string: &Option<String>,
44    secret_binary: &Option<Vec<u8>>,
45) -> VersionIdempotency {
46    let Some(existing) = versions.get(version_id) else {
47        return VersionIdempotency::NotFound;
48    };
49    if &existing.secret_string == secret_string && &existing.secret_binary == secret_binary {
50        VersionIdempotency::Match
51    } else {
52        VersionIdempotency::Conflict
53    }
54}
55
56pub struct SecretsManagerService {
57    state: SharedSecretsManagerState,
58    delivery_bus: Option<Arc<DeliveryBus>>,
59}
60
61impl SecretsManagerService {
62    pub fn new(state: SharedSecretsManagerState) -> Self {
63        Self {
64            state,
65            delivery_bus: None,
66        }
67    }
68
69    pub fn with_delivery(mut self, delivery_bus: Arc<DeliveryBus>) -> Self {
70        self.delivery_bus = Some(delivery_bus);
71        self
72    }
73
74    fn create_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
75        let input = CreateSecretInput::from_body(&req.json_body())?;
76        let has_value = input.secret_string.is_some() || input.secret_binary.is_some();
77
78        let mut state = self.state.write();
79
80        if let Some(existing) = state.secrets.get(&input.name) {
81            if let Some(ref token) = input.client_request_token {
82                match check_secret_version_idempotency(
83                    &existing.versions,
84                    token,
85                    &input.secret_string,
86                    &input.secret_binary,
87                ) {
88                    VersionIdempotency::Match => {
89                        let mut response = json!({
90                            "ARN": existing.arn,
91                            "Name": existing.name,
92                            "VersionId": token,
93                        });
94                        if !has_value {
95                            response.as_object_mut().unwrap().remove("VersionId");
96                        }
97                        return Ok(AwsResponse::ok_json(response));
98                    }
99                    VersionIdempotency::Conflict => {
100                        return Err(AwsServiceError::aws_error(
101                            StatusCode::BAD_REQUEST,
102                            "ResourceExistsException",
103                            format!(
104                                "You can't use ClientRequestToken {token} because that value is already in use for a version of secret {}.",
105                                existing.arn
106                            ),
107                        ));
108                    }
109                    VersionIdempotency::NotFound => {}
110                }
111            }
112            return Err(AwsServiceError::aws_error(
113                StatusCode::BAD_REQUEST,
114                "ResourceExistsException",
115                format!(
116                    "The operation failed because the secret {} already exists.",
117                    input.name
118                ),
119            ));
120        }
121
122        let arn = format!(
123            "arn:aws:secretsmanager:{}:{}:secret:{}-{}",
124            req.region,
125            req.account_id,
126            input.name,
127            &uuid::Uuid::new_v4().to_string()[..6]
128        );
129
130        let now = Utc::now();
131
132        let (versions, current_version_id, version_id_for_response) = if has_value {
133            let vid = input
134                .client_request_token
135                .clone()
136                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
137            let version = SecretVersion {
138                version_id: vid.clone(),
139                secret_string: input.secret_string,
140                secret_binary: input.secret_binary,
141                stages: vec!["AWSCURRENT".to_string()],
142                created_at: now,
143            };
144            let mut versions = std::collections::HashMap::new();
145            versions.insert(vid.clone(), version);
146            (versions, Some(vid.clone()), Some(vid))
147        } else {
148            (std::collections::HashMap::new(), None, None)
149        };
150
151        let tags_ever_set = !input.tags.is_empty();
152        let secret = Secret {
153            name: input.name.clone(),
154            arn: arn.clone(),
155            description: input.description,
156            kms_key_id: input.kms_key_id,
157            versions,
158            current_version_id,
159            tags: input.tags,
160            tags_ever_set,
161            deleted: false,
162            deletion_date: None,
163            created_at: now,
164            last_changed_at: now,
165            last_accessed_at: None,
166            rotation_enabled: None,
167            rotation_lambda_arn: None,
168            rotation_rules: None,
169            last_rotated_at: None,
170            resource_policy: None,
171        };
172
173        state.secrets.insert(input.name.clone(), secret);
174
175        let mut response = json!({
176            "ARN": arn,
177            "Name": input.name,
178        });
179        if let Some(vid) = version_id_for_response {
180            response["VersionId"] = json!(vid);
181        }
182
183        Ok(AwsResponse::ok_json(response))
184    }
185
186    fn get_secret_value(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
187        let body = req.json_body();
188        let secret_id = require_secret_id(&body)?;
189        validate_optional_string_length("versionId", body["VersionId"].as_str(), 32, 64)?;
190        validate_optional_string_length("versionStage", body["VersionStage"].as_str(), 1, 256)?;
191
192        let mut state = self.state.write();
193        let secret = self.find_secret_mut(&mut state, &secret_id)?;
194
195        if secret.deleted {
196            return Err(AwsServiceError::aws_error(
197                StatusCode::BAD_REQUEST,
198                "InvalidRequestException",
199                "You can't perform this operation on the secret because it was marked for deletion.",
200            ));
201        }
202
203        let requested_stage = body["VersionStage"].as_str().unwrap_or("AWSCURRENT");
204
205        // Determine which version to return
206        let version_id = body["VersionId"]
207            .as_str()
208            .map(|s| s.to_string())
209            .or_else(|| {
210                secret
211                    .versions
212                    .iter()
213                    .find(|(_, v)| v.stages.contains(&requested_stage.to_string()))
214                    .map(|(id, _)| id.clone())
215            });
216
217        let version_id = match version_id {
218            Some(vid) => vid,
219            None => {
220                // No versions exist
221                return Err(AwsServiceError::aws_error(
222                    StatusCode::NOT_FOUND,
223                    "ResourceNotFoundException",
224                    format!(
225                        "Secrets Manager can't find the specified secret value for staging label: {requested_stage}"
226                    ),
227                ));
228            }
229        };
230
231        let version = secret.versions.get(&version_id).ok_or_else(|| {
232            AwsServiceError::aws_error(
233                StatusCode::NOT_FOUND,
234                "ResourceNotFoundException",
235                format!(
236                    "Secrets Manager can't find the specified secret value for VersionId: {version_id}"
237                ),
238            )
239        })?;
240
241        // If VersionStage is specified with VersionId, verify they match
242        if body["VersionId"].as_str().is_some() {
243            if let Some(stage) = body["VersionStage"].as_str() {
244                if !version.stages.contains(&stage.to_string()) {
245                    return Err(AwsServiceError::aws_error(
246                        StatusCode::NOT_FOUND,
247                        "ResourceNotFoundException",
248                        "You provided a VersionStage that is not associated to the provided VersionId.",
249                    ));
250                }
251            }
252        }
253
254        // Only set last_accessed_at on successful retrieval
255        secret.last_accessed_at = Some(Utc::now());
256
257        let mut response = json!({
258            "ARN": secret.arn,
259            "Name": secret.name,
260            "VersionId": version.version_id,
261            "VersionStages": version.stages,
262            "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
263        });
264
265        if let Some(ref s) = version.secret_string {
266            response["SecretString"] = json!(s);
267        }
268        if let Some(ref b) = version.secret_binary {
269            response["SecretBinary"] = json!(base64_encode(b));
270        }
271
272        Ok(AwsResponse::ok_json(response))
273    }
274
275    fn put_secret_value(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
276        let body = req.json_body();
277        let secret_id = require_secret_id(&body)?;
278        validate_optional_string_length(
279            "clientRequestToken",
280            body["ClientRequestToken"].as_str(),
281            32,
282            64,
283        )?;
284        validate_optional_string_length("secretString", body["SecretString"].as_str(), 1, 65536)?;
285
286        let secret_string = body["SecretString"].as_str().map(|s| s.to_string());
287        let secret_binary = body["SecretBinary"].as_str().and_then(base64_decode);
288
289        // Validate that either SecretString or SecretBinary is provided
290        if secret_string.is_none() && secret_binary.is_none() {
291            return Err(AwsServiceError::aws_error(
292                StatusCode::BAD_REQUEST,
293                "InvalidRequestException",
294                "You must provide either SecretString or SecretBinary.",
295            ));
296        }
297
298        let mut state = self.state.write();
299        let secret = match self.find_secret_mut(&mut state, &secret_id) {
300            Ok(s) => s,
301            Err(_) => {
302                return Err(AwsServiceError::aws_error(
303                    StatusCode::NOT_FOUND,
304                    "ResourceNotFoundException",
305                    "Secrets Manager can't find the specified secret.",
306                ));
307            }
308        };
309
310        if secret.deleted {
311            return Err(AwsServiceError::aws_error(
312                StatusCode::BAD_REQUEST,
313                "InvalidRequestException",
314                "You can't perform this operation on the secret because it was marked for deletion.",
315            ));
316        }
317
318        let now = Utc::now();
319        let version_id = body["ClientRequestToken"]
320            .as_str()
321            .map(|s| s.to_string())
322            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
323
324        match check_secret_version_idempotency(
325            &secret.versions,
326            &version_id,
327            &secret_string,
328            &secret_binary,
329        ) {
330            VersionIdempotency::Match => {
331                let existing_stages = secret.versions[&version_id].stages.clone();
332                return Ok(AwsResponse::ok_json(json!({
333                    "ARN": secret.arn,
334                    "Name": secret.name,
335                    "VersionId": version_id,
336                    "VersionStages": existing_stages,
337                })));
338            }
339            VersionIdempotency::Conflict => {
340                return Err(AwsServiceError::aws_error(
341                    StatusCode::BAD_REQUEST,
342                    "ResourceExistsException",
343                    format!(
344                        "You can't use ClientRequestToken {version_id} because that value is already in use for a version of secret {}.",
345                        secret.arn
346                    ),
347                ));
348            }
349            VersionIdempotency::NotFound => {}
350        }
351
352        let mut version_stages: Vec<String> = body["VersionStages"]
353            .as_array()
354            .map(|arr| {
355                arr.iter()
356                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
357                    .collect()
358            })
359            .unwrap_or_else(|| vec!["AWSCURRENT".to_string()]);
360
361        // If this is the first version with a value, add AWSCURRENT to stages
362        let has_current = secret
363            .versions
364            .values()
365            .any(|v| v.stages.contains(&"AWSCURRENT".to_string()));
366        if !has_current && !version_stages.contains(&"AWSCURRENT".to_string()) {
367            version_stages.push("AWSCURRENT".to_string());
368        }
369
370        // Move AWSCURRENT from old version to AWSPREVIOUS if new version has AWSCURRENT
371        if version_stages.contains(&"AWSCURRENT".to_string()) {
372            if let Some(ref old_vid) = secret.current_version_id.clone() {
373                if let Some(old_version) = secret.versions.get_mut(old_vid) {
374                    old_version.stages.retain(|s| s != "AWSCURRENT");
375                    if !old_version.stages.contains(&"AWSPREVIOUS".to_string()) {
376                        old_version.stages.push("AWSPREVIOUS".to_string());
377                    }
378                }
379                // Remove AWSPREVIOUS from any other version
380                for (id, v) in secret.versions.iter_mut() {
381                    if id != old_vid {
382                        v.stages.retain(|s| s != "AWSPREVIOUS");
383                    }
384                }
385            }
386            secret.current_version_id = Some(version_id.clone());
387        }
388
389        // Remove custom stages from other versions that have them
390        for stage in &version_stages {
391            if stage == "AWSCURRENT" || stage == "AWSPREVIOUS" {
392                continue;
393            }
394            for v in secret.versions.values_mut() {
395                v.stages.retain(|s| s != stage);
396            }
397        }
398
399        // Remove versions with no stages
400        secret.versions.retain(|_, v| !v.stages.is_empty());
401
402        let version = SecretVersion {
403            version_id: version_id.clone(),
404            secret_string,
405            secret_binary,
406            stages: version_stages.clone(),
407            created_at: now,
408        };
409
410        secret.versions.insert(version_id.clone(), version);
411        secret.last_changed_at = now;
412
413        let response = json!({
414            "ARN": secret.arn,
415            "Name": secret.name,
416            "VersionId": version_id,
417            "VersionStages": version_stages,
418        });
419
420        Ok(AwsResponse::ok_json(response))
421    }
422
423    fn update_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
424        let body = req.json_body();
425        let secret_id = require_secret_id(&body)?;
426        validate_optional_string_length(
427            "clientRequestToken",
428            body["ClientRequestToken"].as_str(),
429            32,
430            64,
431        )?;
432        validate_optional_string_length("description", body["Description"].as_str(), 0, 2048)?;
433        validate_optional_string_length("kmsKeyId", body["KmsKeyId"].as_str(), 0, 2048)?;
434        validate_optional_string_length("secretString", body["SecretString"].as_str(), 1, 65536)?;
435
436        let mut state = self.state.write();
437        let secret = match self.find_secret_mut(&mut state, &secret_id) {
438            Ok(s) => s,
439            Err(_) => {
440                return Err(AwsServiceError::aws_error(
441                    StatusCode::NOT_FOUND,
442                    "ResourceNotFoundException",
443                    "Secrets Manager can't find the specified secret.",
444                ));
445            }
446        };
447
448        if secret.deleted {
449            return Err(AwsServiceError::aws_error(
450                StatusCode::BAD_REQUEST,
451                "InvalidRequestException",
452                "You can't perform this operation on the secret because it was marked for deletion.",
453            ));
454        }
455
456        if let Some(desc) = body["Description"].as_str() {
457            secret.description = Some(desc.to_string());
458        }
459        if let Some(kms) = body["KmsKeyId"].as_str() {
460            secret.kms_key_id = Some(kms.to_string());
461        }
462
463        // If SecretString or SecretBinary is provided, create a new version
464        let secret_string = body["SecretString"].as_str().map(|s| s.to_string());
465        let secret_binary = body["SecretBinary"].as_str().and_then(base64_decode);
466
467        let version_id = if secret_string.is_some() || secret_binary.is_some() {
468            let vid = body["ClientRequestToken"]
469                .as_str()
470                .map(|s| s.to_string())
471                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
472
473            match check_secret_version_idempotency(
474                &secret.versions,
475                &vid,
476                &secret_string,
477                &secret_binary,
478            ) {
479                VersionIdempotency::Match => {
480                    return Ok(AwsResponse::ok_json(json!({
481                        "ARN": secret.arn,
482                        "Name": secret.name,
483                        "VersionId": vid,
484                    })));
485                }
486                VersionIdempotency::Conflict => {
487                    return Err(AwsServiceError::aws_error(
488                        StatusCode::BAD_REQUEST,
489                        "ResourceExistsException",
490                        format!(
491                            "You can't use ClientRequestToken {vid} because that value is already in use for a version of secret {}.",
492                            secret.arn
493                        ),
494                    ));
495                }
496                VersionIdempotency::NotFound => {}
497            }
498
499            let now = Utc::now();
500
501            // Move AWSCURRENT -> AWSPREVIOUS on old version
502            if let Some(ref old_vid) = secret.current_version_id.clone() {
503                if let Some(old_v) = secret.versions.get_mut(old_vid) {
504                    old_v.stages.retain(|s| s != "AWSCURRENT");
505                    if !old_v.stages.contains(&"AWSPREVIOUS".to_string()) {
506                        old_v.stages.push("AWSPREVIOUS".to_string());
507                    }
508                }
509            }
510
511            let version = SecretVersion {
512                version_id: vid.clone(),
513                secret_string,
514                secret_binary,
515                stages: vec!["AWSCURRENT".to_string()],
516                created_at: now,
517            };
518            secret.versions.insert(vid.clone(), version);
519            secret.current_version_id = Some(vid.clone());
520            secret.last_changed_at = now;
521            Some(vid)
522        } else {
523            secret.last_changed_at = Utc::now();
524            None
525        };
526
527        let mut response = json!({
528            "ARN": secret.arn,
529            "Name": secret.name,
530        });
531        if let Some(vid) = version_id {
532            response["VersionId"] = json!(vid);
533        }
534
535        Ok(AwsResponse::ok_json(response))
536    }
537
538    fn delete_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
539        let body = req.json_body();
540        let secret_id = require_secret_id(&body)?;
541
542        let force_delete = body["ForceDeleteWithoutRecovery"]
543            .as_bool()
544            .unwrap_or(false);
545        let recovery_window = body.get("RecoveryWindowInDays").and_then(|v| v.as_i64());
546
547        // Validate recovery window range first (AWS validates this before the conflict check)
548        if let Some(days) = recovery_window {
549            if !(7..=30).contains(&days) {
550                return Err(AwsServiceError::aws_error(
551                    StatusCode::BAD_REQUEST,
552                    "InvalidParameterException",
553                    "An error occurred (InvalidParameterException) when calling the DeleteSecret operation: RecoveryWindowInDays value must be between 7 and 30 days (inclusive).",
554                ));
555            }
556        }
557
558        // Validate: can't use both force delete and recovery window
559        if force_delete && recovery_window.is_some() {
560            return Err(AwsServiceError::aws_error(
561                StatusCode::BAD_REQUEST,
562                "InvalidParameterException",
563                "An error occurred (InvalidParameterException) when calling the DeleteSecret operation: You can't use ForceDeleteWithoutRecovery in conjunction with RecoveryWindowInDays.",
564            ));
565        }
566
567        let mut state = self.state.write();
568
569        if force_delete {
570            // Force delete: if secret doesn't exist, create a fake response
571            match self.find_secret_mut(&mut state, &secret_id) {
572                Ok(secret) => {
573                    let arn = secret.arn.clone();
574                    let name = secret.name.clone();
575                    let deletion_date = Utc::now();
576                    state.secrets.remove(&name);
577                    let response = json!({
578                        "ARN": arn,
579                        "Name": name,
580                        "DeletionDate": deletion_date.timestamp_millis() as f64 / 1000.0,
581                    });
582                    return Ok(AwsResponse::ok_json(response));
583                }
584                Err(_) => {
585                    // For force delete of non-existent secret, AWS returns success
586                    let arn = format!(
587                        "arn:aws:secretsmanager:{}:{}:secret:{}-{}",
588                        req.region,
589                        req.account_id,
590                        secret_id,
591                        &uuid::Uuid::new_v4().to_string()[..6]
592                    );
593                    let deletion_date = Utc::now();
594                    let response = json!({
595                        "ARN": arn,
596                        "Name": secret_id,
597                        "DeletionDate": deletion_date.timestamp_millis() as f64 / 1000.0,
598                    });
599                    return Ok(AwsResponse::ok_json(response));
600                }
601            }
602        }
603
604        let secret = self.find_secret_mut(&mut state, &secret_id)?;
605
606        if secret.deleted {
607            return Err(AwsServiceError::aws_error(
608                StatusCode::BAD_REQUEST,
609                "InvalidRequestException",
610                "You can't perform this operation on the secret because it was already scheduled for deletion.",
611            ));
612        }
613
614        let now = Utc::now();
615        let days = recovery_window.unwrap_or(30);
616        let deletion_date = now + chrono::Duration::days(days);
617        secret.deleted = true;
618        secret.deletion_date = Some(deletion_date);
619
620        let response = json!({
621            "ARN": secret.arn,
622            "Name": secret.name,
623            "DeletionDate": deletion_date.timestamp_millis() as f64 / 1000.0,
624        });
625
626        Ok(AwsResponse::ok_json(response))
627    }
628
629    fn restore_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
630        let body = req.json_body();
631        let secret_id = require_secret_id(&body)?;
632
633        let mut state = self.state.write();
634        let secret = self.find_secret_mut(&mut state, &secret_id)?;
635
636        // AWS allows restoring a secret that is not deleted (no-op)
637        secret.deleted = false;
638        secret.deletion_date = None;
639
640        let response = json!({
641            "ARN": secret.arn,
642            "Name": secret.name,
643        });
644
645        Ok(AwsResponse::ok_json(response))
646    }
647
648    fn describe_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
649        let body = req.json_body();
650        let secret_id = require_secret_id(&body)?;
651
652        let state = self.state.read();
653        let secret = self.find_secret_ref(&state, &secret_id)?;
654
655        let mut response = json!({
656            "ARN": secret.arn,
657            "Name": secret.name,
658            "CreatedDate": secret.created_at.timestamp_millis() as f64 / 1000.0,
659            "LastChangedDate": secret.last_changed_at.timestamp_millis() as f64 / 1000.0,
660        });
661
662        if !secret.versions.is_empty() {
663            let mut version_ids_to_stages: serde_json::Map<String, Value> = serde_json::Map::new();
664            for (vid, version) in &secret.versions {
665                version_ids_to_stages.insert(vid.clone(), json!(version.stages));
666            }
667            response["VersionIdsToStages"] = Value::Object(version_ids_to_stages);
668        }
669
670        if let Some(ref desc) = secret.description {
671            if !desc.is_empty() {
672                response["Description"] = json!(desc);
673            }
674        }
675
676        if secret.tags_ever_set || !secret.tags.is_empty() {
677            response["Tags"] = json!(tags_to_json(&secret.tags));
678        }
679
680        if let Some(ref kms) = secret.kms_key_id {
681            response["KmsKeyId"] = json!(kms);
682        }
683        if secret.deleted {
684            response["DeletedDate"] = json!(secret
685                .deletion_date
686                .map(|d| d.timestamp_millis() as f64 / 1000.0));
687        }
688        if let Some(rotation_enabled) = secret.rotation_enabled {
689            response["RotationEnabled"] = json!(rotation_enabled);
690        }
691        if let Some(ref lambda_arn) = secret.rotation_lambda_arn {
692            response["RotationLambdaARN"] = json!(lambda_arn);
693        }
694        if let Some(ref rules) = secret.rotation_rules {
695            let mut rules_json = json!({});
696            if let Some(days) = rules.automatically_after_days {
697                rules_json["AutomaticallyAfterDays"] = json!(days);
698            }
699            response["RotationRules"] = rules_json;
700        }
701        if let Some(last_rotated) = secret.last_rotated_at {
702            response["LastRotatedDate"] = json!(last_rotated.timestamp_millis() as f64 / 1000.0);
703        }
704        // Calculate NextRotationDate if rotation is enabled
705        if secret.rotation_enabled == Some(true) {
706            if let Some(ref rules) = secret.rotation_rules {
707                if let Some(days) = rules.automatically_after_days {
708                    let base = secret.last_rotated_at.unwrap_or(secret.created_at);
709                    let next = base + chrono::Duration::days(days);
710                    response["NextRotationDate"] = json!(next.timestamp_millis() as f64 / 1000.0);
711                }
712            }
713        }
714
715        Ok(AwsResponse::ok_json(response))
716    }
717
718    fn list_secrets(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
719        let body = req.json_body();
720        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
721        validate_optional_range_i64("maxResults", body["MaxResults"].as_i64(), 1, 100)?;
722        validate_optional_enum("sortBy", body["SortBy"].as_str(), &["name", "created-date"])?;
723        validate_optional_enum("sortOrder", body["SortOrder"].as_str(), &["asc", "desc"])?;
724        let max_results = body["MaxResults"].as_i64().unwrap_or(100) as usize;
725        let next_token = body["NextToken"].as_str();
726        let filters = body["Filters"].as_array();
727        let include_deleted = body["IncludePlannedDeletion"].as_bool().unwrap_or(false);
728
729        // Validate filters
730        if let Some(filters) = filters {
731            for filter in filters {
732                let key = filter["Key"].as_str().unwrap_or("");
733                let values = filter["Values"].as_array();
734
735                if key.is_empty() {
736                    return Err(AwsServiceError::aws_error(
737                        StatusCode::BAD_REQUEST,
738                        "InvalidParameterException",
739                        "Invalid filter key",
740                    ));
741                }
742
743                let valid_keys = [
744                    "all",
745                    "name",
746                    "tag-key",
747                    "description",
748                    "tag-value",
749                    "owning-service",
750                    "primary-region",
751                ];
752                if !valid_keys.contains(&key) {
753                    return Err(AwsServiceError::aws_error(
754                        StatusCode::BAD_REQUEST,
755                        "ValidationException",
756                        format!(
757                            "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]",
758                            key
759                        ),
760                    ));
761                }
762
763                if values.is_none() || values.unwrap().is_empty() {
764                    return Err(AwsServiceError::aws_error(
765                        StatusCode::BAD_REQUEST,
766                        "InvalidParameterException",
767                        format!("Invalid filter values for key: {key}"),
768                    ));
769                }
770            }
771        }
772
773        let state = self.state.read();
774
775        let mut secrets: Vec<&Secret> = state
776            .secrets
777            .values()
778            .filter(|s| {
779                // Exclude deleted unless IncludePlannedDeletion
780                if s.deleted && !include_deleted {
781                    return false;
782                }
783
784                if let Some(filters) = filters {
785                    for filter in filters {
786                        let key = filter["Key"].as_str().unwrap_or("");
787                        let values: Vec<&str> = filter["Values"]
788                            .as_array()
789                            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
790                            .unwrap_or_default();
791
792                        let matches = match key {
793                            "name" => filter_name(s, &values),
794                            "description" => filter_description(s, &values),
795                            "tag-key" => filter_tag_key(s, &values),
796                            "tag-value" => filter_tag_value(s, &values),
797                            "all" => filter_all(s, &values),
798                            "owning-service" => false,
799                            "primary-region" => false,
800                            _ => true,
801                        };
802
803                        if !matches {
804                            return false;
805                        }
806                    }
807                }
808                true
809            })
810            .collect();
811        secrets.sort_by(|a, b| a.created_at.cmp(&b.created_at));
812
813        // Simple pagination with name-based token
814        let start_idx = if let Some(token) = next_token {
815            secrets.iter().position(|s| s.name == token).unwrap_or(0)
816        } else {
817            0
818        };
819
820        let page: Vec<Value> = secrets
821            .iter()
822            .skip(start_idx)
823            .take(max_results)
824            .map(|s| {
825                let mut entry = json!({
826                    "ARN": s.arn,
827                    "Name": s.name,
828                    "CreatedDate": s.created_at.timestamp_millis() as f64 / 1000.0,
829                    "LastChangedDate": s.last_changed_at.timestamp_millis() as f64 / 1000.0,
830                });
831
832                if !s.versions.is_empty() {
833                    let mut version_ids_to_stages: serde_json::Map<String, Value> =
834                        serde_json::Map::new();
835                    for (vid, version) in &s.versions {
836                        version_ids_to_stages.insert(vid.clone(), json!(version.stages));
837                    }
838                    entry["SecretVersionsToStages"] = Value::Object(version_ids_to_stages);
839                }
840
841                if let Some(ref desc) = s.description {
842                    if !desc.is_empty() {
843                        entry["Description"] = json!(desc);
844                    }
845                }
846
847                if s.tags_ever_set || !s.tags.is_empty() {
848                    entry["Tags"] = json!(tags_to_json(&s.tags));
849                }
850
851                if let Some(ref kms) = s.kms_key_id {
852                    entry["KmsKeyId"] = json!(kms);
853                }
854                if s.deleted {
855                    entry["DeletedDate"] = json!(s
856                        .deletion_date
857                        .map(|d| d.timestamp_millis() as f64 / 1000.0));
858                }
859                entry
860            })
861            .collect();
862
863        let has_more = start_idx + max_results < secrets.len();
864        let mut response = json!({
865            "SecretList": page,
866        });
867        if has_more {
868            if let Some(next) = secrets.get(start_idx + max_results) {
869                response["NextToken"] = json!(next.name);
870            }
871        }
872
873        Ok(AwsResponse::ok_json(response))
874    }
875
876    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
877        let body = req.json_body();
878        let secret_id = require_secret_id(&body)?;
879
880        let new_tags = parse_tags(&body["Tags"]);
881
882        let mut state = self.state.write();
883        let secret = self.find_secret_mut(&mut state, &secret_id)?;
884
885        if !new_tags.is_empty() {
886            secret.tags_ever_set = true;
887        }
888        for (k, v) in new_tags {
889            // Update existing tag or add new one
890            if let Some(existing) = secret.tags.iter_mut().find(|(ek, _)| *ek == k) {
891                existing.1 = v;
892            } else {
893                secret.tags.push((k, v));
894            }
895        }
896
897        Ok(AwsResponse::json(StatusCode::OK, "{}"))
898    }
899
900    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
901        let body = req.json_body();
902        let secret_id = require_secret_id(&body)?;
903
904        let tag_keys: Vec<String> = body["TagKeys"]
905            .as_array()
906            .map(|arr| {
907                arr.iter()
908                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
909                    .collect()
910            })
911            .unwrap_or_default();
912
913        let mut state = self.state.write();
914        let secret = self.find_secret_mut(&mut state, &secret_id)?;
915
916        secret.tags.retain(|(k, _)| !tag_keys.contains(k));
917
918        Ok(AwsResponse::json(StatusCode::OK, "{}"))
919    }
920
921    fn list_secret_version_ids(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
922        let body = req.json_body();
923        let secret_id = require_secret_id(&body)?;
924        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
925
926        let state = self.state.read();
927        let secret = self.find_secret_ref(&state, &secret_id)?;
928
929        let versions: Vec<Value> = secret
930            .versions
931            .values()
932            .map(|v| {
933                json!({
934                    "VersionId": v.version_id,
935                    "VersionStages": v.stages,
936                    "CreatedDate": v.created_at.timestamp_millis() as f64 / 1000.0,
937                })
938            })
939            .collect();
940
941        let response = json!({
942            "ARN": secret.arn,
943            "Name": secret.name,
944            "Versions": versions,
945        });
946
947        Ok(AwsResponse::ok_json(response))
948    }
949
950    fn get_random_password(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
951        let body = req.json_body();
952        let length = body["PasswordLength"].as_i64().unwrap_or(32) as usize;
953
954        if length < 4 {
955            return Err(AwsServiceError::aws_error(
956                StatusCode::BAD_REQUEST,
957                "InvalidParameterException",
958                "InvalidParameterException",
959            ));
960        }
961        if length > 4096 {
962            return Err(AwsServiceError::aws_error(
963                StatusCode::BAD_REQUEST,
964                "InvalidParameterValue",
965                "InvalidParameterValue",
966            ));
967        }
968
969        let exclude_lowercase = body["ExcludeLowercase"].as_bool().unwrap_or(false);
970        let exclude_uppercase = body["ExcludeUppercase"].as_bool().unwrap_or(false);
971        let exclude_numbers = body["ExcludeNumbers"].as_bool().unwrap_or(false);
972        let exclude_punctuation = body["ExcludePunctuation"].as_bool().unwrap_or(false);
973        let include_space = body["IncludeSpace"].as_bool().unwrap_or(false);
974        let require_each = body["RequireEachIncludedType"].as_bool().unwrap_or(true);
975        validate_optional_string_length(
976            "excludeCharacters",
977            body["ExcludeCharacters"].as_str(),
978            0,
979            4096,
980        )?;
981        let exclude_chars = body["ExcludeCharacters"].as_str().unwrap_or("").to_string();
982
983        let lowercase = "abcdefghijklmnopqrstuvwxyz";
984        let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
985        let digits = "0123456789";
986        let punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
987
988        let mut char_pool = String::new();
989        let mut required_chars: Vec<String> = Vec::new();
990
991        if !exclude_lowercase {
992            let filtered: String = lowercase
993                .chars()
994                .filter(|c| !exclude_chars.contains(*c))
995                .collect();
996            if !filtered.is_empty() {
997                required_chars.push(filtered.clone());
998                char_pool.push_str(&filtered);
999            }
1000        }
1001        if !exclude_uppercase {
1002            let filtered: String = uppercase
1003                .chars()
1004                .filter(|c| !exclude_chars.contains(*c))
1005                .collect();
1006            if !filtered.is_empty() {
1007                required_chars.push(filtered.clone());
1008                char_pool.push_str(&filtered);
1009            }
1010        }
1011        if !exclude_numbers {
1012            let filtered: String = digits
1013                .chars()
1014                .filter(|c| !exclude_chars.contains(*c))
1015                .collect();
1016            if !filtered.is_empty() {
1017                required_chars.push(filtered.clone());
1018                char_pool.push_str(&filtered);
1019            }
1020        }
1021        if !exclude_punctuation {
1022            let filtered: String = punctuation
1023                .chars()
1024                .filter(|c| !exclude_chars.contains(*c))
1025                .collect();
1026            if !filtered.is_empty() {
1027                required_chars.push(filtered.clone());
1028                char_pool.push_str(&filtered);
1029            }
1030        }
1031        if include_space && !exclude_chars.contains(' ') {
1032            char_pool.push(' ');
1033        }
1034
1035        if char_pool.is_empty() {
1036            return Err(AwsServiceError::aws_error(
1037                StatusCode::BAD_REQUEST,
1038                "InvalidParameterException",
1039                "InvalidParameterException",
1040            ));
1041        }
1042
1043        let pool_bytes: Vec<char> = char_pool.chars().collect();
1044        let mut password = String::with_capacity(length);
1045
1046        // Use simple random generation
1047        if require_each {
1048            // First, ensure at least one character from each required category
1049            for category in &required_chars {
1050                let chars: Vec<char> = category.chars().collect();
1051                let idx = simple_random() % chars.len();
1052                password.push(chars[idx]);
1053            }
1054            if include_space && !exclude_chars.contains(' ') {
1055                password.push(' ');
1056            }
1057        }
1058
1059        // Fill the rest randomly
1060        while password.len() < length {
1061            let idx = simple_random() % pool_bytes.len();
1062            password.push(pool_bytes[idx]);
1063        }
1064
1065        // Shuffle the password (Fisher-Yates)
1066        let mut chars: Vec<char> = password.chars().collect();
1067        for i in (1..chars.len()).rev() {
1068            let j = simple_random() % (i + 1);
1069            chars.swap(i, j);
1070        }
1071        let password: String = chars.into_iter().take(length).collect();
1072
1073        let response = json!({
1074            "RandomPassword": password,
1075        });
1076
1077        Ok(AwsResponse::ok_json(response))
1078    }
1079
1080    fn rotate_secret(
1081        &self,
1082        req: &AwsRequest,
1083    ) -> Result<(AwsResponse, Option<RotationInvocation>), AwsServiceError> {
1084        let body = req.json_body();
1085        let secret_id = require_secret_id(&body)?;
1086
1087        // Validate ClientRequestToken
1088        if let Some(token) = body["ClientRequestToken"].as_str() {
1089            if token.len() < 32 || token.len() > 64 {
1090                return Err(AwsServiceError::aws_error(
1091                    StatusCode::BAD_REQUEST,
1092                    "InvalidParameterException",
1093                    "ClientRequestToken must be 32-64 characters long.",
1094                ));
1095            }
1096        }
1097
1098        // Validate RotationLambdaARN
1099        if let Some(arn) = body["RotationLambdaARN"].as_str() {
1100            if arn.len() > 2048 {
1101                return Err(AwsServiceError::aws_error(
1102                    StatusCode::BAD_REQUEST,
1103                    "InvalidParameterException",
1104                    "RotationLambdaARN length must be less than or equal to 2048.",
1105                ));
1106            }
1107        }
1108
1109        // Validate RotationRules
1110        if let Some(rules) = body["RotationRules"].as_object() {
1111            if let Some(days) = rules.get("AutomaticallyAfterDays").and_then(|v| v.as_i64()) {
1112                if !(1..=1000).contains(&days) {
1113                    return Err(AwsServiceError::aws_error(
1114                        StatusCode::BAD_REQUEST,
1115                        "InvalidParameterException",
1116                        "RotationRules.AutomaticallyAfterDays must be within 1-1000.",
1117                    ));
1118                }
1119            }
1120        }
1121
1122        let mut state = self.state.write();
1123        let secret = self.find_secret_mut(&mut state, &secret_id)?;
1124
1125        if secret.deleted {
1126            return Err(AwsServiceError::aws_error(
1127                StatusCode::BAD_REQUEST,
1128                "InvalidRequestException",
1129                "You can't perform this operation on the secret because it was marked for deletion.",
1130            ));
1131        }
1132
1133        // Set rotation config
1134        if let Some(lambda_arn) = body["RotationLambdaARN"].as_str() {
1135            secret.rotation_lambda_arn = Some(lambda_arn.to_string());
1136        }
1137
1138        if let Some(rules) = body["RotationRules"].as_object() {
1139            let days = rules.get("AutomaticallyAfterDays").and_then(|v| v.as_i64());
1140            secret.rotation_rules = Some(RotationRules {
1141                automatically_after_days: days,
1142            });
1143        }
1144
1145        secret.rotation_enabled = Some(true);
1146        let now = Utc::now();
1147        secret.last_rotated_at = Some(now);
1148        secret.last_changed_at = now;
1149
1150        let version_id = body["ClientRequestToken"]
1151            .as_str()
1152            .map(|s| s.to_string())
1153            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
1154
1155        let has_lambda =
1156            body["RotationLambdaARN"].as_str().is_some() || secret.rotation_lambda_arn.is_some();
1157        let lambda_arn = secret.rotation_lambda_arn.clone();
1158
1159        // If the secret has a value, perform rotation
1160        let mut invocation = None;
1161        if let Some(current_vid) = secret.current_version_id.clone() {
1162            let current_value = secret.versions.get(&current_vid).cloned();
1163
1164            if let Some(cv) = current_value {
1165                if has_lambda {
1166                    // With Lambda: create AWSPENDING version for Lambda to process
1167                    let version = SecretVersion {
1168                        version_id: version_id.clone(),
1169                        secret_string: cv.secret_string.clone(),
1170                        secret_binary: cv.secret_binary.clone(),
1171                        stages: vec!["AWSPENDING".to_string()],
1172                        created_at: now,
1173                    };
1174                    secret.versions.insert(version_id.clone(), version);
1175
1176                    // Schedule Lambda invocation
1177                    if let Some(ref arn) = lambda_arn {
1178                        invocation = Some(RotationInvocation {
1179                            lambda_arn: arn.clone(),
1180                            secret_id: secret.arn.clone(),
1181                            client_request_token: version_id.clone(),
1182                        });
1183                    }
1184                } else {
1185                    // Without Lambda: simple rotation - new version becomes AWSCURRENT
1186                    // Move old version to AWSPREVIOUS
1187                    if let Some(old_v) = secret.versions.get_mut(&current_vid) {
1188                        old_v.stages.retain(|s| s != "AWSCURRENT");
1189                        if !old_v.stages.contains(&"AWSPREVIOUS".to_string()) {
1190                            old_v.stages.push("AWSPREVIOUS".to_string());
1191                        }
1192                    }
1193                    let version = SecretVersion {
1194                        version_id: version_id.clone(),
1195                        secret_string: cv.secret_string.clone(),
1196                        secret_binary: cv.secret_binary.clone(),
1197                        stages: vec!["AWSCURRENT".to_string()],
1198                        created_at: now,
1199                    };
1200                    secret.versions.insert(version_id.clone(), version);
1201                    secret.current_version_id = Some(version_id.clone());
1202                }
1203            }
1204        }
1205
1206        let response = json!({
1207            "ARN": secret.arn,
1208            "Name": secret.name,
1209            "VersionId": version_id,
1210        });
1211
1212        Ok((AwsResponse::ok_json(response), invocation))
1213    }
1214
1215    fn cancel_rotate_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1216        let body = req.json_body();
1217        let secret_id = require_secret_id(&body)?;
1218
1219        let mut state = self.state.write();
1220        let secret = self.find_secret_mut(&mut state, &secret_id)?;
1221
1222        if secret.deleted {
1223            return Err(AwsServiceError::aws_error(
1224                StatusCode::BAD_REQUEST,
1225                "InvalidRequestException",
1226                "You can't perform this operation on the secret because it was marked for deletion.",
1227            ));
1228        }
1229
1230        if secret.rotation_enabled != Some(true) {
1231            return Err(AwsServiceError::aws_error(
1232                StatusCode::BAD_REQUEST,
1233                "InvalidRequestException",
1234                "You can't cancel rotation for a secret that does not have rotation enabled.",
1235            ));
1236        }
1237
1238        secret.rotation_enabled = Some(false);
1239
1240        let response = json!({
1241            "ARN": secret.arn,
1242            "Name": secret.name,
1243        });
1244
1245        Ok(AwsResponse::ok_json(response))
1246    }
1247
1248    fn update_secret_version_stage(
1249        &self,
1250        req: &AwsRequest,
1251    ) -> Result<AwsResponse, AwsServiceError> {
1252        let body = req.json_body();
1253        let secret_id = require_secret_id(&body)?;
1254        let version_stage = body["VersionStage"]
1255            .as_str()
1256            .ok_or_else(|| {
1257                AwsServiceError::aws_error(
1258                    StatusCode::BAD_REQUEST,
1259                    "InvalidParameterException",
1260                    "VersionStage is required",
1261                )
1262            })?
1263            .to_string();
1264        validate_string_length("versionStage", &version_stage, 1, 256)?;
1265        validate_optional_string_length(
1266            "removeFromVersionId",
1267            body["RemoveFromVersionId"].as_str(),
1268            32,
1269            64,
1270        )?;
1271        validate_optional_string_length(
1272            "moveToVersionId",
1273            body["MoveToVersionId"].as_str(),
1274            32,
1275            64,
1276        )?;
1277
1278        let move_to = body["MoveToVersionId"].as_str().map(|s| s.to_string());
1279        let remove_from = body["RemoveFromVersionId"].as_str().map(|s| s.to_string());
1280
1281        let mut state = self.state.write();
1282        let secret = self.find_secret_mut(&mut state, &secret_id)?;
1283
1284        // Validate: if moving AWSCURRENT, must specify RemoveFromVersionId
1285        if version_stage == "AWSCURRENT" && move_to.is_some() && remove_from.is_none() {
1286            // Find the version that currently has AWSCURRENT
1287            let current_holder = secret
1288                .versions
1289                .iter()
1290                .find(|(_, v)| v.stages.contains(&"AWSCURRENT".to_string()))
1291                .map(|(id, _)| id.clone());
1292
1293            if let Some(current_vid) = current_holder {
1294                return Err(AwsServiceError::aws_error(
1295                    StatusCode::BAD_REQUEST,
1296                    "InvalidParameterException",
1297                    format!(
1298                        "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."
1299                    ),
1300                ));
1301            }
1302        }
1303
1304        // Remove stage from specified version
1305        if let Some(ref remove_vid) = remove_from {
1306            if let Some(version) = secret.versions.get_mut(remove_vid) {
1307                version.stages.retain(|s| s != &version_stage);
1308                // If moving AWSCURRENT away, add AWSPREVIOUS and remove from others
1309                if version_stage == "AWSCURRENT" {
1310                    // Remove AWSPREVIOUS from all other versions first
1311                    for (id, v) in secret.versions.iter_mut() {
1312                        if id != remove_vid {
1313                            v.stages.retain(|s| s != "AWSPREVIOUS");
1314                        }
1315                    }
1316                    // Now add AWSPREVIOUS to the version losing AWSCURRENT
1317                    if let Some(v) = secret.versions.get_mut(remove_vid) {
1318                        if !v.stages.contains(&"AWSPREVIOUS".to_string()) {
1319                            v.stages.push("AWSPREVIOUS".to_string());
1320                        }
1321                    }
1322                }
1323            }
1324        }
1325
1326        // Add stage to specified version
1327        if let Some(ref move_vid) = move_to {
1328            if let Some(version) = secret.versions.get_mut(move_vid) {
1329                if !version.stages.contains(&version_stage) {
1330                    version.stages.push(version_stage.clone());
1331                }
1332            }
1333            // Update current_version_id if we moved AWSCURRENT
1334            if version_stage == "AWSCURRENT" {
1335                secret.current_version_id = Some(move_vid.clone());
1336            }
1337        }
1338
1339        let response = json!({
1340            "ARN": secret.arn,
1341            "Name": secret.name,
1342        });
1343
1344        Ok(AwsResponse::ok_json(response))
1345    }
1346
1347    fn batch_get_secret_value(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1348        let body = req.json_body();
1349        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
1350        let secret_id_list = body["SecretIdList"].as_array();
1351        let filters = body["Filters"].as_array();
1352        let max_results = body.get("MaxResults").and_then(|v| v.as_i64());
1353
1354        // Validate: can't use both SecretIdList and Filters
1355        if secret_id_list.is_some() && filters.is_some() {
1356            return Err(AwsServiceError::aws_error(
1357                StatusCode::BAD_REQUEST,
1358                "InvalidParameterException",
1359                "Either 'SecretIdList' or 'Filters' must be provided, but not both.",
1360            ));
1361        }
1362
1363        // Validate: MaxResults requires Filters
1364        if max_results.is_some() && filters.is_none() {
1365            return Err(AwsServiceError::aws_error(
1366                StatusCode::BAD_REQUEST,
1367                "InvalidParameterException",
1368                "'Filters' not specified. 'Filters' must also be specified when 'MaxResults' is provided.",
1369            ));
1370        }
1371
1372        let state = self.state.read();
1373        let mut secret_values: Vec<Value> = Vec::new();
1374        let mut errors: Vec<Value> = Vec::new();
1375
1376        if let Some(id_list) = secret_id_list {
1377            for id_val in id_list {
1378                let sid = id_val.as_str().unwrap_or("");
1379                match self.find_secret_ref(&state, sid) {
1380                    Ok(secret) => {
1381                        if secret.deleted {
1382                            errors.push(json!({
1383                                "SecretId": sid,
1384                                "ErrorCode": "InvalidRequestException",
1385                                "Message": "Secret is currently marked deleted. Secret can be recovered with RestoreSecret. Secret is currently marked deleted.",
1386                            }));
1387                        } else if let Some(ref current_vid) = secret.current_version_id {
1388                            if let Some(version) = secret.versions.get(current_vid) {
1389                                let mut entry = json!({
1390                                    "ARN": secret.arn,
1391                                    "Name": secret.name,
1392                                    "VersionId": version.version_id,
1393                                    "VersionStages": version.stages,
1394                                    "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
1395                                });
1396                                if let Some(ref s) = version.secret_string {
1397                                    entry["SecretString"] = json!(s);
1398                                }
1399                                if let Some(ref b) = version.secret_binary {
1400                                    entry["SecretBinary"] = json!(base64_encode(b));
1401                                }
1402                                secret_values.push(entry);
1403                            } else {
1404                                errors.push(json!({
1405                                    "SecretId": sid,
1406                                    "ErrorCode": "ResourceNotFoundException",
1407                                    "Message": "Secrets Manager can't find the specified secret.",
1408                                }));
1409                            }
1410                        } else {
1411                            errors.push(json!({
1412                                "SecretId": sid,
1413                                "ErrorCode": "ResourceNotFoundException",
1414                                "Message": "Secrets Manager can't find the specified secret.",
1415                            }));
1416                        }
1417                    }
1418                    Err(_) => {
1419                        errors.push(json!({
1420                            "SecretId": sid,
1421                            "ErrorCode": "ResourceNotFoundException",
1422                            "Message": "Secrets Manager can't find the specified secret.",
1423                        }));
1424                    }
1425                }
1426            }
1427        } else if let Some(filters) = filters {
1428            // Get secrets matching filters
1429            let matching: Vec<&Secret> = state
1430                .secrets
1431                .values()
1432                .filter(|s| {
1433                    if s.deleted {
1434                        return false;
1435                    }
1436                    for filter in filters {
1437                        let key = filter["Key"].as_str().unwrap_or("");
1438                        let values: Vec<&str> = filter["Values"]
1439                            .as_array()
1440                            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
1441                            .unwrap_or_default();
1442                        let matches = match key {
1443                            "name" => filter_name(s, &values),
1444                            "description" => filter_description(s, &values),
1445                            "tag-key" => filter_tag_key(s, &values),
1446                            "tag-value" => filter_tag_value(s, &values),
1447                            "all" => filter_all(s, &values),
1448                            _ => true,
1449                        };
1450                        if !matches {
1451                            return false;
1452                        }
1453                    }
1454                    true
1455                })
1456                .collect();
1457
1458            let limit = max_results.unwrap_or(100) as usize;
1459            let mut no_value_found = false;
1460            let mut matching = matching;
1461            matching.sort_by(|a, b| a.name.cmp(&b.name));
1462
1463            for secret in matching.iter().take(limit) {
1464                if let Some(ref current_vid) = secret.current_version_id {
1465                    if let Some(version) = secret.versions.get(current_vid) {
1466                        let mut entry = json!({
1467                            "ARN": secret.arn,
1468                            "Name": secret.name,
1469                            "VersionId": version.version_id,
1470                            "VersionStages": version.stages,
1471                            "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
1472                        });
1473                        if let Some(ref s) = version.secret_string {
1474                            entry["SecretString"] = json!(s);
1475                        }
1476                        if let Some(ref b) = version.secret_binary {
1477                            entry["SecretBinary"] = json!(base64_encode(b));
1478                        }
1479                        secret_values.push(entry);
1480                    } else {
1481                        no_value_found = true;
1482                    }
1483                } else {
1484                    no_value_found = true;
1485                }
1486            }
1487
1488            if no_value_found && secret_values.is_empty() {
1489                return Err(AwsServiceError::aws_error(
1490                    StatusCode::NOT_FOUND,
1491                    "ResourceNotFoundException",
1492                    "Secrets Manager can't find the specified secret.",
1493                ));
1494            }
1495        }
1496
1497        let mut response = json!({
1498            "SecretValues": secret_values,
1499            "Errors": errors,
1500        });
1501
1502        // Remove empty arrays
1503        if errors.is_empty() {
1504            response.as_object_mut().unwrap().remove("Errors");
1505        }
1506
1507        Ok(AwsResponse::ok_json(response))
1508    }
1509
1510    fn get_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1511        let body = req.json_body();
1512        let secret_id = require_secret_id(&body)?;
1513
1514        let state = self.state.read();
1515        let secret = self.find_secret_ref(&state, &secret_id)?;
1516
1517        let mut response = json!({
1518            "ARN": secret.arn,
1519            "Name": secret.name,
1520        });
1521
1522        if let Some(ref policy) = secret.resource_policy {
1523            response["ResourcePolicy"] = json!(policy);
1524        }
1525
1526        Ok(AwsResponse::ok_json(response))
1527    }
1528
1529    fn validate_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1530        let body = req.json_body();
1531        validate_optional_string_length("secretId", body["SecretId"].as_str(), 1, 2048)?;
1532        validate_required("ResourcePolicy", &body["ResourcePolicy"])?;
1533        let policy_str = body["ResourcePolicy"].as_str().ok_or_else(|| {
1534            AwsServiceError::aws_error(
1535                StatusCode::BAD_REQUEST,
1536                "InvalidParameterException",
1537                "ResourcePolicy must be a string",
1538            )
1539        })?;
1540        validate_string_length("resourcePolicy", policy_str, 1, 20480)?;
1541
1542        // If SecretId is provided, verify the secret exists
1543        if let Some(secret_id) = body["SecretId"].as_str() {
1544            let state = self.state.read();
1545            self.find_secret_key(&state, secret_id)?;
1546        }
1547
1548        let response = json!({
1549            "PolicyValidationPassed": true,
1550            "ValidationErrors": [],
1551        });
1552        Ok(AwsResponse::ok_json(response))
1553    }
1554
1555    fn put_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1556        let body = req.json_body();
1557        let secret_id = require_secret_id(&body)?;
1558        validate_required("ResourcePolicy", &body["ResourcePolicy"])?;
1559        validate_optional_string_length(
1560            "resourcePolicy",
1561            body["ResourcePolicy"].as_str(),
1562            1,
1563            20480,
1564        )?;
1565        let policy = body["ResourcePolicy"].as_str().map(|s| s.to_string());
1566
1567        let mut state = self.state.write();
1568        let secret = self.find_secret_mut(&mut state, &secret_id)?;
1569        secret.resource_policy = policy;
1570
1571        let response = json!({
1572            "ARN": secret.arn,
1573            "Name": secret.name,
1574        });
1575
1576        Ok(AwsResponse::ok_json(response))
1577    }
1578
1579    fn delete_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1580        let body = req.json_body();
1581        let secret_id = require_secret_id(&body)?;
1582
1583        let mut state = self.state.write();
1584        let secret = self.find_secret_mut(&mut state, &secret_id)?;
1585        secret.resource_policy = None;
1586
1587        let response = json!({
1588            "ARN": secret.arn,
1589            "Name": secret.name,
1590        });
1591
1592        Ok(AwsResponse::ok_json(response))
1593    }
1594
1595    fn replicate_secret_to_regions(
1596        &self,
1597        req: &AwsRequest,
1598    ) -> Result<AwsResponse, AwsServiceError> {
1599        let body = req.json_body();
1600        let secret_id = require_secret_id(&body)?;
1601
1602        let state = self.state.read();
1603        let secret = self.find_secret_ref(&state, &secret_id)?;
1604
1605        let response = json!({
1606            "ARN": secret.arn,
1607            "ReplicationStatus": [],
1608        });
1609        Ok(AwsResponse::ok_json(response))
1610    }
1611
1612    fn remove_regions_from_replication(
1613        &self,
1614        req: &AwsRequest,
1615    ) -> Result<AwsResponse, AwsServiceError> {
1616        let body = req.json_body();
1617        let secret_id = require_secret_id(&body)?;
1618
1619        let state = self.state.read();
1620        let secret = self.find_secret_ref(&state, &secret_id)?;
1621
1622        let response = json!({
1623            "ARN": secret.arn,
1624            "ReplicationStatus": [],
1625        });
1626        Ok(AwsResponse::ok_json(response))
1627    }
1628
1629    fn stop_replication_to_replica(
1630        &self,
1631        req: &AwsRequest,
1632    ) -> Result<AwsResponse, AwsServiceError> {
1633        let body = req.json_body();
1634        let secret_id = require_secret_id(&body)?;
1635
1636        let state = self.state.read();
1637        let secret = self.find_secret_ref(&state, &secret_id)?;
1638
1639        let response = json!({
1640            "ARN": secret.arn,
1641        });
1642        Ok(AwsResponse::ok_json(response))
1643    }
1644
1645    /// Find a secret by name, full ARN, or partial ARN (mutable).
1646    fn find_secret_mut<'a>(
1647        &self,
1648        state: &'a mut crate::state::SecretsManagerState,
1649        secret_id: &str,
1650    ) -> Result<&'a mut Secret, AwsServiceError> {
1651        let key = self.find_secret_key(state, secret_id)?;
1652        Ok(state.secrets.get_mut(&key).unwrap())
1653    }
1654
1655    fn find_secret_key(
1656        &self,
1657        state: &crate::state::SecretsManagerState,
1658        secret_id: &str,
1659    ) -> Result<String, AwsServiceError> {
1660        if state.secrets.contains_key(secret_id) {
1661            return Ok(secret_id.to_string());
1662        }
1663
1664        for secret in state.secrets.values() {
1665            if secret.arn == secret_id {
1666                return Ok(secret.name.clone());
1667            }
1668        }
1669
1670        if secret_id.starts_with("arn:aws:secretsmanager:") {
1671            for secret in state.secrets.values() {
1672                if secret.arn.starts_with(secret_id) {
1673                    return Ok(secret.name.clone());
1674                }
1675            }
1676        }
1677
1678        Err(AwsServiceError::aws_error(
1679            StatusCode::NOT_FOUND,
1680            "ResourceNotFoundException",
1681            "Secrets Manager can't find the specified secret.",
1682        ))
1683    }
1684
1685    /// Find a secret by name, full ARN, or partial ARN (immutable).
1686    fn find_secret_ref<'a>(
1687        &self,
1688        state: &'a crate::state::SecretsManagerState,
1689        secret_id: &str,
1690    ) -> Result<&'a Secret, AwsServiceError> {
1691        if let Some(secret) = state.secrets.get(secret_id) {
1692            return Ok(secret);
1693        }
1694
1695        // Search by full ARN
1696        for secret in state.secrets.values() {
1697            if secret.arn == secret_id {
1698                return Ok(secret);
1699            }
1700        }
1701
1702        // Search by partial ARN
1703        if secret_id.starts_with("arn:aws:secretsmanager:") {
1704            for secret in state.secrets.values() {
1705                if secret.arn.starts_with(secret_id) {
1706                    return Ok(secret);
1707                }
1708            }
1709        }
1710
1711        Err(AwsServiceError::aws_error(
1712            StatusCode::NOT_FOUND,
1713            "ResourceNotFoundException",
1714            "Secrets Manager can't find the specified secret.",
1715        ))
1716    }
1717}
1718
1719/// Parsed + validated inputs for `CreateSecret`.
1720struct CreateSecretInput {
1721    name: String,
1722    client_request_token: Option<String>,
1723    description: Option<String>,
1724    kms_key_id: Option<String>,
1725    secret_string: Option<String>,
1726    secret_binary: Option<Vec<u8>>,
1727    tags: Vec<(String, String)>,
1728}
1729
1730impl CreateSecretInput {
1731    fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
1732        validate_required("Name", &body["Name"])?;
1733        let name = body["Name"]
1734            .as_str()
1735            .ok_or_else(|| {
1736                AwsServiceError::aws_error(
1737                    StatusCode::BAD_REQUEST,
1738                    "InvalidParameterException",
1739                    "Name is required",
1740                )
1741            })?
1742            .to_string();
1743        validate_string_length("name", &name, 1, 512)?;
1744        validate_optional_string_length(
1745            "clientRequestToken",
1746            body["ClientRequestToken"].as_str(),
1747            32,
1748            64,
1749        )?;
1750        validate_optional_string_length("description", body["Description"].as_str(), 0, 2048)?;
1751        validate_optional_string_length("kmsKeyId", body["KmsKeyId"].as_str(), 0, 2048)?;
1752        validate_optional_string_length("secretString", body["SecretString"].as_str(), 1, 65536)?;
1753
1754        Ok(Self {
1755            name,
1756            client_request_token: body["ClientRequestToken"].as_str().map(|s| s.to_string()),
1757            description: body["Description"].as_str().map(|s| s.to_string()),
1758            kms_key_id: body["KmsKeyId"].as_str().map(|s| s.to_string()),
1759            secret_string: body["SecretString"].as_str().map(|s| s.to_string()),
1760            secret_binary: body["SecretBinary"].as_str().and_then(base64_decode),
1761            tags: parse_tags(&body["Tags"]),
1762        })
1763    }
1764}
1765
1766fn require_secret_id(body: &Value) -> Result<String, AwsServiceError> {
1767    let id = body["SecretId"].as_str().ok_or_else(|| {
1768        AwsServiceError::aws_error(
1769            StatusCode::BAD_REQUEST,
1770            "InvalidParameterException",
1771            "SecretId is required",
1772        )
1773    })?;
1774    validate_string_length("secretId", id, 1, 2048)?;
1775    Ok(id.to_string())
1776}
1777
1778fn parse_tags(tags_val: &Value) -> Vec<(String, String)> {
1779    tags_val
1780        .as_array()
1781        .map(|arr| {
1782            arr.iter()
1783                .filter_map(|t| {
1784                    let key = t["Key"].as_str()?;
1785                    let value = t["Value"].as_str()?;
1786                    Some((key.to_string(), value.to_string()))
1787                })
1788                .collect()
1789        })
1790        .unwrap_or_default()
1791}
1792
1793fn tags_to_json(tags: &[(String, String)]) -> Vec<Value> {
1794    tags.iter()
1795        .map(|(k, v)| json!({"Key": k, "Value": v}))
1796        .collect()
1797}
1798
1799/// Split text into words for secret name filtering.
1800/// Splits on special characters (/ - _ + = . @) and camelCase.
1801/// If multiple different special characters are present, doesn't split.
1802/// Spaces are always split on first.
1803fn split_words(text: &str) -> Vec<String> {
1804    // First split on whitespace, then apply word splitting to each part
1805    let mut all_words = Vec::new();
1806    for space_part in text.split_whitespace() {
1807        all_words.extend(split_words_no_space(space_part));
1808    }
1809    all_words
1810}
1811
1812fn split_words_no_space(text: &str) -> Vec<String> {
1813    let special_chars = ['/', '-', '_', '+', '=', '.', '@'];
1814
1815    // Check if text is just a special char
1816    if text.len() == 1 && special_chars.contains(&text.chars().next().unwrap_or(' ')) {
1817        return vec![];
1818    }
1819
1820    // Find which special chars are present
1821    let present: Vec<char> = special_chars
1822        .iter()
1823        .filter(|&&c| text.contains(c))
1824        .copied()
1825        .collect();
1826
1827    if present.len() > 1 {
1828        // Multiple different special chars: don't split
1829        return vec![text.to_string()];
1830    }
1831
1832    if present.len() == 1 {
1833        let ch = present[0];
1834        let parts: Vec<&str> = text.split(ch).filter(|s| !s.is_empty()).collect();
1835        let mut result = Vec::new();
1836        for part in parts {
1837            result.extend(split_by_uppercase(part));
1838        }
1839        return result;
1840    }
1841
1842    // No special chars: split by uppercase
1843    split_by_uppercase(text)
1844}
1845
1846/// Split a string by the pattern: a non-lowercase char followed by one or more lowercase chars.
1847/// Equivalent to Python regex: re.split(r"([^a-z][a-z]+)", s)
1848fn split_by_uppercase(text: &str) -> Vec<String> {
1849    // Implement the equivalent of Python's re.split(r"([^a-z][a-z]+)", text)
1850    // re.split with capturing group returns: [before, match, between, match, ..., after]
1851    let chars: Vec<char> = text.chars().collect();
1852    let mut words = Vec::new();
1853    let mut last_end = 0;
1854    let mut i = 0;
1855
1856    while i < chars.len() {
1857        // Try to find pattern: [^a-z][a-z]+
1858        if !chars[i].is_ascii_lowercase()
1859            && i + 1 < chars.len()
1860            && chars[i + 1].is_ascii_lowercase()
1861        {
1862            // Text before this match (between previous match end and this match start)
1863            if i > last_end {
1864                let between: String = chars[last_end..i].iter().collect();
1865                let trimmed = between.trim().to_string();
1866                if !trimmed.is_empty() {
1867                    words.push(trimmed);
1868                }
1869            }
1870
1871            // The match itself
1872            let start = i;
1873            i += 2;
1874            while i < chars.len() && chars[i].is_ascii_lowercase() {
1875                i += 1;
1876            }
1877            let word: String = chars[start..i].iter().collect();
1878            let trimmed = word.trim().to_string();
1879            if !trimmed.is_empty() {
1880                words.push(trimmed);
1881            }
1882            last_end = i;
1883        } else {
1884            i += 1;
1885        }
1886    }
1887
1888    // Text after last match
1889    if last_end < chars.len() {
1890        let after: String = chars[last_end..].iter().collect();
1891        let trimmed = after.trim().to_string();
1892        if !trimmed.is_empty() {
1893            words.push(trimmed);
1894        }
1895    }
1896
1897    words
1898}
1899
1900/// Match a pattern against a value.
1901/// - match_prefix=true: simple prefix match on the full string
1902/// - match_prefix=false: split both into words, all pattern words must prefix-match some value word
1903fn match_pattern(pattern: &str, value: &str, match_prefix: bool, case_sensitive: bool) -> bool {
1904    if match_prefix {
1905        if case_sensitive {
1906            value.starts_with(pattern)
1907        } else {
1908            value.to_lowercase().starts_with(&pattern.to_lowercase())
1909        }
1910    } else {
1911        let mut pattern_words = split_words(pattern);
1912        if pattern_words.is_empty() {
1913            return false;
1914        }
1915        let mut value_words = split_words(value);
1916        if !case_sensitive {
1917            pattern_words = pattern_words.iter().map(|w| w.to_lowercase()).collect();
1918            value_words = value_words.iter().map(|w| w.to_lowercase()).collect();
1919        }
1920        for pw in &pattern_words {
1921            if !value_words.iter().any(|vw| vw.starts_with(pw.as_str())) {
1922                return false;
1923            }
1924        }
1925        true
1926    }
1927}
1928
1929/// The main matcher: check patterns against a list of strings.
1930/// Supports negation (!pattern), prefix matching, and case sensitivity.
1931fn matcher(patterns: &[&str], strings: &[&str], match_prefix: bool, case_sensitive: bool) -> bool {
1932    // First check negated patterns
1933    for pattern in patterns.iter().filter(|p| p.starts_with('!')) {
1934        let inner = &pattern[1..];
1935        for s in strings {
1936            if !match_pattern(inner, s, match_prefix, case_sensitive) {
1937                return true;
1938            }
1939        }
1940    }
1941
1942    // Then check positive patterns
1943    for pattern in patterns.iter().filter(|p| !p.starts_with('!')) {
1944        for s in strings {
1945            if match_pattern(pattern, s, match_prefix, case_sensitive) {
1946                return true;
1947            }
1948        }
1949    }
1950    false
1951}
1952
1953/// Name filter: prefix match, case sensitive
1954fn filter_name(secret: &Secret, values: &[&str]) -> bool {
1955    matcher(values, &[secret.name.as_str()], true, true)
1956}
1957
1958/// Description filter: word match, case insensitive
1959fn filter_description(secret: &Secret, values: &[&str]) -> bool {
1960    match secret.description.as_deref() {
1961        Some(desc) if !desc.is_empty() => matcher(values, &[desc], false, false),
1962        _ => false,
1963    }
1964}
1965
1966/// Tag key filter: prefix match, case sensitive
1967fn filter_tag_key(secret: &Secret, values: &[&str]) -> bool {
1968    if secret.tags.is_empty() {
1969        return false;
1970    }
1971    let keys: Vec<&str> = secret.tags.iter().map(|(k, _)| k.as_str()).collect();
1972    matcher(values, &keys, true, true)
1973}
1974
1975/// Tag value filter: prefix match, case sensitive
1976fn filter_tag_value(secret: &Secret, values: &[&str]) -> bool {
1977    if secret.tags.is_empty() {
1978        return false;
1979    }
1980    let vals: Vec<&str> = secret.tags.iter().map(|(_, v)| v.as_str()).collect();
1981    matcher(values, &vals, true, true)
1982}
1983
1984/// All filter: word match, case insensitive, across all fields
1985fn filter_all(secret: &Secret, values: &[&str]) -> bool {
1986    let mut attributes: Vec<&str> = vec![secret.name.as_str()];
1987    if let Some(ref desc) = secret.description {
1988        if !desc.is_empty() {
1989            attributes.push(desc.as_str());
1990        }
1991    }
1992    for (k, v) in &secret.tags {
1993        attributes.push(k.as_str());
1994        attributes.push(v.as_str());
1995    }
1996    matcher(values, &attributes, false, false)
1997}
1998
1999fn simple_random() -> usize {
2000    use std::collections::hash_map::RandomState;
2001    use std::hash::{BuildHasher, Hasher};
2002    let s = RandomState::new();
2003    let mut hasher = s.build_hasher();
2004    hasher.write_usize(0);
2005    hasher.finish() as usize
2006}
2007
2008#[async_trait]
2009impl AwsService for SecretsManagerService {
2010    fn service_name(&self) -> &str {
2011        "secretsmanager"
2012    }
2013
2014    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2015        match req.action.as_str() {
2016            "CreateSecret" => self.create_secret(&req),
2017            "GetSecretValue" => self.get_secret_value(&req),
2018            "PutSecretValue" => self.put_secret_value(&req),
2019            "UpdateSecret" => self.update_secret(&req),
2020            "DeleteSecret" => self.delete_secret(&req),
2021            "RestoreSecret" => self.restore_secret(&req),
2022            "DescribeSecret" => self.describe_secret(&req),
2023            "ListSecrets" => self.list_secrets(&req),
2024            "TagResource" => self.tag_resource(&req),
2025            "UntagResource" => self.untag_resource(&req),
2026            "ListSecretVersionIds" => self.list_secret_version_ids(&req),
2027            "GetRandomPassword" => self.get_random_password(&req),
2028            "RotateSecret" => {
2029                let (response, invocation) = self.rotate_secret(&req)?;
2030                if let Some(inv) = invocation {
2031                    if let Some(ref bus) = self.delivery_bus {
2032                        let bus = bus.clone();
2033                        // AWS invokes the rotation Lambda asynchronously for each step.
2034                        tokio::spawn(async move {
2035                            for step in &["createSecret", "setSecret", "testSecret", "finishSecret"]
2036                            {
2037                                let payload = serde_json::json!({
2038                                    "SecretId": inv.secret_id,
2039                                    "ClientRequestToken": inv.client_request_token,
2040                                    "Step": step,
2041                                });
2042                                let payload_str = payload.to_string();
2043                                match bus.invoke_lambda(&inv.lambda_arn, &payload_str).await {
2044                                    Some(Ok(_)) => {}
2045                                    Some(Err(e)) => {
2046                                        tracing::warn!(
2047                                            step = step,
2048                                            error = %e,
2049                                            "rotation Lambda invocation failed"
2050                                        );
2051                                    }
2052                                    None => {
2053                                        tracing::warn!(
2054                                            lambda_arn = %inv.lambda_arn,
2055                                            step = step,
2056                                            "rotation Lambda delivery not configured; \
2057                                             Lambda invocation skipped"
2058                                        );
2059                                        break;
2060                                    }
2061                                }
2062                            }
2063                        });
2064                    }
2065                }
2066                Ok(response)
2067            }
2068            "CancelRotateSecret" => self.cancel_rotate_secret(&req),
2069            "UpdateSecretVersionStage" => self.update_secret_version_stage(&req),
2070            "BatchGetSecretValue" => self.batch_get_secret_value(&req),
2071            "GetResourcePolicy" => self.get_resource_policy(&req),
2072            "PutResourcePolicy" => self.put_resource_policy(&req),
2073            "DeleteResourcePolicy" => self.delete_resource_policy(&req),
2074            "ValidateResourcePolicy" => self.validate_resource_policy(&req),
2075            "ReplicateSecretToRegions" => self.replicate_secret_to_regions(&req),
2076            "RemoveRegionsFromReplication" => self.remove_regions_from_replication(&req),
2077            "StopReplicationToReplica" => self.stop_replication_to_replica(&req),
2078            _ => Err(AwsServiceError::action_not_implemented(
2079                "secretsmanager",
2080                &req.action,
2081            )),
2082        }
2083    }
2084
2085    fn supported_actions(&self) -> &[&str] {
2086        &[
2087            "CreateSecret",
2088            "GetSecretValue",
2089            "PutSecretValue",
2090            "UpdateSecret",
2091            "DeleteSecret",
2092            "RestoreSecret",
2093            "DescribeSecret",
2094            "ListSecrets",
2095            "TagResource",
2096            "UntagResource",
2097            "ListSecretVersionIds",
2098            "GetRandomPassword",
2099            "RotateSecret",
2100            "CancelRotateSecret",
2101            "UpdateSecretVersionStage",
2102            "BatchGetSecretValue",
2103            "GetResourcePolicy",
2104            "PutResourcePolicy",
2105            "DeleteResourcePolicy",
2106            "ValidateResourcePolicy",
2107            "ReplicateSecretToRegions",
2108            "RemoveRegionsFromReplication",
2109            "StopReplicationToReplica",
2110        ]
2111    }
2112}
2113
2114fn base64_decode(input: &str) -> Option<Vec<u8>> {
2115    let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2116    let mut buf = Vec::new();
2117    let mut bits: u32 = 0;
2118    let mut count = 0;
2119    for &b in input.as_bytes() {
2120        if b == b'=' || b == b'\n' || b == b'\r' {
2121            continue;
2122        }
2123        let val = table.iter().position(|&c| c == b)? as u32;
2124        bits = (bits << 6) | val;
2125        count += 1;
2126        if count == 4 {
2127            buf.push((bits >> 16) as u8);
2128            buf.push((bits >> 8) as u8);
2129            buf.push(bits as u8);
2130            bits = 0;
2131            count = 0;
2132        }
2133    }
2134    match count {
2135        2 => {
2136            bits <<= 12;
2137            buf.push((bits >> 16) as u8);
2138        }
2139        3 => {
2140            bits <<= 6;
2141            buf.push((bits >> 16) as u8);
2142            buf.push((bits >> 8) as u8);
2143        }
2144        _ => {}
2145    }
2146    Some(buf)
2147}
2148
2149fn base64_encode(input: &[u8]) -> String {
2150    let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2151    let mut result = String::new();
2152    for chunk in input.chunks(3) {
2153        let b0 = chunk[0] as u32;
2154        let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
2155        let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
2156        let triple = (b0 << 16) | (b1 << 8) | b2;
2157        result.push(table[((triple >> 18) & 0x3F) as usize] as char);
2158        result.push(table[((triple >> 12) & 0x3F) as usize] as char);
2159        if chunk.len() > 1 {
2160            result.push(table[((triple >> 6) & 0x3F) as usize] as char);
2161        } else {
2162            result.push('=');
2163        }
2164        if chunk.len() > 2 {
2165            result.push(table[(triple & 0x3F) as usize] as char);
2166        } else {
2167            result.push('=');
2168        }
2169    }
2170    result
2171}
2172
2173#[cfg(test)]
2174mod tests {
2175    use super::*;
2176    use crate::state::SecretsManagerState;
2177    use bytes::Bytes;
2178    use http::{HeaderMap, Method};
2179    use parking_lot::RwLock;
2180    use std::collections::HashMap;
2181    use std::sync::Arc;
2182
2183    fn make_state() -> SharedSecretsManagerState {
2184        Arc::new(RwLock::new(SecretsManagerState::new(
2185            "123456789012",
2186            "us-east-1",
2187        )))
2188    }
2189
2190    fn make_request(action: &str, body: &str) -> AwsRequest {
2191        AwsRequest {
2192            service: "secretsmanager".to_string(),
2193            action: action.to_string(),
2194            region: "us-east-1".to_string(),
2195            account_id: "123456789012".to_string(),
2196            request_id: "test-request-id".to_string(),
2197            headers: HeaderMap::new(),
2198            query_params: HashMap::new(),
2199            body: Bytes::from(body.to_string()),
2200            path_segments: vec![],
2201            raw_path: "/".to_string(),
2202            raw_query: String::new(),
2203            method: Method::POST,
2204            is_query_protocol: false,
2205            access_key_id: None,
2206        }
2207    }
2208
2209    #[tokio::test]
2210    async fn test_create_and_get_secret() {
2211        let state = make_state();
2212        let svc = SecretsManagerService::new(state);
2213
2214        let req = make_request(
2215            "CreateSecret",
2216            r#"{"Name": "test/secret", "SecretString": "mysecretvalue"}"#,
2217        );
2218        let resp = svc.handle(req).await.unwrap();
2219        assert_eq!(resp.status, StatusCode::OK);
2220        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2221        assert_eq!(body["Name"], "test/secret");
2222        assert!(body["ARN"].as_str().unwrap().contains("test/secret"));
2223
2224        let req = make_request("GetSecretValue", r#"{"SecretId": "test/secret"}"#);
2225        let resp = svc.handle(req).await.unwrap();
2226        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2227        assert_eq!(body["SecretString"], "mysecretvalue");
2228    }
2229
2230    #[tokio::test]
2231    async fn test_create_secret_without_value() {
2232        let state = make_state();
2233        let svc = SecretsManagerService::new(state);
2234
2235        let req = make_request("CreateSecret", r#"{"Name": "empty-secret"}"#);
2236        let resp = svc.handle(req).await.unwrap();
2237        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2238        assert_eq!(body["Name"], "empty-secret");
2239        assert!(body.get("VersionId").is_none());
2240    }
2241
2242    #[tokio::test]
2243    async fn test_put_secret_value_creates_version() {
2244        let state = make_state();
2245        let svc = SecretsManagerService::new(state);
2246
2247        let req = make_request(
2248            "CreateSecret",
2249            r#"{"Name": "versioned", "SecretString": "v1"}"#,
2250        );
2251        svc.handle(req).await.unwrap();
2252
2253        let req = make_request(
2254            "PutSecretValue",
2255            r#"{"SecretId": "versioned", "SecretString": "v2"}"#,
2256        );
2257        let resp = svc.handle(req).await.unwrap();
2258        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2259        assert_eq!(body["Name"], "versioned");
2260
2261        // Get should return v2
2262        let req = make_request("GetSecretValue", r#"{"SecretId": "versioned"}"#);
2263        let resp = svc.handle(req).await.unwrap();
2264        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2265        assert_eq!(body["SecretString"], "v2");
2266    }
2267
2268    #[tokio::test]
2269    async fn test_delete_and_restore_secret() {
2270        let state = make_state();
2271        let svc = SecretsManagerService::new(state);
2272
2273        let req = make_request(
2274            "CreateSecret",
2275            r#"{"Name": "deleteme", "SecretString": "value"}"#,
2276        );
2277        svc.handle(req).await.unwrap();
2278
2279        // Delete (soft)
2280        let req = make_request("DeleteSecret", r#"{"SecretId": "deleteme"}"#);
2281        let resp = svc.handle(req).await.unwrap();
2282        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2283        assert!(body["DeletionDate"].as_f64().is_some());
2284
2285        // GetSecretValue should fail
2286        let req = make_request("GetSecretValue", r#"{"SecretId": "deleteme"}"#);
2287        assert!(svc.handle(req).await.is_err());
2288
2289        // Restore
2290        let req = make_request("RestoreSecret", r#"{"SecretId": "deleteme"}"#);
2291        let resp = svc.handle(req).await.unwrap();
2292        assert_eq!(resp.status, StatusCode::OK);
2293
2294        // GetSecretValue should work again
2295        let req = make_request("GetSecretValue", r#"{"SecretId": "deleteme"}"#);
2296        let resp = svc.handle(req).await.unwrap();
2297        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2298        assert_eq!(body["SecretString"], "value");
2299    }
2300
2301    #[tokio::test]
2302    async fn test_list_secrets() {
2303        let state = make_state();
2304        let svc = SecretsManagerService::new(state);
2305
2306        for name in &["alpha", "beta", "gamma"] {
2307            let req = make_request(
2308                "CreateSecret",
2309                &format!(r#"{{"Name": "{name}", "SecretString": "val"}}"#),
2310            );
2311            svc.handle(req).await.unwrap();
2312        }
2313
2314        let req = make_request("ListSecrets", "{}");
2315        let resp = svc.handle(req).await.unwrap();
2316        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2317        assert_eq!(body["SecretList"].as_array().unwrap().len(), 3);
2318    }
2319
2320    #[tokio::test]
2321    async fn test_tags() {
2322        let state = make_state();
2323        let svc = SecretsManagerService::new(state);
2324
2325        let req = make_request(
2326            "CreateSecret",
2327            r#"{"Name": "tagged", "SecretString": "val"}"#,
2328        );
2329        svc.handle(req).await.unwrap();
2330
2331        let req = make_request(
2332            "TagResource",
2333            r#"{"SecretId": "tagged", "Tags": [{"Key": "env", "Value": "prod"}]}"#,
2334        );
2335        svc.handle(req).await.unwrap();
2336
2337        let req = make_request("DescribeSecret", r#"{"SecretId": "tagged"}"#);
2338        let resp = svc.handle(req).await.unwrap();
2339        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2340        let tags = body["Tags"].as_array().unwrap();
2341        assert!(tags
2342            .iter()
2343            .any(|t| t["Key"] == "env" && t["Value"] == "prod"));
2344
2345        let req = make_request(
2346            "UntagResource",
2347            r#"{"SecretId": "tagged", "TagKeys": ["env"]}"#,
2348        );
2349        svc.handle(req).await.unwrap();
2350
2351        let req = make_request("DescribeSecret", r#"{"SecretId": "tagged"}"#);
2352        let resp = svc.handle(req).await.unwrap();
2353        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2354        // Tags should be empty list after untagging all (but key present since tags were set)
2355        assert_eq!(body["Tags"].as_array().unwrap().len(), 0);
2356    }
2357
2358    #[tokio::test]
2359    async fn test_get_random_password() {
2360        let state = make_state();
2361        let svc = SecretsManagerService::new(state);
2362
2363        let req = make_request("GetRandomPassword", "{}");
2364        let resp = svc.handle(req).await.unwrap();
2365        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2366        assert_eq!(body["RandomPassword"].as_str().unwrap().len(), 32);
2367    }
2368
2369    #[tokio::test]
2370    async fn test_replication_ops_return_arn() {
2371        let state = make_state();
2372        let svc = SecretsManagerService::new(state);
2373
2374        let req = make_request(
2375            "CreateSecret",
2376            r#"{"Name": "repl-secret", "SecretString": "val"}"#,
2377        );
2378        let resp = svc.handle(req).await.unwrap();
2379        let create_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2380        let expected_arn = create_body["ARN"].as_str().unwrap();
2381
2382        for action in &[
2383            "ReplicateSecretToRegions",
2384            "RemoveRegionsFromReplication",
2385            "StopReplicationToReplica",
2386        ] {
2387            let req = make_request(action, r#"{"SecretId": "repl-secret"}"#);
2388            let resp = svc.handle(req).await.unwrap();
2389            let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2390            assert_eq!(
2391                body["ARN"].as_str().unwrap(),
2392                expected_arn,
2393                "{action} should return the secret's actual ARN"
2394            );
2395        }
2396    }
2397
2398    #[tokio::test]
2399    async fn test_secret_id_length_validation() {
2400        let state = make_state();
2401        let svc = SecretsManagerService::new(state);
2402
2403        // SecretId too long (> 2048)
2404        let long_id = "x".repeat(2049);
2405        let req = make_request("GetSecretValue", &format!(r#"{{"SecretId": "{long_id}"}}"#));
2406        match svc.handle(req).await {
2407            Err(e) => assert!(e.to_string().contains("ValidationException")),
2408            Ok(_) => panic!("expected ValidationException"),
2409        }
2410    }
2411
2412    #[tokio::test]
2413    async fn test_name_length_validation() {
2414        let state = make_state();
2415        let svc = SecretsManagerService::new(state);
2416
2417        // Name too long (> 512)
2418        let long_name = "x".repeat(513);
2419        let req = make_request(
2420            "CreateSecret",
2421            &format!(r#"{{"Name": "{long_name}", "SecretString": "val"}}"#),
2422        );
2423        match svc.handle(req).await {
2424            Err(e) => assert!(e.to_string().contains("ValidationException")),
2425            Ok(_) => panic!("expected ValidationException"),
2426        }
2427    }
2428
2429    #[tokio::test]
2430    async fn test_next_token_length_validation() {
2431        let state = make_state();
2432        let svc = SecretsManagerService::new(state);
2433
2434        // NextToken too long (> 4096)
2435        let long_token = "x".repeat(4097);
2436        let req = make_request(
2437            "ListSecrets",
2438            &format!(r#"{{"NextToken": "{long_token}"}}"#),
2439        );
2440        match svc.handle(req).await {
2441            Err(e) => assert!(e.to_string().contains("ValidationException")),
2442            Ok(_) => panic!("expected ValidationException"),
2443        }
2444    }
2445
2446    #[tokio::test]
2447    async fn test_client_request_token_length_validation() {
2448        let state = make_state();
2449        let svc = SecretsManagerService::new(state);
2450
2451        // ClientRequestToken too short (< 32)
2452        let req = make_request(
2453            "CreateSecret",
2454            r#"{"Name": "test", "SecretString": "val", "ClientRequestToken": "short"}"#,
2455        );
2456        match svc.handle(req).await {
2457            Err(e) => assert!(e.to_string().contains("ValidationException")),
2458            Ok(_) => panic!("expected ValidationException"),
2459        }
2460    }
2461
2462    #[tokio::test]
2463    async fn test_rotate_secret_with_lambda_creates_pending_version() {
2464        let state = make_state();
2465        let svc = SecretsManagerService::new(state.clone());
2466
2467        // Create a secret
2468        let req = make_request(
2469            "CreateSecret",
2470            r#"{"Name": "rotate-me", "SecretString": "old-password"}"#,
2471        );
2472        svc.handle(req).await.unwrap();
2473
2474        // Rotate with a Lambda ARN
2475        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2476        let body = serde_json::json!({
2477            "SecretId": "rotate-me",
2478            "RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:rotator",
2479            "ClientRequestToken": token,
2480        });
2481        let req = make_request("RotateSecret", &body.to_string());
2482        let resp = svc.handle(req).await.unwrap();
2483        assert_eq!(resp.status, StatusCode::OK);
2484        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2485        assert_eq!(resp_body["VersionId"], token);
2486
2487        // Verify AWSPENDING version exists
2488        let s = state.read();
2489        let secret = s.secrets.get("rotate-me").unwrap();
2490        let pending = secret.versions.get(token).unwrap();
2491        assert!(pending.stages.contains(&"AWSPENDING".to_string()));
2492        assert_eq!(pending.secret_string.as_deref(), Some("old-password"));
2493
2494        // Verify rotation config was set
2495        assert_eq!(
2496            secret.rotation_lambda_arn.as_deref(),
2497            Some("arn:aws:lambda:us-east-1:123456789012:function:rotator")
2498        );
2499        assert_eq!(secret.rotation_enabled, Some(true));
2500    }
2501
2502    #[tokio::test]
2503    async fn test_rotate_secret_without_lambda_promotes_directly() {
2504        let state = make_state();
2505        let svc = SecretsManagerService::new(state.clone());
2506
2507        // Create a secret
2508        let req = make_request(
2509            "CreateSecret",
2510            r#"{"Name": "rotate-no-lambda", "SecretString": "value1"}"#,
2511        );
2512        svc.handle(req).await.unwrap();
2513
2514        // Rotate without Lambda ARN
2515        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2516        let body = serde_json::json!({
2517            "SecretId": "rotate-no-lambda",
2518            "ClientRequestToken": token,
2519        });
2520        let req = make_request("RotateSecret", &body.to_string());
2521        svc.handle(req).await.unwrap();
2522
2523        // Verify the new version is AWSCURRENT (no pending)
2524        let s = state.read();
2525        let secret = s.secrets.get("rotate-no-lambda").unwrap();
2526        let new_ver = secret.versions.get(token).unwrap();
2527        assert!(new_ver.stages.contains(&"AWSCURRENT".to_string()));
2528        assert_eq!(secret.current_version_id.as_deref(), Some(token));
2529    }
2530
2531    #[tokio::test]
2532    async fn test_rotate_secret_stores_rotation_config() {
2533        let state = make_state();
2534        let svc = SecretsManagerService::new(state.clone());
2535
2536        let req = make_request(
2537            "CreateSecret",
2538            r#"{"Name": "rot-cfg", "SecretString": "pw"}"#,
2539        );
2540        svc.handle(req).await.unwrap();
2541
2542        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2543        let body = serde_json::json!({
2544            "SecretId": "rot-cfg",
2545            "RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:my-rotator",
2546            "RotationRules": { "AutomaticallyAfterDays": 30 },
2547            "ClientRequestToken": token,
2548        });
2549        let req = make_request("RotateSecret", &body.to_string());
2550        let resp = svc.handle(req).await.unwrap();
2551        assert_eq!(resp.status, StatusCode::OK);
2552
2553        let s = state.read();
2554        let secret = s.secrets.get("rot-cfg").unwrap();
2555        assert_eq!(secret.rotation_enabled, Some(true));
2556        assert_eq!(
2557            secret.rotation_lambda_arn.as_deref(),
2558            Some("arn:aws:lambda:us-east-1:123456789012:function:my-rotator")
2559        );
2560        assert!(secret.last_rotated_at.is_some());
2561        let rules = secret.rotation_rules.as_ref().unwrap();
2562        assert_eq!(rules.automatically_after_days, Some(30));
2563
2564        // Verify AWSPENDING version was created (Lambda ARN present)
2565        let pending = secret.versions.get(token).unwrap();
2566        assert!(pending.stages.contains(&"AWSPENDING".to_string()));
2567    }
2568
2569    #[tokio::test]
2570    async fn test_rotate_secret_version_stages_change() {
2571        let state = make_state();
2572        let svc = SecretsManagerService::new(state.clone());
2573
2574        let req = make_request(
2575            "CreateSecret",
2576            r#"{"Name": "rot-stages", "SecretString": "original"}"#,
2577        );
2578        svc.handle(req).await.unwrap();
2579
2580        // Get original version id
2581        let original_vid = {
2582            let s = state.read();
2583            let secret = s.secrets.get("rot-stages").unwrap();
2584            secret.current_version_id.clone().unwrap()
2585        };
2586
2587        // Rotate without Lambda (simple rotation)
2588        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2589        let body = serde_json::json!({
2590            "SecretId": "rot-stages",
2591            "ClientRequestToken": token,
2592        });
2593        let req = make_request("RotateSecret", &body.to_string());
2594        svc.handle(req).await.unwrap();
2595
2596        let s = state.read();
2597        let secret = s.secrets.get("rot-stages").unwrap();
2598
2599        // New version should be AWSCURRENT
2600        let new_ver = secret.versions.get(token).unwrap();
2601        assert!(new_ver.stages.contains(&"AWSCURRENT".to_string()));
2602
2603        // Old version should be AWSPREVIOUS
2604        let old_ver = secret.versions.get(&original_vid).unwrap();
2605        assert!(old_ver.stages.contains(&"AWSPREVIOUS".to_string()));
2606        assert!(!old_ver.stages.contains(&"AWSCURRENT".to_string()));
2607    }
2608
2609    #[tokio::test]
2610    async fn test_cancel_rotate_secret() {
2611        let state = make_state();
2612        let svc = SecretsManagerService::new(state.clone());
2613
2614        let req = make_request(
2615            "CreateSecret",
2616            r#"{"Name": "cancel-rot", "SecretString": "pw"}"#,
2617        );
2618        svc.handle(req).await.unwrap();
2619
2620        // Enable rotation first
2621        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2622        let body = serde_json::json!({
2623            "SecretId": "cancel-rot",
2624            "ClientRequestToken": token,
2625        });
2626        let req = make_request("RotateSecret", &body.to_string());
2627        svc.handle(req).await.unwrap();
2628
2629        // Verify rotation is enabled
2630        {
2631            let s = state.read();
2632            let secret = s.secrets.get("cancel-rot").unwrap();
2633            assert_eq!(secret.rotation_enabled, Some(true));
2634        }
2635
2636        // Cancel rotation
2637        let req = make_request("CancelRotateSecret", r#"{"SecretId": "cancel-rot"}"#);
2638        let resp = svc.handle(req).await.unwrap();
2639        assert_eq!(resp.status, StatusCode::OK);
2640        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2641        assert_eq!(body["Name"], "cancel-rot");
2642
2643        // Verify rotation is disabled
2644        let s = state.read();
2645        let secret = s.secrets.get("cancel-rot").unwrap();
2646        assert_eq!(secret.rotation_enabled, Some(false));
2647    }
2648
2649    #[tokio::test]
2650    async fn test_cancel_rotate_secret_fails_when_not_enabled() {
2651        let state = make_state();
2652        let svc = SecretsManagerService::new(state);
2653
2654        let req = make_request(
2655            "CreateSecret",
2656            r#"{"Name": "no-rot", "SecretString": "pw"}"#,
2657        );
2658        svc.handle(req).await.unwrap();
2659
2660        let req = make_request("CancelRotateSecret", r#"{"SecretId": "no-rot"}"#);
2661        let result = svc.handle(req).await;
2662        assert!(result.is_err());
2663    }
2664
2665    #[tokio::test]
2666    async fn test_batch_get_secret_value_multiple() {
2667        let state = make_state();
2668        let svc = SecretsManagerService::new(state);
2669
2670        for (name, val) in &[("batch-a", "va"), ("batch-b", "vb"), ("batch-c", "vc")] {
2671            let req = make_request(
2672                "CreateSecret",
2673                &format!(r#"{{"Name": "{name}", "SecretString": "{val}"}}"#),
2674            );
2675            svc.handle(req).await.unwrap();
2676        }
2677
2678        let body = serde_json::json!({
2679            "SecretIdList": ["batch-a", "batch-b", "batch-c"]
2680        });
2681        let req = make_request("BatchGetSecretValue", &body.to_string());
2682        let resp = svc.handle(req).await.unwrap();
2683        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2684
2685        let values = resp_body["SecretValues"].as_array().unwrap();
2686        assert_eq!(values.len(), 3);
2687
2688        // Verify each secret has the right value
2689        let names: Vec<&str> = values.iter().map(|v| v["Name"].as_str().unwrap()).collect();
2690        assert!(names.contains(&"batch-a"));
2691        assert!(names.contains(&"batch-b"));
2692        assert!(names.contains(&"batch-c"));
2693
2694        // Verify no errors
2695        assert!(resp_body.get("Errors").is_none());
2696    }
2697
2698    #[tokio::test]
2699    async fn test_batch_get_secret_value_with_missing() {
2700        let state = make_state();
2701        let svc = SecretsManagerService::new(state);
2702
2703        let req = make_request(
2704            "CreateSecret",
2705            r#"{"Name": "exists", "SecretString": "val"}"#,
2706        );
2707        svc.handle(req).await.unwrap();
2708
2709        let body = serde_json::json!({
2710            "SecretIdList": ["exists", "nonexistent"]
2711        });
2712        let req = make_request("BatchGetSecretValue", &body.to_string());
2713        let resp = svc.handle(req).await.unwrap();
2714        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2715
2716        let values = resp_body["SecretValues"].as_array().unwrap();
2717        assert_eq!(values.len(), 1);
2718        assert_eq!(values[0]["Name"], "exists");
2719
2720        let errors = resp_body["Errors"].as_array().unwrap();
2721        assert_eq!(errors.len(), 1);
2722        assert_eq!(errors[0]["SecretId"], "nonexistent");
2723        assert_eq!(errors[0]["ErrorCode"], "ResourceNotFoundException");
2724    }
2725
2726    #[tokio::test]
2727    async fn test_update_secret_changes_description_and_kms() {
2728        let state = make_state();
2729        let svc = SecretsManagerService::new(state);
2730
2731        let req = make_request(
2732            "CreateSecret",
2733            r#"{"Name": "updatable", "SecretString": "val", "Description": "old desc"}"#,
2734        );
2735        svc.handle(req).await.unwrap();
2736
2737        // Update description and KmsKeyId
2738        let body = serde_json::json!({
2739            "SecretId": "updatable",
2740            "Description": "new desc",
2741            "KmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/my-key"
2742        });
2743        let req = make_request("UpdateSecret", &body.to_string());
2744        let resp = svc.handle(req).await.unwrap();
2745        assert_eq!(resp.status, StatusCode::OK);
2746        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2747        assert_eq!(resp_body["Name"], "updatable");
2748        // No VersionId since no new value was provided
2749        assert!(resp_body.get("VersionId").is_none());
2750
2751        // Describe to verify changes
2752        let req = make_request("DescribeSecret", r#"{"SecretId": "updatable"}"#);
2753        let resp = svc.handle(req).await.unwrap();
2754        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2755        assert_eq!(body["Description"], "new desc");
2756        assert_eq!(
2757            body["KmsKeyId"],
2758            "arn:aws:kms:us-east-1:123456789012:key/my-key"
2759        );
2760    }
2761
2762    #[tokio::test]
2763    async fn test_update_secret_with_new_value() {
2764        let state = make_state();
2765        let svc = SecretsManagerService::new(state);
2766
2767        let req = make_request(
2768            "CreateSecret",
2769            r#"{"Name": "upd-val", "SecretString": "old"}"#,
2770        );
2771        svc.handle(req).await.unwrap();
2772
2773        // Update with a new value
2774        let body = serde_json::json!({
2775            "SecretId": "upd-val",
2776            "SecretString": "new-value"
2777        });
2778        let req = make_request("UpdateSecret", &body.to_string());
2779        let resp = svc.handle(req).await.unwrap();
2780        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2781        assert!(resp_body["VersionId"].as_str().is_some());
2782
2783        // Get should return new value
2784        let req = make_request("GetSecretValue", r#"{"SecretId": "upd-val"}"#);
2785        let resp = svc.handle(req).await.unwrap();
2786        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2787        assert_eq!(body["SecretString"], "new-value");
2788    }
2789
2790    #[tokio::test]
2791    async fn test_get_random_password_custom_length() {
2792        let state = make_state();
2793        let svc = SecretsManagerService::new(state);
2794
2795        let req = make_request("GetRandomPassword", r#"{"PasswordLength": 64}"#);
2796        let resp = svc.handle(req).await.unwrap();
2797        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2798        assert_eq!(body["RandomPassword"].as_str().unwrap().len(), 64);
2799    }
2800
2801    #[tokio::test]
2802    async fn test_get_random_password_exclude_chars() {
2803        let state = make_state();
2804        let svc = SecretsManagerService::new(state);
2805
2806        let req = make_request(
2807            "GetRandomPassword",
2808            r#"{"PasswordLength": 100, "ExcludeCharacters": "abc123"}"#,
2809        );
2810        let resp = svc.handle(req).await.unwrap();
2811        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2812        let password = body["RandomPassword"].as_str().unwrap();
2813        assert_eq!(password.len(), 100);
2814        assert!(!password.contains('a'));
2815        assert!(!password.contains('b'));
2816        assert!(!password.contains('c'));
2817        assert!(!password.contains('1'));
2818        assert!(!password.contains('2'));
2819        assert!(!password.contains('3'));
2820    }
2821
2822    #[tokio::test]
2823    async fn test_get_random_password_exclude_types() {
2824        let state = make_state();
2825        let svc = SecretsManagerService::new(state);
2826
2827        // Exclude everything except lowercase
2828        let body = serde_json::json!({
2829            "PasswordLength": 50,
2830            "ExcludeUppercase": true,
2831            "ExcludeNumbers": true,
2832            "ExcludePunctuation": true,
2833            "RequireEachIncludedType": false,
2834        });
2835        let req = make_request("GetRandomPassword", &body.to_string());
2836        let resp = svc.handle(req).await.unwrap();
2837        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2838        let password = resp_body["RandomPassword"].as_str().unwrap();
2839        assert_eq!(password.len(), 50);
2840        assert!(password.chars().all(|c| c.is_ascii_lowercase()));
2841    }
2842
2843    #[tokio::test]
2844    async fn test_get_random_password_too_short() {
2845        let state = make_state();
2846        let svc = SecretsManagerService::new(state);
2847
2848        let req = make_request("GetRandomPassword", r#"{"PasswordLength": 3}"#);
2849        assert!(svc.handle(req).await.is_err());
2850    }
2851
2852    #[tokio::test]
2853    async fn test_get_random_password_too_long() {
2854        let state = make_state();
2855        let svc = SecretsManagerService::new(state);
2856
2857        let req = make_request("GetRandomPassword", r#"{"PasswordLength": 4097}"#);
2858        assert!(svc.handle(req).await.is_err());
2859    }
2860
2861    #[tokio::test]
2862    async fn test_update_secret_version_stage_move_current() {
2863        let state = make_state();
2864        let svc = SecretsManagerService::new(state.clone());
2865
2866        let req = make_request(
2867            "CreateSecret",
2868            r#"{"Name": "stage-test", "SecretString": "v1"}"#,
2869        );
2870        svc.handle(req).await.unwrap();
2871
2872        // Put a second version
2873        let req = make_request(
2874            "PutSecretValue",
2875            r#"{"SecretId": "stage-test", "SecretString": "v2"}"#,
2876        );
2877        svc.handle(req).await.unwrap();
2878
2879        // Get version IDs
2880        let (v1_id, v2_id) = {
2881            let s = state.read();
2882            let secret = s.secrets.get("stage-test").unwrap();
2883            let current = secret.current_version_id.clone().unwrap();
2884            let previous = secret
2885                .versions
2886                .iter()
2887                .find(|(id, _)| **id != current)
2888                .map(|(id, _)| id.clone())
2889                .unwrap();
2890            (previous, current)
2891        };
2892
2893        // Move AWSCURRENT from v2 back to v1
2894        let body = serde_json::json!({
2895            "SecretId": "stage-test",
2896            "VersionStage": "AWSCURRENT",
2897            "MoveToVersionId": v1_id,
2898            "RemoveFromVersionId": v2_id,
2899        });
2900        let req = make_request("UpdateSecretVersionStage", &body.to_string());
2901        let resp = svc.handle(req).await.unwrap();
2902        assert_eq!(resp.status, StatusCode::OK);
2903
2904        // Verify v1 is now AWSCURRENT
2905        let s = state.read();
2906        let secret = s.secrets.get("stage-test").unwrap();
2907        let v1 = secret.versions.get(&v1_id).unwrap();
2908        assert!(v1.stages.contains(&"AWSCURRENT".to_string()));
2909
2910        // v2 should have AWSPREVIOUS
2911        let v2 = secret.versions.get(&v2_id).unwrap();
2912        assert!(v2.stages.contains(&"AWSPREVIOUS".to_string()));
2913        assert!(!v2.stages.contains(&"AWSCURRENT".to_string()));
2914
2915        assert_eq!(secret.current_version_id.as_deref(), Some(v1_id.as_str()));
2916    }
2917
2918    #[tokio::test]
2919    async fn test_update_secret_version_stage_custom_label() {
2920        let state = make_state();
2921        let svc = SecretsManagerService::new(state.clone());
2922
2923        let req = make_request(
2924            "CreateSecret",
2925            r#"{"Name": "custom-stage", "SecretString": "v1"}"#,
2926        );
2927        svc.handle(req).await.unwrap();
2928
2929        let vid = {
2930            let s = state.read();
2931            s.secrets
2932                .get("custom-stage")
2933                .unwrap()
2934                .current_version_id
2935                .clone()
2936                .unwrap()
2937        };
2938
2939        // Add a custom label
2940        let body = serde_json::json!({
2941            "SecretId": "custom-stage",
2942            "VersionStage": "MYAPP_LIVE",
2943            "MoveToVersionId": vid,
2944        });
2945        let req = make_request("UpdateSecretVersionStage", &body.to_string());
2946        svc.handle(req).await.unwrap();
2947
2948        let s = state.read();
2949        let secret = s.secrets.get("custom-stage").unwrap();
2950        let ver = secret.versions.get(&vid).unwrap();
2951        assert!(ver.stages.contains(&"MYAPP_LIVE".to_string()));
2952        assert!(ver.stages.contains(&"AWSCURRENT".to_string()));
2953    }
2954
2955    #[tokio::test]
2956    async fn test_validate_resource_policy() {
2957        let state = make_state();
2958        let svc = SecretsManagerService::new(state);
2959
2960        let policy = serde_json::json!({
2961            "Version": "2012-10-17",
2962            "Statement": [{
2963                "Effect": "Allow",
2964                "Principal": {"AWS": "arn:aws:iam::123456789012:root"},
2965                "Action": "secretsmanager:GetSecretValue",
2966                "Resource": "*"
2967            }]
2968        });
2969
2970        let body = serde_json::json!({
2971            "ResourcePolicy": policy.to_string(),
2972        });
2973        let req = make_request("ValidateResourcePolicy", &body.to_string());
2974        let resp = svc.handle(req).await.unwrap();
2975        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2976        assert_eq!(resp_body["PolicyValidationPassed"], true);
2977        assert_eq!(resp_body["ValidationErrors"].as_array().unwrap().len(), 0);
2978    }
2979
2980    #[tokio::test]
2981    async fn test_validate_resource_policy_requires_policy() {
2982        let state = make_state();
2983        let svc = SecretsManagerService::new(state);
2984
2985        let req = make_request("ValidateResourcePolicy", r#"{}"#);
2986        assert!(svc.handle(req).await.is_err());
2987    }
2988
2989    #[tokio::test]
2990    async fn test_put_get_delete_resource_policy() {
2991        let state = make_state();
2992        let svc = SecretsManagerService::new(state);
2993
2994        let req = make_request(
2995            "CreateSecret",
2996            r#"{"Name": "policy-secret", "SecretString": "val"}"#,
2997        );
2998        svc.handle(req).await.unwrap();
2999
3000        // Get policy (should be empty initially)
3001        let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3002        let resp = svc.handle(req).await.unwrap();
3003        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3004        assert_eq!(body["Name"], "policy-secret");
3005        assert!(body.get("ResourcePolicy").is_none());
3006
3007        // Put policy
3008        let policy = r#"{"Version":"2012-10-17","Statement":[]}"#;
3009        let put_body = serde_json::json!({
3010            "SecretId": "policy-secret",
3011            "ResourcePolicy": policy,
3012        });
3013        let req = make_request("PutResourcePolicy", &put_body.to_string());
3014        let resp = svc.handle(req).await.unwrap();
3015        assert_eq!(resp.status, StatusCode::OK);
3016
3017        // Get policy (should have it now)
3018        let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3019        let resp = svc.handle(req).await.unwrap();
3020        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3021        assert_eq!(body["ResourcePolicy"], policy);
3022
3023        // Delete policy
3024        let req = make_request("DeleteResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3025        let resp = svc.handle(req).await.unwrap();
3026        assert_eq!(resp.status, StatusCode::OK);
3027
3028        // Get again (should be gone)
3029        let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3030        let resp = svc.handle(req).await.unwrap();
3031        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3032        assert!(body.get("ResourcePolicy").is_none());
3033    }
3034
3035    #[tokio::test]
3036    async fn test_batch_get_secret_value_with_deleted() {
3037        let state = make_state();
3038        let svc = SecretsManagerService::new(state);
3039
3040        let req = make_request(
3041            "CreateSecret",
3042            r#"{"Name": "batch-del", "SecretString": "val"}"#,
3043        );
3044        svc.handle(req).await.unwrap();
3045
3046        // Soft-delete it
3047        let req = make_request("DeleteSecret", r#"{"SecretId": "batch-del"}"#);
3048        svc.handle(req).await.unwrap();
3049
3050        let body = serde_json::json!({
3051            "SecretIdList": ["batch-del"]
3052        });
3053        let req = make_request("BatchGetSecretValue", &body.to_string());
3054        let resp = svc.handle(req).await.unwrap();
3055        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3056
3057        // Should have 0 values and 1 error
3058        assert_eq!(resp_body["SecretValues"].as_array().unwrap().len(), 0);
3059        let errors = resp_body["Errors"].as_array().unwrap();
3060        assert_eq!(errors.len(), 1);
3061        assert_eq!(errors[0]["ErrorCode"], "InvalidRequestException");
3062    }
3063}