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        }
2202    }
2203
2204    #[tokio::test]
2205    async fn test_create_and_get_secret() {
2206        let state = make_state();
2207        let svc = SecretsManagerService::new(state);
2208
2209        let req = make_request(
2210            "CreateSecret",
2211            r#"{"Name": "test/secret", "SecretString": "mysecretvalue"}"#,
2212        );
2213        let resp = svc.handle(req).await.unwrap();
2214        assert_eq!(resp.status, StatusCode::OK);
2215        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2216        assert_eq!(body["Name"], "test/secret");
2217        assert!(body["ARN"].as_str().unwrap().contains("test/secret"));
2218
2219        let req = make_request("GetSecretValue", r#"{"SecretId": "test/secret"}"#);
2220        let resp = svc.handle(req).await.unwrap();
2221        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2222        assert_eq!(body["SecretString"], "mysecretvalue");
2223    }
2224
2225    #[tokio::test]
2226    async fn test_create_secret_without_value() {
2227        let state = make_state();
2228        let svc = SecretsManagerService::new(state);
2229
2230        let req = make_request("CreateSecret", r#"{"Name": "empty-secret"}"#);
2231        let resp = svc.handle(req).await.unwrap();
2232        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2233        assert_eq!(body["Name"], "empty-secret");
2234        assert!(body.get("VersionId").is_none());
2235    }
2236
2237    #[tokio::test]
2238    async fn test_put_secret_value_creates_version() {
2239        let state = make_state();
2240        let svc = SecretsManagerService::new(state);
2241
2242        let req = make_request(
2243            "CreateSecret",
2244            r#"{"Name": "versioned", "SecretString": "v1"}"#,
2245        );
2246        svc.handle(req).await.unwrap();
2247
2248        let req = make_request(
2249            "PutSecretValue",
2250            r#"{"SecretId": "versioned", "SecretString": "v2"}"#,
2251        );
2252        let resp = svc.handle(req).await.unwrap();
2253        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2254        assert_eq!(body["Name"], "versioned");
2255
2256        // Get should return v2
2257        let req = make_request("GetSecretValue", r#"{"SecretId": "versioned"}"#);
2258        let resp = svc.handle(req).await.unwrap();
2259        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2260        assert_eq!(body["SecretString"], "v2");
2261    }
2262
2263    #[tokio::test]
2264    async fn test_delete_and_restore_secret() {
2265        let state = make_state();
2266        let svc = SecretsManagerService::new(state);
2267
2268        let req = make_request(
2269            "CreateSecret",
2270            r#"{"Name": "deleteme", "SecretString": "value"}"#,
2271        );
2272        svc.handle(req).await.unwrap();
2273
2274        // Delete (soft)
2275        let req = make_request("DeleteSecret", r#"{"SecretId": "deleteme"}"#);
2276        let resp = svc.handle(req).await.unwrap();
2277        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2278        assert!(body["DeletionDate"].as_f64().is_some());
2279
2280        // GetSecretValue should fail
2281        let req = make_request("GetSecretValue", r#"{"SecretId": "deleteme"}"#);
2282        assert!(svc.handle(req).await.is_err());
2283
2284        // Restore
2285        let req = make_request("RestoreSecret", r#"{"SecretId": "deleteme"}"#);
2286        let resp = svc.handle(req).await.unwrap();
2287        assert_eq!(resp.status, StatusCode::OK);
2288
2289        // GetSecretValue should work again
2290        let req = make_request("GetSecretValue", r#"{"SecretId": "deleteme"}"#);
2291        let resp = svc.handle(req).await.unwrap();
2292        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2293        assert_eq!(body["SecretString"], "value");
2294    }
2295
2296    #[tokio::test]
2297    async fn test_list_secrets() {
2298        let state = make_state();
2299        let svc = SecretsManagerService::new(state);
2300
2301        for name in &["alpha", "beta", "gamma"] {
2302            let req = make_request(
2303                "CreateSecret",
2304                &format!(r#"{{"Name": "{name}", "SecretString": "val"}}"#),
2305            );
2306            svc.handle(req).await.unwrap();
2307        }
2308
2309        let req = make_request("ListSecrets", "{}");
2310        let resp = svc.handle(req).await.unwrap();
2311        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2312        assert_eq!(body["SecretList"].as_array().unwrap().len(), 3);
2313    }
2314
2315    #[tokio::test]
2316    async fn test_tags() {
2317        let state = make_state();
2318        let svc = SecretsManagerService::new(state);
2319
2320        let req = make_request(
2321            "CreateSecret",
2322            r#"{"Name": "tagged", "SecretString": "val"}"#,
2323        );
2324        svc.handle(req).await.unwrap();
2325
2326        let req = make_request(
2327            "TagResource",
2328            r#"{"SecretId": "tagged", "Tags": [{"Key": "env", "Value": "prod"}]}"#,
2329        );
2330        svc.handle(req).await.unwrap();
2331
2332        let req = make_request("DescribeSecret", r#"{"SecretId": "tagged"}"#);
2333        let resp = svc.handle(req).await.unwrap();
2334        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2335        let tags = body["Tags"].as_array().unwrap();
2336        assert!(tags
2337            .iter()
2338            .any(|t| t["Key"] == "env" && t["Value"] == "prod"));
2339
2340        let req = make_request(
2341            "UntagResource",
2342            r#"{"SecretId": "tagged", "TagKeys": ["env"]}"#,
2343        );
2344        svc.handle(req).await.unwrap();
2345
2346        let req = make_request("DescribeSecret", r#"{"SecretId": "tagged"}"#);
2347        let resp = svc.handle(req).await.unwrap();
2348        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2349        // Tags should be empty list after untagging all (but key present since tags were set)
2350        assert_eq!(body["Tags"].as_array().unwrap().len(), 0);
2351    }
2352
2353    #[tokio::test]
2354    async fn test_get_random_password() {
2355        let state = make_state();
2356        let svc = SecretsManagerService::new(state);
2357
2358        let req = make_request("GetRandomPassword", "{}");
2359        let resp = svc.handle(req).await.unwrap();
2360        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2361        assert_eq!(body["RandomPassword"].as_str().unwrap().len(), 32);
2362    }
2363
2364    #[tokio::test]
2365    async fn test_replication_ops_return_arn() {
2366        let state = make_state();
2367        let svc = SecretsManagerService::new(state);
2368
2369        let req = make_request(
2370            "CreateSecret",
2371            r#"{"Name": "repl-secret", "SecretString": "val"}"#,
2372        );
2373        let resp = svc.handle(req).await.unwrap();
2374        let create_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2375        let expected_arn = create_body["ARN"].as_str().unwrap();
2376
2377        for action in &[
2378            "ReplicateSecretToRegions",
2379            "RemoveRegionsFromReplication",
2380            "StopReplicationToReplica",
2381        ] {
2382            let req = make_request(action, r#"{"SecretId": "repl-secret"}"#);
2383            let resp = svc.handle(req).await.unwrap();
2384            let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2385            assert_eq!(
2386                body["ARN"].as_str().unwrap(),
2387                expected_arn,
2388                "{action} should return the secret's actual ARN"
2389            );
2390        }
2391    }
2392
2393    #[tokio::test]
2394    async fn test_secret_id_length_validation() {
2395        let state = make_state();
2396        let svc = SecretsManagerService::new(state);
2397
2398        // SecretId too long (> 2048)
2399        let long_id = "x".repeat(2049);
2400        let req = make_request("GetSecretValue", &format!(r#"{{"SecretId": "{long_id}"}}"#));
2401        match svc.handle(req).await {
2402            Err(e) => assert!(e.to_string().contains("ValidationException")),
2403            Ok(_) => panic!("expected ValidationException"),
2404        }
2405    }
2406
2407    #[tokio::test]
2408    async fn test_name_length_validation() {
2409        let state = make_state();
2410        let svc = SecretsManagerService::new(state);
2411
2412        // Name too long (> 512)
2413        let long_name = "x".repeat(513);
2414        let req = make_request(
2415            "CreateSecret",
2416            &format!(r#"{{"Name": "{long_name}", "SecretString": "val"}}"#),
2417        );
2418        match svc.handle(req).await {
2419            Err(e) => assert!(e.to_string().contains("ValidationException")),
2420            Ok(_) => panic!("expected ValidationException"),
2421        }
2422    }
2423
2424    #[tokio::test]
2425    async fn test_next_token_length_validation() {
2426        let state = make_state();
2427        let svc = SecretsManagerService::new(state);
2428
2429        // NextToken too long (> 4096)
2430        let long_token = "x".repeat(4097);
2431        let req = make_request(
2432            "ListSecrets",
2433            &format!(r#"{{"NextToken": "{long_token}"}}"#),
2434        );
2435        match svc.handle(req).await {
2436            Err(e) => assert!(e.to_string().contains("ValidationException")),
2437            Ok(_) => panic!("expected ValidationException"),
2438        }
2439    }
2440
2441    #[tokio::test]
2442    async fn test_client_request_token_length_validation() {
2443        let state = make_state();
2444        let svc = SecretsManagerService::new(state);
2445
2446        // ClientRequestToken too short (< 32)
2447        let req = make_request(
2448            "CreateSecret",
2449            r#"{"Name": "test", "SecretString": "val", "ClientRequestToken": "short"}"#,
2450        );
2451        match svc.handle(req).await {
2452            Err(e) => assert!(e.to_string().contains("ValidationException")),
2453            Ok(_) => panic!("expected ValidationException"),
2454        }
2455    }
2456
2457    #[tokio::test]
2458    async fn test_rotate_secret_with_lambda_creates_pending_version() {
2459        let state = make_state();
2460        let svc = SecretsManagerService::new(state.clone());
2461
2462        // Create a secret
2463        let req = make_request(
2464            "CreateSecret",
2465            r#"{"Name": "rotate-me", "SecretString": "old-password"}"#,
2466        );
2467        svc.handle(req).await.unwrap();
2468
2469        // Rotate with a Lambda ARN
2470        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2471        let body = serde_json::json!({
2472            "SecretId": "rotate-me",
2473            "RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:rotator",
2474            "ClientRequestToken": token,
2475        });
2476        let req = make_request("RotateSecret", &body.to_string());
2477        let resp = svc.handle(req).await.unwrap();
2478        assert_eq!(resp.status, StatusCode::OK);
2479        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2480        assert_eq!(resp_body["VersionId"], token);
2481
2482        // Real AWS leaves the AWSPENDING version creation to the rotation
2483        // Lambda's createSecret step, so we should NOT pre-create it. Verify
2484        // that no version with the rotation token exists yet.
2485        let s = state.read();
2486        let secret = s.secrets.get("rotate-me").unwrap();
2487        assert!(
2488            !secret.versions.contains_key(token),
2489            "AWSPENDING version must not be pre-created; the rotation Lambda creates it"
2490        );
2491
2492        // Verify rotation config was set
2493        assert_eq!(
2494            secret.rotation_lambda_arn.as_deref(),
2495            Some("arn:aws:lambda:us-east-1:123456789012:function:rotator")
2496        );
2497        assert_eq!(secret.rotation_enabled, Some(true));
2498    }
2499
2500    #[tokio::test]
2501    async fn test_rotate_secret_without_lambda_promotes_directly() {
2502        let state = make_state();
2503        let svc = SecretsManagerService::new(state.clone());
2504
2505        // Create a secret
2506        let req = make_request(
2507            "CreateSecret",
2508            r#"{"Name": "rotate-no-lambda", "SecretString": "value1"}"#,
2509        );
2510        svc.handle(req).await.unwrap();
2511
2512        // Rotate without Lambda ARN
2513        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2514        let body = serde_json::json!({
2515            "SecretId": "rotate-no-lambda",
2516            "ClientRequestToken": token,
2517        });
2518        let req = make_request("RotateSecret", &body.to_string());
2519        svc.handle(req).await.unwrap();
2520
2521        // Verify the new version is AWSCURRENT (no pending)
2522        let s = state.read();
2523        let secret = s.secrets.get("rotate-no-lambda").unwrap();
2524        let new_ver = secret.versions.get(token).unwrap();
2525        assert!(new_ver.stages.contains(&"AWSCURRENT".to_string()));
2526        assert_eq!(secret.current_version_id.as_deref(), Some(token));
2527    }
2528
2529    #[tokio::test]
2530    async fn test_rotate_secret_stores_rotation_config() {
2531        let state = make_state();
2532        let svc = SecretsManagerService::new(state.clone());
2533
2534        let req = make_request(
2535            "CreateSecret",
2536            r#"{"Name": "rot-cfg", "SecretString": "pw"}"#,
2537        );
2538        svc.handle(req).await.unwrap();
2539
2540        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2541        let body = serde_json::json!({
2542            "SecretId": "rot-cfg",
2543            "RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:my-rotator",
2544            "RotationRules": { "AutomaticallyAfterDays": 30 },
2545            "ClientRequestToken": token,
2546        });
2547        let req = make_request("RotateSecret", &body.to_string());
2548        let resp = svc.handle(req).await.unwrap();
2549        assert_eq!(resp.status, StatusCode::OK);
2550
2551        let s = state.read();
2552        let secret = s.secrets.get("rot-cfg").unwrap();
2553        assert_eq!(secret.rotation_enabled, Some(true));
2554        assert_eq!(
2555            secret.rotation_lambda_arn.as_deref(),
2556            Some("arn:aws:lambda:us-east-1:123456789012:function:my-rotator")
2557        );
2558        assert!(secret.last_rotated_at.is_some());
2559        let rules = secret.rotation_rules.as_ref().unwrap();
2560        assert_eq!(rules.automatically_after_days, Some(30));
2561
2562        // The AWSPENDING version is created by the rotation Lambda's
2563        // createSecret step, not by RotateSecret itself, so verify that no
2564        // version with this token exists yet.
2565        assert!(!secret.versions.contains_key(token));
2566    }
2567
2568    #[tokio::test]
2569    async fn test_rotate_secret_version_stages_change() {
2570        let state = make_state();
2571        let svc = SecretsManagerService::new(state.clone());
2572
2573        let req = make_request(
2574            "CreateSecret",
2575            r#"{"Name": "rot-stages", "SecretString": "original"}"#,
2576        );
2577        svc.handle(req).await.unwrap();
2578
2579        // Get original version id
2580        let original_vid = {
2581            let s = state.read();
2582            let secret = s.secrets.get("rot-stages").unwrap();
2583            secret.current_version_id.clone().unwrap()
2584        };
2585
2586        // Rotate without Lambda (simple rotation)
2587        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2588        let body = serde_json::json!({
2589            "SecretId": "rot-stages",
2590            "ClientRequestToken": token,
2591        });
2592        let req = make_request("RotateSecret", &body.to_string());
2593        svc.handle(req).await.unwrap();
2594
2595        let s = state.read();
2596        let secret = s.secrets.get("rot-stages").unwrap();
2597
2598        // New version should be AWSCURRENT
2599        let new_ver = secret.versions.get(token).unwrap();
2600        assert!(new_ver.stages.contains(&"AWSCURRENT".to_string()));
2601
2602        // Old version should be AWSPREVIOUS
2603        let old_ver = secret.versions.get(&original_vid).unwrap();
2604        assert!(old_ver.stages.contains(&"AWSPREVIOUS".to_string()));
2605        assert!(!old_ver.stages.contains(&"AWSCURRENT".to_string()));
2606    }
2607
2608    #[tokio::test]
2609    async fn test_cancel_rotate_secret() {
2610        let state = make_state();
2611        let svc = SecretsManagerService::new(state.clone());
2612
2613        let req = make_request(
2614            "CreateSecret",
2615            r#"{"Name": "cancel-rot", "SecretString": "pw"}"#,
2616        );
2617        svc.handle(req).await.unwrap();
2618
2619        // Enable rotation first
2620        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2621        let body = serde_json::json!({
2622            "SecretId": "cancel-rot",
2623            "ClientRequestToken": token,
2624        });
2625        let req = make_request("RotateSecret", &body.to_string());
2626        svc.handle(req).await.unwrap();
2627
2628        // Verify rotation is enabled
2629        {
2630            let s = state.read();
2631            let secret = s.secrets.get("cancel-rot").unwrap();
2632            assert_eq!(secret.rotation_enabled, Some(true));
2633        }
2634
2635        // Cancel rotation
2636        let req = make_request("CancelRotateSecret", r#"{"SecretId": "cancel-rot"}"#);
2637        let resp = svc.handle(req).await.unwrap();
2638        assert_eq!(resp.status, StatusCode::OK);
2639        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2640        assert_eq!(body["Name"], "cancel-rot");
2641
2642        // Verify rotation is disabled
2643        let s = state.read();
2644        let secret = s.secrets.get("cancel-rot").unwrap();
2645        assert_eq!(secret.rotation_enabled, Some(false));
2646    }
2647
2648    #[tokio::test]
2649    async fn test_cancel_rotate_secret_fails_when_not_enabled() {
2650        let state = make_state();
2651        let svc = SecretsManagerService::new(state);
2652
2653        let req = make_request(
2654            "CreateSecret",
2655            r#"{"Name": "no-rot", "SecretString": "pw"}"#,
2656        );
2657        svc.handle(req).await.unwrap();
2658
2659        let req = make_request("CancelRotateSecret", r#"{"SecretId": "no-rot"}"#);
2660        let result = svc.handle(req).await;
2661        assert!(result.is_err());
2662    }
2663
2664    #[tokio::test]
2665    async fn test_batch_get_secret_value_multiple() {
2666        let state = make_state();
2667        let svc = SecretsManagerService::new(state);
2668
2669        for (name, val) in &[("batch-a", "va"), ("batch-b", "vb"), ("batch-c", "vc")] {
2670            let req = make_request(
2671                "CreateSecret",
2672                &format!(r#"{{"Name": "{name}", "SecretString": "{val}"}}"#),
2673            );
2674            svc.handle(req).await.unwrap();
2675        }
2676
2677        let body = serde_json::json!({
2678            "SecretIdList": ["batch-a", "batch-b", "batch-c"]
2679        });
2680        let req = make_request("BatchGetSecretValue", &body.to_string());
2681        let resp = svc.handle(req).await.unwrap();
2682        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2683
2684        let values = resp_body["SecretValues"].as_array().unwrap();
2685        assert_eq!(values.len(), 3);
2686
2687        // Verify each secret has the right value
2688        let names: Vec<&str> = values.iter().map(|v| v["Name"].as_str().unwrap()).collect();
2689        assert!(names.contains(&"batch-a"));
2690        assert!(names.contains(&"batch-b"));
2691        assert!(names.contains(&"batch-c"));
2692
2693        // Verify no errors
2694        assert!(resp_body.get("Errors").is_none());
2695    }
2696
2697    #[tokio::test]
2698    async fn test_batch_get_secret_value_with_missing() {
2699        let state = make_state();
2700        let svc = SecretsManagerService::new(state);
2701
2702        let req = make_request(
2703            "CreateSecret",
2704            r#"{"Name": "exists", "SecretString": "val"}"#,
2705        );
2706        svc.handle(req).await.unwrap();
2707
2708        let body = serde_json::json!({
2709            "SecretIdList": ["exists", "nonexistent"]
2710        });
2711        let req = make_request("BatchGetSecretValue", &body.to_string());
2712        let resp = svc.handle(req).await.unwrap();
2713        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2714
2715        let values = resp_body["SecretValues"].as_array().unwrap();
2716        assert_eq!(values.len(), 1);
2717        assert_eq!(values[0]["Name"], "exists");
2718
2719        let errors = resp_body["Errors"].as_array().unwrap();
2720        assert_eq!(errors.len(), 1);
2721        assert_eq!(errors[0]["SecretId"], "nonexistent");
2722        assert_eq!(errors[0]["ErrorCode"], "ResourceNotFoundException");
2723    }
2724
2725    #[tokio::test]
2726    async fn test_update_secret_changes_description_and_kms() {
2727        let state = make_state();
2728        let svc = SecretsManagerService::new(state);
2729
2730        let req = make_request(
2731            "CreateSecret",
2732            r#"{"Name": "updatable", "SecretString": "val", "Description": "old desc"}"#,
2733        );
2734        svc.handle(req).await.unwrap();
2735
2736        // Update description and KmsKeyId
2737        let body = serde_json::json!({
2738            "SecretId": "updatable",
2739            "Description": "new desc",
2740            "KmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/my-key"
2741        });
2742        let req = make_request("UpdateSecret", &body.to_string());
2743        let resp = svc.handle(req).await.unwrap();
2744        assert_eq!(resp.status, StatusCode::OK);
2745        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2746        assert_eq!(resp_body["Name"], "updatable");
2747        // No VersionId since no new value was provided
2748        assert!(resp_body.get("VersionId").is_none());
2749
2750        // Describe to verify changes
2751        let req = make_request("DescribeSecret", r#"{"SecretId": "updatable"}"#);
2752        let resp = svc.handle(req).await.unwrap();
2753        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2754        assert_eq!(body["Description"], "new desc");
2755        assert_eq!(
2756            body["KmsKeyId"],
2757            "arn:aws:kms:us-east-1:123456789012:key/my-key"
2758        );
2759    }
2760
2761    #[tokio::test]
2762    async fn test_update_secret_with_new_value() {
2763        let state = make_state();
2764        let svc = SecretsManagerService::new(state);
2765
2766        let req = make_request(
2767            "CreateSecret",
2768            r#"{"Name": "upd-val", "SecretString": "old"}"#,
2769        );
2770        svc.handle(req).await.unwrap();
2771
2772        // Update with a new value
2773        let body = serde_json::json!({
2774            "SecretId": "upd-val",
2775            "SecretString": "new-value"
2776        });
2777        let req = make_request("UpdateSecret", &body.to_string());
2778        let resp = svc.handle(req).await.unwrap();
2779        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2780        assert!(resp_body["VersionId"].as_str().is_some());
2781
2782        // Get should return new value
2783        let req = make_request("GetSecretValue", r#"{"SecretId": "upd-val"}"#);
2784        let resp = svc.handle(req).await.unwrap();
2785        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2786        assert_eq!(body["SecretString"], "new-value");
2787    }
2788
2789    #[tokio::test]
2790    async fn test_get_random_password_custom_length() {
2791        let state = make_state();
2792        let svc = SecretsManagerService::new(state);
2793
2794        let req = make_request("GetRandomPassword", r#"{"PasswordLength": 64}"#);
2795        let resp = svc.handle(req).await.unwrap();
2796        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2797        assert_eq!(body["RandomPassword"].as_str().unwrap().len(), 64);
2798    }
2799
2800    #[tokio::test]
2801    async fn test_get_random_password_exclude_chars() {
2802        let state = make_state();
2803        let svc = SecretsManagerService::new(state);
2804
2805        let req = make_request(
2806            "GetRandomPassword",
2807            r#"{"PasswordLength": 100, "ExcludeCharacters": "abc123"}"#,
2808        );
2809        let resp = svc.handle(req).await.unwrap();
2810        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2811        let password = body["RandomPassword"].as_str().unwrap();
2812        assert_eq!(password.len(), 100);
2813        assert!(!password.contains('a'));
2814        assert!(!password.contains('b'));
2815        assert!(!password.contains('c'));
2816        assert!(!password.contains('1'));
2817        assert!(!password.contains('2'));
2818        assert!(!password.contains('3'));
2819    }
2820
2821    #[tokio::test]
2822    async fn test_get_random_password_exclude_types() {
2823        let state = make_state();
2824        let svc = SecretsManagerService::new(state);
2825
2826        // Exclude everything except lowercase
2827        let body = serde_json::json!({
2828            "PasswordLength": 50,
2829            "ExcludeUppercase": true,
2830            "ExcludeNumbers": true,
2831            "ExcludePunctuation": true,
2832            "RequireEachIncludedType": false,
2833        });
2834        let req = make_request("GetRandomPassword", &body.to_string());
2835        let resp = svc.handle(req).await.unwrap();
2836        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2837        let password = resp_body["RandomPassword"].as_str().unwrap();
2838        assert_eq!(password.len(), 50);
2839        assert!(password.chars().all(|c| c.is_ascii_lowercase()));
2840    }
2841
2842    #[tokio::test]
2843    async fn test_get_random_password_too_short() {
2844        let state = make_state();
2845        let svc = SecretsManagerService::new(state);
2846
2847        let req = make_request("GetRandomPassword", r#"{"PasswordLength": 3}"#);
2848        assert!(svc.handle(req).await.is_err());
2849    }
2850
2851    #[tokio::test]
2852    async fn test_get_random_password_too_long() {
2853        let state = make_state();
2854        let svc = SecretsManagerService::new(state);
2855
2856        let req = make_request("GetRandomPassword", r#"{"PasswordLength": 4097}"#);
2857        assert!(svc.handle(req).await.is_err());
2858    }
2859
2860    #[tokio::test]
2861    async fn test_update_secret_version_stage_move_current() {
2862        let state = make_state();
2863        let svc = SecretsManagerService::new(state.clone());
2864
2865        let req = make_request(
2866            "CreateSecret",
2867            r#"{"Name": "stage-test", "SecretString": "v1"}"#,
2868        );
2869        svc.handle(req).await.unwrap();
2870
2871        // Put a second version
2872        let req = make_request(
2873            "PutSecretValue",
2874            r#"{"SecretId": "stage-test", "SecretString": "v2"}"#,
2875        );
2876        svc.handle(req).await.unwrap();
2877
2878        // Get version IDs
2879        let (v1_id, v2_id) = {
2880            let s = state.read();
2881            let secret = s.secrets.get("stage-test").unwrap();
2882            let current = secret.current_version_id.clone().unwrap();
2883            let previous = secret
2884                .versions
2885                .iter()
2886                .find(|(id, _)| **id != current)
2887                .map(|(id, _)| id.clone())
2888                .unwrap();
2889            (previous, current)
2890        };
2891
2892        // Move AWSCURRENT from v2 back to v1
2893        let body = serde_json::json!({
2894            "SecretId": "stage-test",
2895            "VersionStage": "AWSCURRENT",
2896            "MoveToVersionId": v1_id,
2897            "RemoveFromVersionId": v2_id,
2898        });
2899        let req = make_request("UpdateSecretVersionStage", &body.to_string());
2900        let resp = svc.handle(req).await.unwrap();
2901        assert_eq!(resp.status, StatusCode::OK);
2902
2903        // Verify v1 is now AWSCURRENT
2904        let s = state.read();
2905        let secret = s.secrets.get("stage-test").unwrap();
2906        let v1 = secret.versions.get(&v1_id).unwrap();
2907        assert!(v1.stages.contains(&"AWSCURRENT".to_string()));
2908
2909        // v2 should have AWSPREVIOUS
2910        let v2 = secret.versions.get(&v2_id).unwrap();
2911        assert!(v2.stages.contains(&"AWSPREVIOUS".to_string()));
2912        assert!(!v2.stages.contains(&"AWSCURRENT".to_string()));
2913
2914        assert_eq!(secret.current_version_id.as_deref(), Some(v1_id.as_str()));
2915    }
2916
2917    #[tokio::test]
2918    async fn test_update_secret_version_stage_custom_label() {
2919        let state = make_state();
2920        let svc = SecretsManagerService::new(state.clone());
2921
2922        let req = make_request(
2923            "CreateSecret",
2924            r#"{"Name": "custom-stage", "SecretString": "v1"}"#,
2925        );
2926        svc.handle(req).await.unwrap();
2927
2928        let vid = {
2929            let s = state.read();
2930            s.secrets
2931                .get("custom-stage")
2932                .unwrap()
2933                .current_version_id
2934                .clone()
2935                .unwrap()
2936        };
2937
2938        // Add a custom label
2939        let body = serde_json::json!({
2940            "SecretId": "custom-stage",
2941            "VersionStage": "MYAPP_LIVE",
2942            "MoveToVersionId": vid,
2943        });
2944        let req = make_request("UpdateSecretVersionStage", &body.to_string());
2945        svc.handle(req).await.unwrap();
2946
2947        let s = state.read();
2948        let secret = s.secrets.get("custom-stage").unwrap();
2949        let ver = secret.versions.get(&vid).unwrap();
2950        assert!(ver.stages.contains(&"MYAPP_LIVE".to_string()));
2951        assert!(ver.stages.contains(&"AWSCURRENT".to_string()));
2952    }
2953
2954    #[tokio::test]
2955    async fn test_validate_resource_policy() {
2956        let state = make_state();
2957        let svc = SecretsManagerService::new(state);
2958
2959        let policy = serde_json::json!({
2960            "Version": "2012-10-17",
2961            "Statement": [{
2962                "Effect": "Allow",
2963                "Principal": {"AWS": "arn:aws:iam::123456789012:root"},
2964                "Action": "secretsmanager:GetSecretValue",
2965                "Resource": "*"
2966            }]
2967        });
2968
2969        let body = serde_json::json!({
2970            "ResourcePolicy": policy.to_string(),
2971        });
2972        let req = make_request("ValidateResourcePolicy", &body.to_string());
2973        let resp = svc.handle(req).await.unwrap();
2974        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2975        assert_eq!(resp_body["PolicyValidationPassed"], true);
2976        assert_eq!(resp_body["ValidationErrors"].as_array().unwrap().len(), 0);
2977    }
2978
2979    #[tokio::test]
2980    async fn test_validate_resource_policy_requires_policy() {
2981        let state = make_state();
2982        let svc = SecretsManagerService::new(state);
2983
2984        let req = make_request("ValidateResourcePolicy", r#"{}"#);
2985        assert!(svc.handle(req).await.is_err());
2986    }
2987
2988    #[tokio::test]
2989    async fn test_put_get_delete_resource_policy() {
2990        let state = make_state();
2991        let svc = SecretsManagerService::new(state);
2992
2993        let req = make_request(
2994            "CreateSecret",
2995            r#"{"Name": "policy-secret", "SecretString": "val"}"#,
2996        );
2997        svc.handle(req).await.unwrap();
2998
2999        // Get policy (should be empty initially)
3000        let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3001        let resp = svc.handle(req).await.unwrap();
3002        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3003        assert_eq!(body["Name"], "policy-secret");
3004        assert!(body.get("ResourcePolicy").is_none());
3005
3006        // Put policy
3007        let policy = r#"{"Version":"2012-10-17","Statement":[]}"#;
3008        let put_body = serde_json::json!({
3009            "SecretId": "policy-secret",
3010            "ResourcePolicy": policy,
3011        });
3012        let req = make_request("PutResourcePolicy", &put_body.to_string());
3013        let resp = svc.handle(req).await.unwrap();
3014        assert_eq!(resp.status, StatusCode::OK);
3015
3016        // Get policy (should have it now)
3017        let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3018        let resp = svc.handle(req).await.unwrap();
3019        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3020        assert_eq!(body["ResourcePolicy"], policy);
3021
3022        // Delete policy
3023        let req = make_request("DeleteResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3024        let resp = svc.handle(req).await.unwrap();
3025        assert_eq!(resp.status, StatusCode::OK);
3026
3027        // Get again (should be gone)
3028        let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3029        let resp = svc.handle(req).await.unwrap();
3030        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3031        assert!(body.get("ResourcePolicy").is_none());
3032    }
3033
3034    #[tokio::test]
3035    async fn test_batch_get_secret_value_with_deleted() {
3036        let state = make_state();
3037        let svc = SecretsManagerService::new(state);
3038
3039        let req = make_request(
3040            "CreateSecret",
3041            r#"{"Name": "batch-del", "SecretString": "val"}"#,
3042        );
3043        svc.handle(req).await.unwrap();
3044
3045        // Soft-delete it
3046        let req = make_request("DeleteSecret", r#"{"SecretId": "batch-del"}"#);
3047        svc.handle(req).await.unwrap();
3048
3049        let body = serde_json::json!({
3050            "SecretIdList": ["batch-del"]
3051        });
3052        let req = make_request("BatchGetSecretValue", &body.to_string());
3053        let resp = svc.handle(req).await.unwrap();
3054        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3055
3056        // Should have 0 values and 1 error
3057        assert_eq!(resp_body["SecretValues"].as_array().unwrap().len(), 0);
3058        let errors = resp_body["Errors"].as_array().unwrap();
3059        assert_eq!(errors.len(), 1);
3060        assert_eq!(errors[0]["ErrorCode"], "InvalidRequestException");
3061    }
3062}