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: do NOT pre-create the AWSPENDING version. The
1167                    // rotation Lambda is responsible for putting the new value via
1168                    // PutSecretValue with VersionStages=[AWSPENDING] during the
1169                    // createSecret step (matching real AWS Secrets Manager behavior).
1170
1171                    // Schedule Lambda invocation
1172                    if let Some(ref arn) = lambda_arn {
1173                        invocation = Some(RotationInvocation {
1174                            lambda_arn: arn.clone(),
1175                            secret_id: secret.arn.clone(),
1176                            client_request_token: version_id.clone(),
1177                        });
1178                    }
1179                } else {
1180                    // Without Lambda: simple rotation - new version becomes AWSCURRENT
1181                    // Move old version to AWSPREVIOUS
1182                    if let Some(old_v) = secret.versions.get_mut(&current_vid) {
1183                        old_v.stages.retain(|s| s != "AWSCURRENT");
1184                        if !old_v.stages.contains(&"AWSPREVIOUS".to_string()) {
1185                            old_v.stages.push("AWSPREVIOUS".to_string());
1186                        }
1187                    }
1188                    let version = SecretVersion {
1189                        version_id: version_id.clone(),
1190                        secret_string: cv.secret_string.clone(),
1191                        secret_binary: cv.secret_binary.clone(),
1192                        stages: vec!["AWSCURRENT".to_string()],
1193                        created_at: now,
1194                    };
1195                    secret.versions.insert(version_id.clone(), version);
1196                    secret.current_version_id = Some(version_id.clone());
1197                }
1198            }
1199        }
1200
1201        let response = json!({
1202            "ARN": secret.arn,
1203            "Name": secret.name,
1204            "VersionId": version_id,
1205        });
1206
1207        Ok((AwsResponse::ok_json(response), invocation))
1208    }
1209
1210    fn cancel_rotate_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1211        let body = req.json_body();
1212        let secret_id = require_secret_id(&body)?;
1213
1214        let mut state = self.state.write();
1215        let secret = self.find_secret_mut(&mut state, &secret_id)?;
1216
1217        if secret.deleted {
1218            return Err(AwsServiceError::aws_error(
1219                StatusCode::BAD_REQUEST,
1220                "InvalidRequestException",
1221                "You can't perform this operation on the secret because it was marked for deletion.",
1222            ));
1223        }
1224
1225        if secret.rotation_enabled != Some(true) {
1226            return Err(AwsServiceError::aws_error(
1227                StatusCode::BAD_REQUEST,
1228                "InvalidRequestException",
1229                "You can't cancel rotation for a secret that does not have rotation enabled.",
1230            ));
1231        }
1232
1233        secret.rotation_enabled = Some(false);
1234
1235        let response = json!({
1236            "ARN": secret.arn,
1237            "Name": secret.name,
1238        });
1239
1240        Ok(AwsResponse::ok_json(response))
1241    }
1242
1243    fn update_secret_version_stage(
1244        &self,
1245        req: &AwsRequest,
1246    ) -> Result<AwsResponse, AwsServiceError> {
1247        let body = req.json_body();
1248        let secret_id = require_secret_id(&body)?;
1249        let version_stage = body["VersionStage"]
1250            .as_str()
1251            .ok_or_else(|| {
1252                AwsServiceError::aws_error(
1253                    StatusCode::BAD_REQUEST,
1254                    "InvalidParameterException",
1255                    "VersionStage is required",
1256                )
1257            })?
1258            .to_string();
1259        validate_string_length("versionStage", &version_stage, 1, 256)?;
1260        validate_optional_string_length(
1261            "removeFromVersionId",
1262            body["RemoveFromVersionId"].as_str(),
1263            32,
1264            64,
1265        )?;
1266        validate_optional_string_length(
1267            "moveToVersionId",
1268            body["MoveToVersionId"].as_str(),
1269            32,
1270            64,
1271        )?;
1272
1273        let move_to = body["MoveToVersionId"].as_str().map(|s| s.to_string());
1274        let remove_from = body["RemoveFromVersionId"].as_str().map(|s| s.to_string());
1275
1276        let mut state = self.state.write();
1277        let secret = self.find_secret_mut(&mut state, &secret_id)?;
1278
1279        // Validate: if moving AWSCURRENT, must specify RemoveFromVersionId
1280        if version_stage == "AWSCURRENT" && move_to.is_some() && remove_from.is_none() {
1281            // Find the version that currently has AWSCURRENT
1282            let current_holder = secret
1283                .versions
1284                .iter()
1285                .find(|(_, v)| v.stages.contains(&"AWSCURRENT".to_string()))
1286                .map(|(id, _)| id.clone());
1287
1288            if let Some(current_vid) = current_holder {
1289                return Err(AwsServiceError::aws_error(
1290                    StatusCode::BAD_REQUEST,
1291                    "InvalidParameterException",
1292                    format!(
1293                        "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."
1294                    ),
1295                ));
1296            }
1297        }
1298
1299        // Remove stage from specified version
1300        if let Some(ref remove_vid) = remove_from {
1301            if let Some(version) = secret.versions.get_mut(remove_vid) {
1302                version.stages.retain(|s| s != &version_stage);
1303                // If moving AWSCURRENT away, add AWSPREVIOUS and remove from others
1304                if version_stage == "AWSCURRENT" {
1305                    // Remove AWSPREVIOUS from all other versions first
1306                    for (id, v) in secret.versions.iter_mut() {
1307                        if id != remove_vid {
1308                            v.stages.retain(|s| s != "AWSPREVIOUS");
1309                        }
1310                    }
1311                    // Now add AWSPREVIOUS to the version losing AWSCURRENT
1312                    if let Some(v) = secret.versions.get_mut(remove_vid) {
1313                        if !v.stages.contains(&"AWSPREVIOUS".to_string()) {
1314                            v.stages.push("AWSPREVIOUS".to_string());
1315                        }
1316                    }
1317                }
1318            }
1319        }
1320
1321        // Add stage to specified version
1322        if let Some(ref move_vid) = move_to {
1323            if let Some(version) = secret.versions.get_mut(move_vid) {
1324                if !version.stages.contains(&version_stage) {
1325                    version.stages.push(version_stage.clone());
1326                }
1327            }
1328            // Update current_version_id if we moved AWSCURRENT
1329            if version_stage == "AWSCURRENT" {
1330                secret.current_version_id = Some(move_vid.clone());
1331            }
1332        }
1333
1334        let response = json!({
1335            "ARN": secret.arn,
1336            "Name": secret.name,
1337        });
1338
1339        Ok(AwsResponse::ok_json(response))
1340    }
1341
1342    fn batch_get_secret_value(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1343        let body = req.json_body();
1344        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
1345        let secret_id_list = body["SecretIdList"].as_array();
1346        let filters = body["Filters"].as_array();
1347        let max_results = body.get("MaxResults").and_then(|v| v.as_i64());
1348
1349        // Validate: can't use both SecretIdList and Filters
1350        if secret_id_list.is_some() && filters.is_some() {
1351            return Err(AwsServiceError::aws_error(
1352                StatusCode::BAD_REQUEST,
1353                "InvalidParameterException",
1354                "Either 'SecretIdList' or 'Filters' must be provided, but not both.",
1355            ));
1356        }
1357
1358        // Validate: MaxResults requires Filters
1359        if max_results.is_some() && filters.is_none() {
1360            return Err(AwsServiceError::aws_error(
1361                StatusCode::BAD_REQUEST,
1362                "InvalidParameterException",
1363                "'Filters' not specified. 'Filters' must also be specified when 'MaxResults' is provided.",
1364            ));
1365        }
1366
1367        let state = self.state.read();
1368        let mut secret_values: Vec<Value> = Vec::new();
1369        let mut errors: Vec<Value> = Vec::new();
1370
1371        if let Some(id_list) = secret_id_list {
1372            for id_val in id_list {
1373                let sid = id_val.as_str().unwrap_or("");
1374                match self.find_secret_ref(&state, sid) {
1375                    Ok(secret) => {
1376                        if secret.deleted {
1377                            errors.push(json!({
1378                                "SecretId": sid,
1379                                "ErrorCode": "InvalidRequestException",
1380                                "Message": "Secret is currently marked deleted. Secret can be recovered with RestoreSecret. Secret is currently marked deleted.",
1381                            }));
1382                        } else if let Some(ref current_vid) = secret.current_version_id {
1383                            if let Some(version) = secret.versions.get(current_vid) {
1384                                let mut entry = json!({
1385                                    "ARN": secret.arn,
1386                                    "Name": secret.name,
1387                                    "VersionId": version.version_id,
1388                                    "VersionStages": version.stages,
1389                                    "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
1390                                });
1391                                if let Some(ref s) = version.secret_string {
1392                                    entry["SecretString"] = json!(s);
1393                                }
1394                                if let Some(ref b) = version.secret_binary {
1395                                    entry["SecretBinary"] = json!(base64_encode(b));
1396                                }
1397                                secret_values.push(entry);
1398                            } else {
1399                                errors.push(json!({
1400                                    "SecretId": sid,
1401                                    "ErrorCode": "ResourceNotFoundException",
1402                                    "Message": "Secrets Manager can't find the specified secret.",
1403                                }));
1404                            }
1405                        } else {
1406                            errors.push(json!({
1407                                "SecretId": sid,
1408                                "ErrorCode": "ResourceNotFoundException",
1409                                "Message": "Secrets Manager can't find the specified secret.",
1410                            }));
1411                        }
1412                    }
1413                    Err(_) => {
1414                        errors.push(json!({
1415                            "SecretId": sid,
1416                            "ErrorCode": "ResourceNotFoundException",
1417                            "Message": "Secrets Manager can't find the specified secret.",
1418                        }));
1419                    }
1420                }
1421            }
1422        } else if let Some(filters) = filters {
1423            // Get secrets matching filters
1424            let matching: Vec<&Secret> = state
1425                .secrets
1426                .values()
1427                .filter(|s| {
1428                    if s.deleted {
1429                        return false;
1430                    }
1431                    for filter in filters {
1432                        let key = filter["Key"].as_str().unwrap_or("");
1433                        let values: Vec<&str> = filter["Values"]
1434                            .as_array()
1435                            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
1436                            .unwrap_or_default();
1437                        let matches = match key {
1438                            "name" => filter_name(s, &values),
1439                            "description" => filter_description(s, &values),
1440                            "tag-key" => filter_tag_key(s, &values),
1441                            "tag-value" => filter_tag_value(s, &values),
1442                            "all" => filter_all(s, &values),
1443                            _ => true,
1444                        };
1445                        if !matches {
1446                            return false;
1447                        }
1448                    }
1449                    true
1450                })
1451                .collect();
1452
1453            let limit = max_results.unwrap_or(100) as usize;
1454            let mut no_value_found = false;
1455            let mut matching = matching;
1456            matching.sort_by(|a, b| a.name.cmp(&b.name));
1457
1458            for secret in matching.iter().take(limit) {
1459                if let Some(ref current_vid) = secret.current_version_id {
1460                    if let Some(version) = secret.versions.get(current_vid) {
1461                        let mut entry = json!({
1462                            "ARN": secret.arn,
1463                            "Name": secret.name,
1464                            "VersionId": version.version_id,
1465                            "VersionStages": version.stages,
1466                            "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
1467                        });
1468                        if let Some(ref s) = version.secret_string {
1469                            entry["SecretString"] = json!(s);
1470                        }
1471                        if let Some(ref b) = version.secret_binary {
1472                            entry["SecretBinary"] = json!(base64_encode(b));
1473                        }
1474                        secret_values.push(entry);
1475                    } else {
1476                        no_value_found = true;
1477                    }
1478                } else {
1479                    no_value_found = true;
1480                }
1481            }
1482
1483            if no_value_found && secret_values.is_empty() {
1484                return Err(AwsServiceError::aws_error(
1485                    StatusCode::NOT_FOUND,
1486                    "ResourceNotFoundException",
1487                    "Secrets Manager can't find the specified secret.",
1488                ));
1489            }
1490        }
1491
1492        let mut response = json!({
1493            "SecretValues": secret_values,
1494            "Errors": errors,
1495        });
1496
1497        // Remove empty arrays
1498        if errors.is_empty() {
1499            response.as_object_mut().unwrap().remove("Errors");
1500        }
1501
1502        Ok(AwsResponse::ok_json(response))
1503    }
1504
1505    fn get_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1506        let body = req.json_body();
1507        let secret_id = require_secret_id(&body)?;
1508
1509        let state = self.state.read();
1510        let secret = self.find_secret_ref(&state, &secret_id)?;
1511
1512        let mut response = json!({
1513            "ARN": secret.arn,
1514            "Name": secret.name,
1515        });
1516
1517        if let Some(ref policy) = secret.resource_policy {
1518            response["ResourcePolicy"] = json!(policy);
1519        }
1520
1521        Ok(AwsResponse::ok_json(response))
1522    }
1523
1524    fn validate_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1525        let body = req.json_body();
1526        validate_optional_string_length("secretId", body["SecretId"].as_str(), 1, 2048)?;
1527        validate_required("ResourcePolicy", &body["ResourcePolicy"])?;
1528        let policy_str = body["ResourcePolicy"].as_str().ok_or_else(|| {
1529            AwsServiceError::aws_error(
1530                StatusCode::BAD_REQUEST,
1531                "InvalidParameterException",
1532                "ResourcePolicy must be a string",
1533            )
1534        })?;
1535        validate_string_length("resourcePolicy", policy_str, 1, 20480)?;
1536
1537        // If SecretId is provided, verify the secret exists
1538        if let Some(secret_id) = body["SecretId"].as_str() {
1539            let state = self.state.read();
1540            self.find_secret_key(&state, secret_id)?;
1541        }
1542
1543        let response = json!({
1544            "PolicyValidationPassed": true,
1545            "ValidationErrors": [],
1546        });
1547        Ok(AwsResponse::ok_json(response))
1548    }
1549
1550    fn put_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1551        let body = req.json_body();
1552        let secret_id = require_secret_id(&body)?;
1553        validate_required("ResourcePolicy", &body["ResourcePolicy"])?;
1554        validate_optional_string_length(
1555            "resourcePolicy",
1556            body["ResourcePolicy"].as_str(),
1557            1,
1558            20480,
1559        )?;
1560        let policy = body["ResourcePolicy"].as_str().map(|s| s.to_string());
1561
1562        let mut state = self.state.write();
1563        let secret = self.find_secret_mut(&mut state, &secret_id)?;
1564        secret.resource_policy = policy;
1565
1566        let response = json!({
1567            "ARN": secret.arn,
1568            "Name": secret.name,
1569        });
1570
1571        Ok(AwsResponse::ok_json(response))
1572    }
1573
1574    fn delete_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1575        let body = req.json_body();
1576        let secret_id = require_secret_id(&body)?;
1577
1578        let mut state = self.state.write();
1579        let secret = self.find_secret_mut(&mut state, &secret_id)?;
1580        secret.resource_policy = None;
1581
1582        let response = json!({
1583            "ARN": secret.arn,
1584            "Name": secret.name,
1585        });
1586
1587        Ok(AwsResponse::ok_json(response))
1588    }
1589
1590    fn replicate_secret_to_regions(
1591        &self,
1592        req: &AwsRequest,
1593    ) -> Result<AwsResponse, AwsServiceError> {
1594        let body = req.json_body();
1595        let secret_id = require_secret_id(&body)?;
1596
1597        let state = self.state.read();
1598        let secret = self.find_secret_ref(&state, &secret_id)?;
1599
1600        let response = json!({
1601            "ARN": secret.arn,
1602            "ReplicationStatus": [],
1603        });
1604        Ok(AwsResponse::ok_json(response))
1605    }
1606
1607    fn remove_regions_from_replication(
1608        &self,
1609        req: &AwsRequest,
1610    ) -> Result<AwsResponse, AwsServiceError> {
1611        let body = req.json_body();
1612        let secret_id = require_secret_id(&body)?;
1613
1614        let state = self.state.read();
1615        let secret = self.find_secret_ref(&state, &secret_id)?;
1616
1617        let response = json!({
1618            "ARN": secret.arn,
1619            "ReplicationStatus": [],
1620        });
1621        Ok(AwsResponse::ok_json(response))
1622    }
1623
1624    fn stop_replication_to_replica(
1625        &self,
1626        req: &AwsRequest,
1627    ) -> Result<AwsResponse, AwsServiceError> {
1628        let body = req.json_body();
1629        let secret_id = require_secret_id(&body)?;
1630
1631        let state = self.state.read();
1632        let secret = self.find_secret_ref(&state, &secret_id)?;
1633
1634        let response = json!({
1635            "ARN": secret.arn,
1636        });
1637        Ok(AwsResponse::ok_json(response))
1638    }
1639
1640    /// Find a secret by name, full ARN, or partial ARN (mutable).
1641    fn find_secret_mut<'a>(
1642        &self,
1643        state: &'a mut crate::state::SecretsManagerState,
1644        secret_id: &str,
1645    ) -> Result<&'a mut Secret, AwsServiceError> {
1646        let key = self.find_secret_key(state, secret_id)?;
1647        Ok(state.secrets.get_mut(&key).unwrap())
1648    }
1649
1650    fn find_secret_key(
1651        &self,
1652        state: &crate::state::SecretsManagerState,
1653        secret_id: &str,
1654    ) -> Result<String, AwsServiceError> {
1655        if state.secrets.contains_key(secret_id) {
1656            return Ok(secret_id.to_string());
1657        }
1658
1659        for secret in state.secrets.values() {
1660            if secret.arn == secret_id {
1661                return Ok(secret.name.clone());
1662            }
1663        }
1664
1665        if secret_id.starts_with("arn:aws:secretsmanager:") {
1666            for secret in state.secrets.values() {
1667                if secret.arn.starts_with(secret_id) {
1668                    return Ok(secret.name.clone());
1669                }
1670            }
1671        }
1672
1673        Err(AwsServiceError::aws_error(
1674            StatusCode::NOT_FOUND,
1675            "ResourceNotFoundException",
1676            "Secrets Manager can't find the specified secret.",
1677        ))
1678    }
1679
1680    /// Find a secret by name, full ARN, or partial ARN (immutable).
1681    fn find_secret_ref<'a>(
1682        &self,
1683        state: &'a crate::state::SecretsManagerState,
1684        secret_id: &str,
1685    ) -> Result<&'a Secret, AwsServiceError> {
1686        if let Some(secret) = state.secrets.get(secret_id) {
1687            return Ok(secret);
1688        }
1689
1690        // Search by full ARN
1691        for secret in state.secrets.values() {
1692            if secret.arn == secret_id {
1693                return Ok(secret);
1694            }
1695        }
1696
1697        // Search by partial ARN
1698        if secret_id.starts_with("arn:aws:secretsmanager:") {
1699            for secret in state.secrets.values() {
1700                if secret.arn.starts_with(secret_id) {
1701                    return Ok(secret);
1702                }
1703            }
1704        }
1705
1706        Err(AwsServiceError::aws_error(
1707            StatusCode::NOT_FOUND,
1708            "ResourceNotFoundException",
1709            "Secrets Manager can't find the specified secret.",
1710        ))
1711    }
1712}
1713
1714/// Parsed + validated inputs for `CreateSecret`.
1715struct CreateSecretInput {
1716    name: String,
1717    client_request_token: Option<String>,
1718    description: Option<String>,
1719    kms_key_id: Option<String>,
1720    secret_string: Option<String>,
1721    secret_binary: Option<Vec<u8>>,
1722    tags: Vec<(String, String)>,
1723}
1724
1725impl CreateSecretInput {
1726    fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
1727        validate_required("Name", &body["Name"])?;
1728        let name = body["Name"]
1729            .as_str()
1730            .ok_or_else(|| {
1731                AwsServiceError::aws_error(
1732                    StatusCode::BAD_REQUEST,
1733                    "InvalidParameterException",
1734                    "Name is required",
1735                )
1736            })?
1737            .to_string();
1738        validate_string_length("name", &name, 1, 512)?;
1739        validate_optional_string_length(
1740            "clientRequestToken",
1741            body["ClientRequestToken"].as_str(),
1742            32,
1743            64,
1744        )?;
1745        validate_optional_string_length("description", body["Description"].as_str(), 0, 2048)?;
1746        validate_optional_string_length("kmsKeyId", body["KmsKeyId"].as_str(), 0, 2048)?;
1747        validate_optional_string_length("secretString", body["SecretString"].as_str(), 1, 65536)?;
1748
1749        Ok(Self {
1750            name,
1751            client_request_token: body["ClientRequestToken"].as_str().map(|s| s.to_string()),
1752            description: body["Description"].as_str().map(|s| s.to_string()),
1753            kms_key_id: body["KmsKeyId"].as_str().map(|s| s.to_string()),
1754            secret_string: body["SecretString"].as_str().map(|s| s.to_string()),
1755            secret_binary: body["SecretBinary"].as_str().and_then(base64_decode),
1756            tags: parse_tags(&body["Tags"]),
1757        })
1758    }
1759}
1760
1761fn require_secret_id(body: &Value) -> Result<String, AwsServiceError> {
1762    let id = body["SecretId"].as_str().ok_or_else(|| {
1763        AwsServiceError::aws_error(
1764            StatusCode::BAD_REQUEST,
1765            "InvalidParameterException",
1766            "SecretId is required",
1767        )
1768    })?;
1769    validate_string_length("secretId", id, 1, 2048)?;
1770    Ok(id.to_string())
1771}
1772
1773fn parse_tags(tags_val: &Value) -> Vec<(String, String)> {
1774    tags_val
1775        .as_array()
1776        .map(|arr| {
1777            arr.iter()
1778                .filter_map(|t| {
1779                    let key = t["Key"].as_str()?;
1780                    let value = t["Value"].as_str()?;
1781                    Some((key.to_string(), value.to_string()))
1782                })
1783                .collect()
1784        })
1785        .unwrap_or_default()
1786}
1787
1788fn tags_to_json(tags: &[(String, String)]) -> Vec<Value> {
1789    tags.iter()
1790        .map(|(k, v)| json!({"Key": k, "Value": v}))
1791        .collect()
1792}
1793
1794/// Split text into words for secret name filtering.
1795/// Splits on special characters (/ - _ + = . @) and camelCase.
1796/// If multiple different special characters are present, doesn't split.
1797/// Spaces are always split on first.
1798fn split_words(text: &str) -> Vec<String> {
1799    // First split on whitespace, then apply word splitting to each part
1800    let mut all_words = Vec::new();
1801    for space_part in text.split_whitespace() {
1802        all_words.extend(split_words_no_space(space_part));
1803    }
1804    all_words
1805}
1806
1807fn split_words_no_space(text: &str) -> Vec<String> {
1808    let special_chars = ['/', '-', '_', '+', '=', '.', '@'];
1809
1810    // Check if text is just a special char
1811    if text.len() == 1 && special_chars.contains(&text.chars().next().unwrap_or(' ')) {
1812        return vec![];
1813    }
1814
1815    // Find which special chars are present
1816    let present: Vec<char> = special_chars
1817        .iter()
1818        .filter(|&&c| text.contains(c))
1819        .copied()
1820        .collect();
1821
1822    if present.len() > 1 {
1823        // Multiple different special chars: don't split
1824        return vec![text.to_string()];
1825    }
1826
1827    if present.len() == 1 {
1828        let ch = present[0];
1829        let parts: Vec<&str> = text.split(ch).filter(|s| !s.is_empty()).collect();
1830        let mut result = Vec::new();
1831        for part in parts {
1832            result.extend(split_by_uppercase(part));
1833        }
1834        return result;
1835    }
1836
1837    // No special chars: split by uppercase
1838    split_by_uppercase(text)
1839}
1840
1841/// Split a string by the pattern: a non-lowercase char followed by one or more lowercase chars.
1842/// Equivalent to Python regex: re.split(r"([^a-z][a-z]+)", s)
1843fn split_by_uppercase(text: &str) -> Vec<String> {
1844    // Implement the equivalent of Python's re.split(r"([^a-z][a-z]+)", text)
1845    // re.split with capturing group returns: [before, match, between, match, ..., after]
1846    let chars: Vec<char> = text.chars().collect();
1847    let mut words = Vec::new();
1848    let mut last_end = 0;
1849    let mut i = 0;
1850
1851    while i < chars.len() {
1852        // Try to find pattern: [^a-z][a-z]+
1853        if !chars[i].is_ascii_lowercase()
1854            && i + 1 < chars.len()
1855            && chars[i + 1].is_ascii_lowercase()
1856        {
1857            // Text before this match (between previous match end and this match start)
1858            if i > last_end {
1859                let between: String = chars[last_end..i].iter().collect();
1860                let trimmed = between.trim().to_string();
1861                if !trimmed.is_empty() {
1862                    words.push(trimmed);
1863                }
1864            }
1865
1866            // The match itself
1867            let start = i;
1868            i += 2;
1869            while i < chars.len() && chars[i].is_ascii_lowercase() {
1870                i += 1;
1871            }
1872            let word: String = chars[start..i].iter().collect();
1873            let trimmed = word.trim().to_string();
1874            if !trimmed.is_empty() {
1875                words.push(trimmed);
1876            }
1877            last_end = i;
1878        } else {
1879            i += 1;
1880        }
1881    }
1882
1883    // Text after last match
1884    if last_end < chars.len() {
1885        let after: String = chars[last_end..].iter().collect();
1886        let trimmed = after.trim().to_string();
1887        if !trimmed.is_empty() {
1888            words.push(trimmed);
1889        }
1890    }
1891
1892    words
1893}
1894
1895/// Match a pattern against a value.
1896/// - match_prefix=true: simple prefix match on the full string
1897/// - match_prefix=false: split both into words, all pattern words must prefix-match some value word
1898fn match_pattern(pattern: &str, value: &str, match_prefix: bool, case_sensitive: bool) -> bool {
1899    if match_prefix {
1900        if case_sensitive {
1901            value.starts_with(pattern)
1902        } else {
1903            value.to_lowercase().starts_with(&pattern.to_lowercase())
1904        }
1905    } else {
1906        let mut pattern_words = split_words(pattern);
1907        if pattern_words.is_empty() {
1908            return false;
1909        }
1910        let mut value_words = split_words(value);
1911        if !case_sensitive {
1912            pattern_words = pattern_words.iter().map(|w| w.to_lowercase()).collect();
1913            value_words = value_words.iter().map(|w| w.to_lowercase()).collect();
1914        }
1915        for pw in &pattern_words {
1916            if !value_words.iter().any(|vw| vw.starts_with(pw.as_str())) {
1917                return false;
1918            }
1919        }
1920        true
1921    }
1922}
1923
1924/// The main matcher: check patterns against a list of strings.
1925/// Supports negation (!pattern), prefix matching, and case sensitivity.
1926fn matcher(patterns: &[&str], strings: &[&str], match_prefix: bool, case_sensitive: bool) -> bool {
1927    // First check negated patterns
1928    for pattern in patterns.iter().filter(|p| p.starts_with('!')) {
1929        let inner = &pattern[1..];
1930        for s in strings {
1931            if !match_pattern(inner, s, match_prefix, case_sensitive) {
1932                return true;
1933            }
1934        }
1935    }
1936
1937    // Then check positive patterns
1938    for pattern in patterns.iter().filter(|p| !p.starts_with('!')) {
1939        for s in strings {
1940            if match_pattern(pattern, s, match_prefix, case_sensitive) {
1941                return true;
1942            }
1943        }
1944    }
1945    false
1946}
1947
1948/// Name filter: prefix match, case sensitive
1949fn filter_name(secret: &Secret, values: &[&str]) -> bool {
1950    matcher(values, &[secret.name.as_str()], true, true)
1951}
1952
1953/// Description filter: word match, case insensitive
1954fn filter_description(secret: &Secret, values: &[&str]) -> bool {
1955    match secret.description.as_deref() {
1956        Some(desc) if !desc.is_empty() => matcher(values, &[desc], false, false),
1957        _ => false,
1958    }
1959}
1960
1961/// Tag key filter: prefix match, case sensitive
1962fn filter_tag_key(secret: &Secret, values: &[&str]) -> bool {
1963    if secret.tags.is_empty() {
1964        return false;
1965    }
1966    let keys: Vec<&str> = secret.tags.iter().map(|(k, _)| k.as_str()).collect();
1967    matcher(values, &keys, true, true)
1968}
1969
1970/// Tag value filter: prefix match, case sensitive
1971fn filter_tag_value(secret: &Secret, values: &[&str]) -> bool {
1972    if secret.tags.is_empty() {
1973        return false;
1974    }
1975    let vals: Vec<&str> = secret.tags.iter().map(|(_, v)| v.as_str()).collect();
1976    matcher(values, &vals, true, true)
1977}
1978
1979/// All filter: word match, case insensitive, across all fields
1980fn filter_all(secret: &Secret, values: &[&str]) -> bool {
1981    let mut attributes: Vec<&str> = vec![secret.name.as_str()];
1982    if let Some(ref desc) = secret.description {
1983        if !desc.is_empty() {
1984            attributes.push(desc.as_str());
1985        }
1986    }
1987    for (k, v) in &secret.tags {
1988        attributes.push(k.as_str());
1989        attributes.push(v.as_str());
1990    }
1991    matcher(values, &attributes, false, false)
1992}
1993
1994fn simple_random() -> usize {
1995    use std::collections::hash_map::RandomState;
1996    use std::hash::{BuildHasher, Hasher};
1997    let s = RandomState::new();
1998    let mut hasher = s.build_hasher();
1999    hasher.write_usize(0);
2000    hasher.finish() as usize
2001}
2002
2003#[async_trait]
2004impl AwsService for SecretsManagerService {
2005    fn service_name(&self) -> &str {
2006        "secretsmanager"
2007    }
2008
2009    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2010        match req.action.as_str() {
2011            "CreateSecret" => self.create_secret(&req),
2012            "GetSecretValue" => self.get_secret_value(&req),
2013            "PutSecretValue" => self.put_secret_value(&req),
2014            "UpdateSecret" => self.update_secret(&req),
2015            "DeleteSecret" => self.delete_secret(&req),
2016            "RestoreSecret" => self.restore_secret(&req),
2017            "DescribeSecret" => self.describe_secret(&req),
2018            "ListSecrets" => self.list_secrets(&req),
2019            "TagResource" => self.tag_resource(&req),
2020            "UntagResource" => self.untag_resource(&req),
2021            "ListSecretVersionIds" => self.list_secret_version_ids(&req),
2022            "GetRandomPassword" => self.get_random_password(&req),
2023            "RotateSecret" => {
2024                let (response, invocation) = self.rotate_secret(&req)?;
2025                if let Some(inv) = invocation {
2026                    if let Some(ref bus) = self.delivery_bus {
2027                        let bus = bus.clone();
2028                        // AWS invokes the rotation Lambda asynchronously for each step.
2029                        tokio::spawn(async move {
2030                            for step in &["createSecret", "setSecret", "testSecret", "finishSecret"]
2031                            {
2032                                let payload = serde_json::json!({
2033                                    "SecretId": inv.secret_id,
2034                                    "ClientRequestToken": inv.client_request_token,
2035                                    "Step": step,
2036                                });
2037                                let payload_str = payload.to_string();
2038                                match bus.invoke_lambda(&inv.lambda_arn, &payload_str).await {
2039                                    Some(Ok(_)) => {}
2040                                    Some(Err(e)) => {
2041                                        tracing::warn!(
2042                                            step = step,
2043                                            error = %e,
2044                                            "rotation Lambda invocation failed"
2045                                        );
2046                                    }
2047                                    None => {
2048                                        tracing::warn!(
2049                                            lambda_arn = %inv.lambda_arn,
2050                                            step = step,
2051                                            "rotation Lambda delivery not configured; \
2052                                             Lambda invocation skipped"
2053                                        );
2054                                        break;
2055                                    }
2056                                }
2057                            }
2058                        });
2059                    }
2060                }
2061                Ok(response)
2062            }
2063            "CancelRotateSecret" => self.cancel_rotate_secret(&req),
2064            "UpdateSecretVersionStage" => self.update_secret_version_stage(&req),
2065            "BatchGetSecretValue" => self.batch_get_secret_value(&req),
2066            "GetResourcePolicy" => self.get_resource_policy(&req),
2067            "PutResourcePolicy" => self.put_resource_policy(&req),
2068            "DeleteResourcePolicy" => self.delete_resource_policy(&req),
2069            "ValidateResourcePolicy" => self.validate_resource_policy(&req),
2070            "ReplicateSecretToRegions" => self.replicate_secret_to_regions(&req),
2071            "RemoveRegionsFromReplication" => self.remove_regions_from_replication(&req),
2072            "StopReplicationToReplica" => self.stop_replication_to_replica(&req),
2073            _ => Err(AwsServiceError::action_not_implemented(
2074                "secretsmanager",
2075                &req.action,
2076            )),
2077        }
2078    }
2079
2080    fn supported_actions(&self) -> &[&str] {
2081        &[
2082            "CreateSecret",
2083            "GetSecretValue",
2084            "PutSecretValue",
2085            "UpdateSecret",
2086            "DeleteSecret",
2087            "RestoreSecret",
2088            "DescribeSecret",
2089            "ListSecrets",
2090            "TagResource",
2091            "UntagResource",
2092            "ListSecretVersionIds",
2093            "GetRandomPassword",
2094            "RotateSecret",
2095            "CancelRotateSecret",
2096            "UpdateSecretVersionStage",
2097            "BatchGetSecretValue",
2098            "GetResourcePolicy",
2099            "PutResourcePolicy",
2100            "DeleteResourcePolicy",
2101            "ValidateResourcePolicy",
2102            "ReplicateSecretToRegions",
2103            "RemoveRegionsFromReplication",
2104            "StopReplicationToReplica",
2105        ]
2106    }
2107}
2108
2109fn base64_decode(input: &str) -> Option<Vec<u8>> {
2110    let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2111    let mut buf = Vec::new();
2112    let mut bits: u32 = 0;
2113    let mut count = 0;
2114    for &b in input.as_bytes() {
2115        if b == b'=' || b == b'\n' || b == b'\r' {
2116            continue;
2117        }
2118        let val = table.iter().position(|&c| c == b)? as u32;
2119        bits = (bits << 6) | val;
2120        count += 1;
2121        if count == 4 {
2122            buf.push((bits >> 16) as u8);
2123            buf.push((bits >> 8) as u8);
2124            buf.push(bits as u8);
2125            bits = 0;
2126            count = 0;
2127        }
2128    }
2129    match count {
2130        2 => {
2131            bits <<= 12;
2132            buf.push((bits >> 16) as u8);
2133        }
2134        3 => {
2135            bits <<= 6;
2136            buf.push((bits >> 16) as u8);
2137            buf.push((bits >> 8) as u8);
2138        }
2139        _ => {}
2140    }
2141    Some(buf)
2142}
2143
2144fn base64_encode(input: &[u8]) -> String {
2145    let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2146    let mut result = String::new();
2147    for chunk in input.chunks(3) {
2148        let b0 = chunk[0] as u32;
2149        let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
2150        let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
2151        let triple = (b0 << 16) | (b1 << 8) | b2;
2152        result.push(table[((triple >> 18) & 0x3F) as usize] as char);
2153        result.push(table[((triple >> 12) & 0x3F) as usize] as char);
2154        if chunk.len() > 1 {
2155            result.push(table[((triple >> 6) & 0x3F) as usize] as char);
2156        } else {
2157            result.push('=');
2158        }
2159        if chunk.len() > 2 {
2160            result.push(table[(triple & 0x3F) as usize] as char);
2161        } else {
2162            result.push('=');
2163        }
2164    }
2165    result
2166}
2167
2168#[cfg(test)]
2169mod tests {
2170    use super::*;
2171    use crate::state::SecretsManagerState;
2172    use bytes::Bytes;
2173    use http::{HeaderMap, Method};
2174    use parking_lot::RwLock;
2175    use std::collections::HashMap;
2176    use std::sync::Arc;
2177
2178    fn make_state() -> SharedSecretsManagerState {
2179        Arc::new(RwLock::new(SecretsManagerState::new(
2180            "123456789012",
2181            "us-east-1",
2182        )))
2183    }
2184
2185    fn make_request(action: &str, body: &str) -> AwsRequest {
2186        AwsRequest {
2187            service: "secretsmanager".to_string(),
2188            action: action.to_string(),
2189            region: "us-east-1".to_string(),
2190            account_id: "123456789012".to_string(),
2191            request_id: "test-request-id".to_string(),
2192            headers: HeaderMap::new(),
2193            query_params: HashMap::new(),
2194            body: Bytes::from(body.to_string()),
2195            path_segments: vec![],
2196            raw_path: "/".to_string(),
2197            raw_query: String::new(),
2198            method: Method::POST,
2199            is_query_protocol: false,
2200            access_key_id: None,
2201            principal: None,
2202        }
2203    }
2204
2205    #[tokio::test]
2206    async fn test_create_and_get_secret() {
2207        let state = make_state();
2208        let svc = SecretsManagerService::new(state);
2209
2210        let req = make_request(
2211            "CreateSecret",
2212            r#"{"Name": "test/secret", "SecretString": "mysecretvalue"}"#,
2213        );
2214        let resp = svc.handle(req).await.unwrap();
2215        assert_eq!(resp.status, StatusCode::OK);
2216        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2217        assert_eq!(body["Name"], "test/secret");
2218        assert!(body["ARN"].as_str().unwrap().contains("test/secret"));
2219
2220        let req = make_request("GetSecretValue", r#"{"SecretId": "test/secret"}"#);
2221        let resp = svc.handle(req).await.unwrap();
2222        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2223        assert_eq!(body["SecretString"], "mysecretvalue");
2224    }
2225
2226    #[tokio::test]
2227    async fn test_create_secret_without_value() {
2228        let state = make_state();
2229        let svc = SecretsManagerService::new(state);
2230
2231        let req = make_request("CreateSecret", r#"{"Name": "empty-secret"}"#);
2232        let resp = svc.handle(req).await.unwrap();
2233        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2234        assert_eq!(body["Name"], "empty-secret");
2235        assert!(body.get("VersionId").is_none());
2236    }
2237
2238    #[tokio::test]
2239    async fn test_put_secret_value_creates_version() {
2240        let state = make_state();
2241        let svc = SecretsManagerService::new(state);
2242
2243        let req = make_request(
2244            "CreateSecret",
2245            r#"{"Name": "versioned", "SecretString": "v1"}"#,
2246        );
2247        svc.handle(req).await.unwrap();
2248
2249        let req = make_request(
2250            "PutSecretValue",
2251            r#"{"SecretId": "versioned", "SecretString": "v2"}"#,
2252        );
2253        let resp = svc.handle(req).await.unwrap();
2254        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2255        assert_eq!(body["Name"], "versioned");
2256
2257        // Get should return v2
2258        let req = make_request("GetSecretValue", r#"{"SecretId": "versioned"}"#);
2259        let resp = svc.handle(req).await.unwrap();
2260        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2261        assert_eq!(body["SecretString"], "v2");
2262    }
2263
2264    #[tokio::test]
2265    async fn test_delete_and_restore_secret() {
2266        let state = make_state();
2267        let svc = SecretsManagerService::new(state);
2268
2269        let req = make_request(
2270            "CreateSecret",
2271            r#"{"Name": "deleteme", "SecretString": "value"}"#,
2272        );
2273        svc.handle(req).await.unwrap();
2274
2275        // Delete (soft)
2276        let req = make_request("DeleteSecret", r#"{"SecretId": "deleteme"}"#);
2277        let resp = svc.handle(req).await.unwrap();
2278        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2279        assert!(body["DeletionDate"].as_f64().is_some());
2280
2281        // GetSecretValue should fail
2282        let req = make_request("GetSecretValue", r#"{"SecretId": "deleteme"}"#);
2283        assert!(svc.handle(req).await.is_err());
2284
2285        // Restore
2286        let req = make_request("RestoreSecret", r#"{"SecretId": "deleteme"}"#);
2287        let resp = svc.handle(req).await.unwrap();
2288        assert_eq!(resp.status, StatusCode::OK);
2289
2290        // GetSecretValue should work again
2291        let req = make_request("GetSecretValue", r#"{"SecretId": "deleteme"}"#);
2292        let resp = svc.handle(req).await.unwrap();
2293        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2294        assert_eq!(body["SecretString"], "value");
2295    }
2296
2297    #[tokio::test]
2298    async fn test_list_secrets() {
2299        let state = make_state();
2300        let svc = SecretsManagerService::new(state);
2301
2302        for name in &["alpha", "beta", "gamma"] {
2303            let req = make_request(
2304                "CreateSecret",
2305                &format!(r#"{{"Name": "{name}", "SecretString": "val"}}"#),
2306            );
2307            svc.handle(req).await.unwrap();
2308        }
2309
2310        let req = make_request("ListSecrets", "{}");
2311        let resp = svc.handle(req).await.unwrap();
2312        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2313        assert_eq!(body["SecretList"].as_array().unwrap().len(), 3);
2314    }
2315
2316    #[tokio::test]
2317    async fn test_tags() {
2318        let state = make_state();
2319        let svc = SecretsManagerService::new(state);
2320
2321        let req = make_request(
2322            "CreateSecret",
2323            r#"{"Name": "tagged", "SecretString": "val"}"#,
2324        );
2325        svc.handle(req).await.unwrap();
2326
2327        let req = make_request(
2328            "TagResource",
2329            r#"{"SecretId": "tagged", "Tags": [{"Key": "env", "Value": "prod"}]}"#,
2330        );
2331        svc.handle(req).await.unwrap();
2332
2333        let req = make_request("DescribeSecret", r#"{"SecretId": "tagged"}"#);
2334        let resp = svc.handle(req).await.unwrap();
2335        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2336        let tags = body["Tags"].as_array().unwrap();
2337        assert!(tags
2338            .iter()
2339            .any(|t| t["Key"] == "env" && t["Value"] == "prod"));
2340
2341        let req = make_request(
2342            "UntagResource",
2343            r#"{"SecretId": "tagged", "TagKeys": ["env"]}"#,
2344        );
2345        svc.handle(req).await.unwrap();
2346
2347        let req = make_request("DescribeSecret", r#"{"SecretId": "tagged"}"#);
2348        let resp = svc.handle(req).await.unwrap();
2349        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2350        // Tags should be empty list after untagging all (but key present since tags were set)
2351        assert_eq!(body["Tags"].as_array().unwrap().len(), 0);
2352    }
2353
2354    #[tokio::test]
2355    async fn test_get_random_password() {
2356        let state = make_state();
2357        let svc = SecretsManagerService::new(state);
2358
2359        let req = make_request("GetRandomPassword", "{}");
2360        let resp = svc.handle(req).await.unwrap();
2361        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2362        assert_eq!(body["RandomPassword"].as_str().unwrap().len(), 32);
2363    }
2364
2365    #[tokio::test]
2366    async fn test_replication_ops_return_arn() {
2367        let state = make_state();
2368        let svc = SecretsManagerService::new(state);
2369
2370        let req = make_request(
2371            "CreateSecret",
2372            r#"{"Name": "repl-secret", "SecretString": "val"}"#,
2373        );
2374        let resp = svc.handle(req).await.unwrap();
2375        let create_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2376        let expected_arn = create_body["ARN"].as_str().unwrap();
2377
2378        for action in &[
2379            "ReplicateSecretToRegions",
2380            "RemoveRegionsFromReplication",
2381            "StopReplicationToReplica",
2382        ] {
2383            let req = make_request(action, r#"{"SecretId": "repl-secret"}"#);
2384            let resp = svc.handle(req).await.unwrap();
2385            let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2386            assert_eq!(
2387                body["ARN"].as_str().unwrap(),
2388                expected_arn,
2389                "{action} should return the secret's actual ARN"
2390            );
2391        }
2392    }
2393
2394    #[tokio::test]
2395    async fn test_secret_id_length_validation() {
2396        let state = make_state();
2397        let svc = SecretsManagerService::new(state);
2398
2399        // SecretId too long (> 2048)
2400        let long_id = "x".repeat(2049);
2401        let req = make_request("GetSecretValue", &format!(r#"{{"SecretId": "{long_id}"}}"#));
2402        match svc.handle(req).await {
2403            Err(e) => assert!(e.to_string().contains("ValidationException")),
2404            Ok(_) => panic!("expected ValidationException"),
2405        }
2406    }
2407
2408    #[tokio::test]
2409    async fn test_name_length_validation() {
2410        let state = make_state();
2411        let svc = SecretsManagerService::new(state);
2412
2413        // Name too long (> 512)
2414        let long_name = "x".repeat(513);
2415        let req = make_request(
2416            "CreateSecret",
2417            &format!(r#"{{"Name": "{long_name}", "SecretString": "val"}}"#),
2418        );
2419        match svc.handle(req).await {
2420            Err(e) => assert!(e.to_string().contains("ValidationException")),
2421            Ok(_) => panic!("expected ValidationException"),
2422        }
2423    }
2424
2425    #[tokio::test]
2426    async fn test_next_token_length_validation() {
2427        let state = make_state();
2428        let svc = SecretsManagerService::new(state);
2429
2430        // NextToken too long (> 4096)
2431        let long_token = "x".repeat(4097);
2432        let req = make_request(
2433            "ListSecrets",
2434            &format!(r#"{{"NextToken": "{long_token}"}}"#),
2435        );
2436        match svc.handle(req).await {
2437            Err(e) => assert!(e.to_string().contains("ValidationException")),
2438            Ok(_) => panic!("expected ValidationException"),
2439        }
2440    }
2441
2442    #[tokio::test]
2443    async fn test_client_request_token_length_validation() {
2444        let state = make_state();
2445        let svc = SecretsManagerService::new(state);
2446
2447        // ClientRequestToken too short (< 32)
2448        let req = make_request(
2449            "CreateSecret",
2450            r#"{"Name": "test", "SecretString": "val", "ClientRequestToken": "short"}"#,
2451        );
2452        match svc.handle(req).await {
2453            Err(e) => assert!(e.to_string().contains("ValidationException")),
2454            Ok(_) => panic!("expected ValidationException"),
2455        }
2456    }
2457
2458    #[tokio::test]
2459    async fn test_rotate_secret_with_lambda_creates_pending_version() {
2460        let state = make_state();
2461        let svc = SecretsManagerService::new(state.clone());
2462
2463        // Create a secret
2464        let req = make_request(
2465            "CreateSecret",
2466            r#"{"Name": "rotate-me", "SecretString": "old-password"}"#,
2467        );
2468        svc.handle(req).await.unwrap();
2469
2470        // Rotate with a Lambda ARN
2471        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2472        let body = serde_json::json!({
2473            "SecretId": "rotate-me",
2474            "RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:rotator",
2475            "ClientRequestToken": token,
2476        });
2477        let req = make_request("RotateSecret", &body.to_string());
2478        let resp = svc.handle(req).await.unwrap();
2479        assert_eq!(resp.status, StatusCode::OK);
2480        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2481        assert_eq!(resp_body["VersionId"], token);
2482
2483        // Real AWS leaves the AWSPENDING version creation to the rotation
2484        // Lambda's createSecret step, so we should NOT pre-create it. Verify
2485        // that no version with the rotation token exists yet.
2486        let s = state.read();
2487        let secret = s.secrets.get("rotate-me").unwrap();
2488        assert!(
2489            !secret.versions.contains_key(token),
2490            "AWSPENDING version must not be pre-created; the rotation Lambda creates it"
2491        );
2492
2493        // Verify rotation config was set
2494        assert_eq!(
2495            secret.rotation_lambda_arn.as_deref(),
2496            Some("arn:aws:lambda:us-east-1:123456789012:function:rotator")
2497        );
2498        assert_eq!(secret.rotation_enabled, Some(true));
2499    }
2500
2501    #[tokio::test]
2502    async fn test_rotate_secret_without_lambda_promotes_directly() {
2503        let state = make_state();
2504        let svc = SecretsManagerService::new(state.clone());
2505
2506        // Create a secret
2507        let req = make_request(
2508            "CreateSecret",
2509            r#"{"Name": "rotate-no-lambda", "SecretString": "value1"}"#,
2510        );
2511        svc.handle(req).await.unwrap();
2512
2513        // Rotate without Lambda ARN
2514        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2515        let body = serde_json::json!({
2516            "SecretId": "rotate-no-lambda",
2517            "ClientRequestToken": token,
2518        });
2519        let req = make_request("RotateSecret", &body.to_string());
2520        svc.handle(req).await.unwrap();
2521
2522        // Verify the new version is AWSCURRENT (no pending)
2523        let s = state.read();
2524        let secret = s.secrets.get("rotate-no-lambda").unwrap();
2525        let new_ver = secret.versions.get(token).unwrap();
2526        assert!(new_ver.stages.contains(&"AWSCURRENT".to_string()));
2527        assert_eq!(secret.current_version_id.as_deref(), Some(token));
2528    }
2529
2530    #[tokio::test]
2531    async fn test_rotate_secret_stores_rotation_config() {
2532        let state = make_state();
2533        let svc = SecretsManagerService::new(state.clone());
2534
2535        let req = make_request(
2536            "CreateSecret",
2537            r#"{"Name": "rot-cfg", "SecretString": "pw"}"#,
2538        );
2539        svc.handle(req).await.unwrap();
2540
2541        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2542        let body = serde_json::json!({
2543            "SecretId": "rot-cfg",
2544            "RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:my-rotator",
2545            "RotationRules": { "AutomaticallyAfterDays": 30 },
2546            "ClientRequestToken": token,
2547        });
2548        let req = make_request("RotateSecret", &body.to_string());
2549        let resp = svc.handle(req).await.unwrap();
2550        assert_eq!(resp.status, StatusCode::OK);
2551
2552        let s = state.read();
2553        let secret = s.secrets.get("rot-cfg").unwrap();
2554        assert_eq!(secret.rotation_enabled, Some(true));
2555        assert_eq!(
2556            secret.rotation_lambda_arn.as_deref(),
2557            Some("arn:aws:lambda:us-east-1:123456789012:function:my-rotator")
2558        );
2559        assert!(secret.last_rotated_at.is_some());
2560        let rules = secret.rotation_rules.as_ref().unwrap();
2561        assert_eq!(rules.automatically_after_days, Some(30));
2562
2563        // The AWSPENDING version is created by the rotation Lambda's
2564        // createSecret step, not by RotateSecret itself, so verify that no
2565        // version with this token exists yet.
2566        assert!(!secret.versions.contains_key(token));
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}