Skip to main content

fakecloud_secretsmanager/
service.rs

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