Skip to main content

winterbaume_secretsmanager/
state.rs

1use std::collections::HashMap;
2
3use chrono::Utc;
4
5use crate::types::*;
6
7#[derive(Debug, Default)]
8pub struct SecretsManagerState {
9    pub secrets: HashMap<String, Secret>,
10}
11
12#[derive(Debug, thiserror::Error)]
13pub enum SecretsManagerError {
14    #[error("A resource with the ID you requested already exists.")]
15    ResourceExists,
16    #[error("You tried to perform the operation on a secret that's currently marked deleted.")]
17    SecretMarkedDeleted,
18    #[error("Secrets Manager can't find the specified secret value for VersionId: {version_id}")]
19    SecretVersionNotFoundById { version_id: String },
20    #[error("Secrets Manager can't find the specified secret value for staging label: {stage}")]
21    SecretVersionNotFoundByStage { stage: String },
22    #[error(
23        "An error occurred (InvalidParameterException) when calling the DeleteSecret operation: RecoveryWindowInDays value must be between 7 and 30 days (inclusive)."
24    )]
25    InvalidRecoveryWindow,
26    #[error(
27        "An error occurred (InvalidParameterException) when calling the DeleteSecret operation: You can't use ForceDeleteWithoutRecovery in conjunction with RecoveryWindowInDays."
28    )]
29    ForceDeleteWithRecoveryWindow,
30    #[error("Secrets Manager can't find the specified secret.")]
31    SecretNotFound,
32    #[error("ClientError")]
33    PasswordTooShort,
34    #[error("InvalidParameterValue")]
35    PasswordTooLong,
36    #[error("InvalidParameterException")]
37    NoValidCharsForPassword,
38    #[error("RotationRules.AutomaticallyAfterDays must be within 1-1000.")]
39    InvalidRotationDays,
40    #[error(
41        "The parameter RemoveFromVersionId can't be empty. Staging label AWSCURRENT is currently attached to version {vid}, so you must explicitly reference that version in RemoveFromVersionId."
42    )]
43    MissingRemoveFromVersionId { vid: String },
44    #[error("Not a valid version: {version_id}")]
45    NotAValidVersion { version_id: String },
46}
47
48impl SecretsManagerState {
49    pub fn create_secret(
50        &mut self,
51        name: &str,
52        description: &str,
53        secret_string: Option<&str>,
54        secret_binary: Option<Vec<u8>>,
55        account_id: &str,
56        region: &str,
57        tags: HashMap<String, String>,
58    ) -> Result<&Secret, SecretsManagerError> {
59        if self.secrets.contains_key(name) {
60            return Err(SecretsManagerError::ResourceExists);
61        }
62
63        let arn = format!(
64            "arn:aws:secretsmanager:{region}:{account_id}:secret:{name}-{suffix}",
65            suffix = &uuid::Uuid::new_v4().to_string()[..6]
66        );
67        let now = Utc::now();
68
69        let has_value = secret_string.is_some() || secret_binary.is_some();
70
71        let mut versions = HashMap::new();
72        let current_version_id = if has_value {
73            let version_id = uuid::Uuid::new_v4().to_string();
74            let version = SecretVersion {
75                version_id: version_id.clone(),
76                secret_string: secret_string.map(|s| s.to_string()),
77                secret_binary,
78                created_date: now,
79                version_stages: vec!["AWSCURRENT".to_string()],
80            };
81            versions.insert(version_id.clone(), version);
82            Some(version_id)
83        } else {
84            None
85        };
86
87        let secret = Secret {
88            name: name.to_string(),
89            arn,
90            description: description.to_string(),
91            created_date: now,
92            last_changed_date: now,
93            versions,
94            current_version_id,
95            deleted_date: None,
96            tags,
97            resource_policy: None,
98            rotation_enabled: None,
99            rotation_lambda_arn: None,
100            rotation_rules: None,
101            last_rotated_date: None,
102            replication_status: Vec::new(),
103            primary_region: Some(region.to_string()),
104        };
105
106        self.secrets.insert(name.to_string(), secret);
107        Ok(self.secrets.get(name).unwrap())
108    }
109
110    pub fn get_secret_value(
111        &self,
112        secret_id: &str,
113    ) -> Result<(&Secret, &SecretVersion), SecretsManagerError> {
114        self.get_secret_value_by_stage(secret_id, None)
115    }
116
117    pub fn get_secret_value_by_version_id(
118        &self,
119        secret_id: &str,
120        version_id: &str,
121    ) -> Result<(&Secret, &SecretVersion), SecretsManagerError> {
122        let secret = self.find_secret(secret_id)?;
123
124        if secret.deleted_date.is_some() {
125            return Err(SecretsManagerError::SecretMarkedDeleted);
126        }
127
128        match secret.versions.get(version_id) {
129            Some(version) => Ok((secret, version)),
130            None => Err(SecretsManagerError::SecretVersionNotFoundById {
131                version_id: version_id.to_string(),
132            }),
133        }
134    }
135
136    pub fn get_secret_value_by_stage(
137        &self,
138        secret_id: &str,
139        version_stage: Option<&str>,
140    ) -> Result<(&Secret, &SecretVersion), SecretsManagerError> {
141        let secret = self.find_secret(secret_id)?;
142        let stage = version_stage.unwrap_or("AWSCURRENT");
143
144        if secret.deleted_date.is_some() {
145            return Err(SecretsManagerError::SecretMarkedDeleted);
146        }
147
148        // Find the version that has the requested stage label
149        for version in secret.versions.values() {
150            if version.version_stages.contains(&stage.to_string()) {
151                return Ok((secret, version));
152            }
153        }
154
155        Err(SecretsManagerError::SecretVersionNotFoundByStage {
156            stage: stage.to_string(),
157        })
158    }
159
160    pub fn put_secret_value(
161        &mut self,
162        secret_id: &str,
163        secret_string: Option<&str>,
164        secret_binary: Option<Vec<u8>>,
165    ) -> Result<(&Secret, String), SecretsManagerError> {
166        match self.put_secret_value_ext(secret_id, secret_string, secret_binary, None, None) {
167            Ok((secret, version_id, _stages)) => Ok((secret, version_id)),
168            Err(e) => Err(e),
169        }
170    }
171
172    pub fn put_secret_value_ext(
173        &mut self,
174        secret_id: &str,
175        secret_string: Option<&str>,
176        secret_binary: Option<Vec<u8>>,
177        client_request_token: Option<&str>,
178        version_stages: Option<&[String]>,
179    ) -> Result<(&Secret, String, Vec<String>), SecretsManagerError> {
180        let secret = self.find_secret_mut(secret_id)?;
181
182        if secret.deleted_date.is_some() {
183            return Err(SecretsManagerError::SecretMarkedDeleted);
184        }
185
186        let now = Utc::now();
187        let new_version_id = client_request_token
188            .map(|s| s.to_string())
189            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
190
191        let stages = version_stages
192            .map(|s| s.to_vec())
193            .unwrap_or_else(|| vec!["AWSCURRENT".to_string()]);
194
195        let is_current = stages.contains(&"AWSCURRENT".to_string());
196
197        if is_current {
198            // Move AWSCURRENT from old version to AWSPREVIOUS
199            if let Some(old_vid) = &secret.current_version_id
200                && let Some(old_version) = secret.versions.get_mut(old_vid)
201            {
202                old_version.version_stages.retain(|s| s != "AWSCURRENT");
203                if !old_version
204                    .version_stages
205                    .contains(&"AWSPREVIOUS".to_string())
206                {
207                    old_version.version_stages.push("AWSPREVIOUS".to_string());
208                }
209            }
210        }
211
212        // For non-AWSCURRENT stages, remove stage labels from any existing versions that have them
213        for stage in &stages {
214            if stage != "AWSCURRENT" {
215                for existing_version in secret.versions.values_mut() {
216                    existing_version.version_stages.retain(|s| s != stage);
217                }
218            }
219        }
220
221        let version = SecretVersion {
222            version_id: new_version_id.clone(),
223            secret_string: secret_string.map(|s| s.to_string()),
224            secret_binary,
225            created_date: now,
226            version_stages: stages.clone(),
227        };
228
229        secret.versions.insert(new_version_id.clone(), version);
230        if is_current {
231            secret.current_version_id = Some(new_version_id.clone());
232        }
233        secret.last_changed_date = now;
234
235        // Clean up versions with no stages
236        secret.versions.retain(|_k, v| !v.version_stages.is_empty());
237
238        let name = secret.name.clone();
239        Ok((self.secrets.get(&name).unwrap(), new_version_id, stages))
240    }
241
242    pub fn delete_secret(
243        &mut self,
244        secret_id: &str,
245        recovery_window_in_days: Option<i64>,
246        force_delete: bool,
247        account_id: &str,
248        region: &str,
249    ) -> Result<Secret, SecretsManagerError> {
250        // Validate recovery window
251        if let Some(days) = recovery_window_in_days
252            && !(7..=30).contains(&days)
253        {
254            return Err(SecretsManagerError::InvalidRecoveryWindow);
255        }
256
257        // Cannot specify both force delete and recovery window
258        if force_delete && recovery_window_in_days.is_some() {
259            return Err(SecretsManagerError::ForceDeleteWithRecoveryWindow);
260        }
261
262        if force_delete {
263            // Force delete - for non-existent secrets, moto returns success with the name
264            match self.resolve_name(secret_id) {
265                Ok(name) => {
266                    let _secret = self.find_secret_mut_raw(&name)?;
267                    // Allow force-deleting already-deleted secrets
268                    let mut secret = self.secrets.remove(&name).unwrap();
269                    secret.deleted_date = Some(Utc::now());
270                    return Ok(secret);
271                }
272                Err(_) => {
273                    // For force delete on non-existent secret, moto creates a stub response
274                    return Ok(Secret {
275                        name: secret_id.to_string(),
276                        arn: format!(
277                            "arn:aws:secretsmanager:{region}:{account_id}:secret:{secret_id}"
278                        ),
279                        description: String::new(),
280                        created_date: Utc::now(),
281                        last_changed_date: Utc::now(),
282                        versions: HashMap::new(),
283                        current_version_id: None,
284                        deleted_date: Some(Utc::now()),
285                        tags: HashMap::new(),
286                        resource_policy: None,
287                        rotation_enabled: None,
288                        rotation_lambda_arn: None,
289                        rotation_rules: None,
290                        last_rotated_date: None,
291                        replication_status: Vec::new(),
292                        primary_region: None,
293                    });
294                }
295            }
296        }
297
298        let secret = self.find_secret_mut(secret_id)?;
299
300        if secret.deleted_date.is_some() {
301            return Err(SecretsManagerError::SecretMarkedDeleted);
302        }
303
304        let _window = recovery_window_in_days.unwrap_or(30);
305        secret.deleted_date = Some(Utc::now());
306
307        Ok(secret.clone())
308    }
309
310    fn find_secret_mut_raw(&mut self, name: &str) -> Result<&mut Secret, SecretsManagerError> {
311        self.secrets
312            .get_mut(name)
313            .ok_or(SecretsManagerError::SecretNotFound)
314    }
315
316    pub fn restore_secret(&mut self, secret_id: &str) -> Result<&Secret, SecretsManagerError> {
317        let secret = self.find_secret_mut(secret_id)?;
318        // AWS allows calling RestoreSecret on non-deleted secrets (it's a no-op)
319        secret.deleted_date = None;
320        let name = secret.name.clone();
321        Ok(self.secrets.get(&name).unwrap())
322    }
323
324    pub fn describe_secret(&self, secret_id: &str) -> Result<&Secret, SecretsManagerError> {
325        self.find_secret(secret_id)
326    }
327
328    pub fn list_secrets(&self) -> Vec<&Secret> {
329        self.secrets
330            .values()
331            .filter(|s| s.deleted_date.is_none())
332            .collect()
333    }
334
335    pub fn update_secret(
336        &mut self,
337        secret_id: &str,
338        description: Option<&str>,
339        secret_string: Option<&str>,
340        secret_binary: Option<Vec<u8>>,
341    ) -> Result<&Secret, SecretsManagerError> {
342        let secret = self.find_secret_mut(secret_id)?;
343
344        if let Some(desc) = description {
345            secret.description = desc.to_string();
346        }
347
348        if secret_string.is_some() || secret_binary.is_some() {
349            let now = Utc::now();
350            let new_version_id = uuid::Uuid::new_v4().to_string();
351
352            if let Some(old_vid) = &secret.current_version_id
353                && let Some(old_version) = secret.versions.get_mut(old_vid)
354            {
355                old_version.version_stages.retain(|s| s != "AWSCURRENT");
356                if !old_version
357                    .version_stages
358                    .contains(&"AWSPREVIOUS".to_string())
359                {
360                    old_version.version_stages.push("AWSPREVIOUS".to_string());
361                }
362            }
363
364            let version = SecretVersion {
365                version_id: new_version_id.clone(),
366                secret_string: secret_string.map(|s| s.to_string()),
367                secret_binary,
368                created_date: now,
369                version_stages: vec!["AWSCURRENT".to_string()],
370            };
371
372            secret.versions.insert(new_version_id.clone(), version);
373            secret.current_version_id = Some(new_version_id);
374            secret.last_changed_date = now;
375        }
376
377        let name = secret.name.clone();
378        Ok(self.secrets.get(&name).unwrap())
379    }
380
381    pub fn batch_get_secret_value(
382        &self,
383        secret_ids: &[String],
384    ) -> Result<Vec<(&Secret, &SecretVersion)>, Vec<(String, SecretsManagerError)>> {
385        let mut values = Vec::new();
386        let mut errors = Vec::new();
387
388        for secret_id in secret_ids {
389            match self.get_secret_value(secret_id) {
390                Ok(pair) => values.push(pair),
391                Err(e) => errors.push((secret_id.clone(), e)),
392            }
393        }
394
395        if values.is_empty() && !errors.is_empty() {
396            Err(errors)
397        } else {
398            Ok(values)
399        }
400    }
401
402    pub fn cancel_rotate_secret(
403        &mut self,
404        secret_id: &str,
405    ) -> Result<&Secret, SecretsManagerError> {
406        let secret = self.find_secret_mut(secret_id)?;
407        // Cancel any pending rotation - just return current state
408        let name = secret.name.clone();
409        Ok(self.secrets.get(&name).unwrap())
410    }
411
412    pub fn get_random_password(
413        &self,
414        password_length: Option<i64>,
415        exclude_characters: Option<&str>,
416        exclude_numbers: bool,
417        exclude_punctuation: bool,
418        exclude_uppercase: bool,
419        exclude_lowercase: bool,
420        include_space: bool,
421        require_each_included_type: bool,
422    ) -> Result<String, SecretsManagerError> {
423        let length = password_length.unwrap_or(32) as usize;
424        if length < 4 {
425            return Err(SecretsManagerError::PasswordTooShort);
426        }
427        if length > 4096 {
428            return Err(SecretsManagerError::PasswordTooLong);
429        }
430
431        let exclude_chars = exclude_characters.unwrap_or("");
432
433        let lowercase = "abcdefghijklmnopqrstuvwxyz";
434        let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
435        let digits = "0123456789";
436        let punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
437
438        let mut chars = String::new();
439        let mut required_chars: Vec<char> = Vec::new();
440
441        // Space goes first in required chars since it's explicitly requested
442        if include_space {
443            chars.push(' ');
444            required_chars.push(' ');
445        }
446        if !exclude_lowercase {
447            chars.push_str(lowercase);
448            let filtered: Vec<char> = lowercase
449                .chars()
450                .filter(|c| !exclude_chars.contains(*c))
451                .collect();
452            if !filtered.is_empty() {
453                let idx = (uuid::Uuid::new_v4().as_bytes()[0] as usize) % filtered.len();
454                required_chars.push(filtered[idx]);
455            }
456        }
457        if !exclude_uppercase {
458            chars.push_str(uppercase);
459            let filtered: Vec<char> = uppercase
460                .chars()
461                .filter(|c| !exclude_chars.contains(*c))
462                .collect();
463            if !filtered.is_empty() {
464                let idx = (uuid::Uuid::new_v4().as_bytes()[0] as usize) % filtered.len();
465                required_chars.push(filtered[idx]);
466            }
467        }
468        if !exclude_numbers {
469            chars.push_str(digits);
470            let filtered: Vec<char> = digits
471                .chars()
472                .filter(|c| !exclude_chars.contains(*c))
473                .collect();
474            if !filtered.is_empty() {
475                let idx = (uuid::Uuid::new_v4().as_bytes()[0] as usize) % filtered.len();
476                required_chars.push(filtered[idx]);
477            }
478        }
479        if !exclude_punctuation {
480            chars.push_str(punctuation);
481            let filtered: Vec<char> = punctuation
482                .chars()
483                .filter(|c| !exclude_chars.contains(*c))
484                .collect();
485            if !filtered.is_empty() {
486                let idx = (uuid::Uuid::new_v4().as_bytes()[0] as usize) % filtered.len();
487                required_chars.push(filtered[idx]);
488            }
489        }
490
491        if !exclude_chars.is_empty() {
492            chars = chars
493                .chars()
494                .filter(|c| !exclude_chars.contains(*c))
495                .collect();
496        }
497
498        if chars.is_empty() {
499            return Err(SecretsManagerError::NoValidCharsForPassword);
500        }
501
502        let char_vec: Vec<char> = chars.chars().collect();
503        let mut password: Vec<char> = (0..length)
504            .map(|i| {
505                let idx = (uuid::Uuid::new_v4().as_bytes()[i % 16] as usize) % char_vec.len();
506                char_vec[idx]
507            })
508            .collect();
509
510        // Ensure each required type is represented
511        if require_each_included_type {
512            for (i, required) in required_chars.iter().enumerate() {
513                if i < password.len() {
514                    password[i] = *required;
515                }
516            }
517        }
518
519        Ok(password.into_iter().collect())
520    }
521
522    pub fn put_resource_policy(
523        &mut self,
524        secret_id: &str,
525        resource_policy: &str,
526    ) -> Result<&Secret, SecretsManagerError> {
527        let secret = self.find_secret_mut(secret_id)?;
528        secret.resource_policy = Some(resource_policy.to_string());
529        secret.last_changed_date = Utc::now();
530        let name = secret.name.clone();
531        Ok(self.secrets.get(&name).unwrap())
532    }
533
534    pub fn get_resource_policy(&self, secret_id: &str) -> Result<&Secret, SecretsManagerError> {
535        self.find_secret(secret_id)
536    }
537
538    pub fn delete_resource_policy(
539        &mut self,
540        secret_id: &str,
541    ) -> Result<&Secret, SecretsManagerError> {
542        let secret = self.find_secret_mut(secret_id)?;
543        secret.resource_policy = None;
544        secret.last_changed_date = Utc::now();
545        let name = secret.name.clone();
546        Ok(self.secrets.get(&name).unwrap())
547    }
548
549    pub fn list_secret_version_ids(&self, secret_id: &str) -> Result<&Secret, SecretsManagerError> {
550        self.find_secret(secret_id)
551    }
552
553    pub fn rotate_secret(
554        &mut self,
555        secret_id: &str,
556        rotation_lambda_arn: Option<&str>,
557        rotation_rules: Option<RotationRules>,
558    ) -> Result<(&Secret, String), SecretsManagerError> {
559        let secret = self.find_secret_mut(secret_id)?;
560
561        if secret.deleted_date.is_some() {
562            return Err(SecretsManagerError::SecretMarkedDeleted);
563        }
564
565        // Validate rotation rules before applying
566        if let Some(ref rules) = rotation_rules
567            && let Some(days) = rules.automatically_after_days
568            && (!(1..=1000).contains(&days))
569        {
570            return Err(SecretsManagerError::InvalidRotationDays);
571        }
572
573        if let Some(arn) = rotation_lambda_arn {
574            secret.rotation_lambda_arn = Some(arn.to_string());
575        }
576        if let Some(rules) = rotation_rules {
577            secret.rotation_rules = Some(rules);
578        }
579        secret.rotation_enabled = Some(true);
580
581        // Create a new version to simulate rotation
582        let now = Utc::now();
583        let new_version_id = uuid::Uuid::new_v4().to_string();
584
585        // Move AWSCURRENT from old version to AWSPREVIOUS
586        if let Some(old_vid) = &secret.current_version_id
587            && let Some(old_version) = secret.versions.get_mut(old_vid)
588        {
589            old_version.version_stages.retain(|s| s != "AWSCURRENT");
590            if !old_version
591                .version_stages
592                .contains(&"AWSPREVIOUS".to_string())
593            {
594                old_version.version_stages.push("AWSPREVIOUS".to_string());
595            }
596        }
597
598        // Copy current secret value into new version
599        let secret_string = secret
600            .current_version_id
601            .as_ref()
602            .and_then(|vid| secret.versions.get(vid))
603            .and_then(|v| v.secret_string.clone());
604
605        let version = SecretVersion {
606            version_id: new_version_id.clone(),
607            secret_string,
608            secret_binary: None,
609            created_date: now,
610            version_stages: vec!["AWSCURRENT".to_string()],
611        };
612
613        secret.versions.insert(new_version_id.clone(), version);
614        secret.current_version_id = Some(new_version_id.clone());
615        secret.last_changed_date = now;
616        secret.last_rotated_date = Some(now);
617
618        let name = secret.name.clone();
619        Ok((self.secrets.get(&name).unwrap(), new_version_id))
620    }
621
622    pub fn tag_resource(
623        &mut self,
624        secret_id: &str,
625        tags: HashMap<String, String>,
626    ) -> Result<(), SecretsManagerError> {
627        let secret = self.find_secret_mut(secret_id)?;
628        for (k, v) in tags {
629            secret.tags.insert(k, v);
630        }
631        secret.last_changed_date = Utc::now();
632        Ok(())
633    }
634
635    pub fn untag_resource(
636        &mut self,
637        secret_id: &str,
638        tag_keys: &[String],
639    ) -> Result<(), SecretsManagerError> {
640        let secret = self.find_secret_mut(secret_id)?;
641        for key in tag_keys {
642            secret.tags.remove(key);
643        }
644        secret.last_changed_date = Utc::now();
645        Ok(())
646    }
647
648    pub fn update_secret_version_stage(
649        &mut self,
650        secret_id: &str,
651        version_stage: &str,
652        move_to_version_id: Option<&str>,
653        remove_from_version_id: Option<&str>,
654    ) -> Result<&Secret, SecretsManagerError> {
655        let secret = self.find_secret_mut(secret_id)?;
656
657        // If moving AWSCURRENT and no remove_from is specified, require it
658        if version_stage == "AWSCURRENT" && remove_from_version_id.is_none() {
659            let current_vid = secret
660                .versions
661                .values()
662                .find(|v| v.version_stages.contains(&"AWSCURRENT".to_string()))
663                .map(|v| v.version_id.clone());
664            if let Some(vid) = current_vid {
665                return Err(SecretsManagerError::MissingRemoveFromVersionId { vid });
666            }
667        }
668
669        if let Some(remove_vid) = remove_from_version_id {
670            if let Some(version) = secret.versions.get_mut(remove_vid) {
671                version.version_stages.retain(|s| s != version_stage);
672            } else {
673                return Err(SecretsManagerError::NotAValidVersion {
674                    version_id: remove_vid.to_string(),
675                });
676            }
677        }
678
679        if let Some(move_vid) = move_to_version_id {
680            if let Some(version) = secret.versions.get_mut(move_vid) {
681                if !version.version_stages.contains(&version_stage.to_string()) {
682                    version.version_stages.push(version_stage.to_string());
683                }
684            } else {
685                return Err(SecretsManagerError::NotAValidVersion {
686                    version_id: move_vid.to_string(),
687                });
688            }
689        }
690
691        // When moving AWSCURRENT, automatically manage AWSPREVIOUS
692        if version_stage == "AWSCURRENT" {
693            if let Some(remove_vid) = remove_from_version_id {
694                let remove_vid = remove_vid.to_string();
695                // Remove AWSPREVIOUS from whatever version currently has it
696                for version in secret.versions.values_mut() {
697                    version.version_stages.retain(|s| s != "AWSPREVIOUS");
698                }
699                // Add AWSPREVIOUS to the version that AWSCURRENT was removed from
700                if let Some(version) = secret.versions.get_mut(&remove_vid) {
701                    version.version_stages.push("AWSPREVIOUS".to_string());
702                }
703            }
704
705            // Remove AWSPREVIOUS from the target version if it has it
706            if let Some(move_vid) = move_to_version_id
707                && let Some(version) = secret.versions.get_mut(move_vid)
708            {
709                version.version_stages.retain(|s| s != "AWSPREVIOUS");
710            }
711
712            if let Some(move_vid) = move_to_version_id {
713                secret.current_version_id = Some(move_vid.to_string());
714            }
715        }
716
717        secret.last_changed_date = Utc::now();
718        let name = secret.name.clone();
719        Ok(self.secrets.get(&name).unwrap())
720    }
721
722    pub fn replicate_secret_to_regions(
723        &mut self,
724        secret_id: &str,
725        regions: &[String],
726    ) -> Result<&Secret, SecretsManagerError> {
727        let secret = self.find_secret_mut(secret_id)?;
728
729        for region in regions {
730            // Don't add duplicate regions
731            if !secret
732                .replication_status
733                .iter()
734                .any(|r| r.region == *region)
735            {
736                secret.replication_status.push(ReplicationStatus {
737                    region: region.clone(),
738                    status: "InSync".to_string(),
739                    status_message: None,
740                    kms_key_id: None,
741                    last_accessed_date: None,
742                });
743            }
744        }
745
746        secret.last_changed_date = Utc::now();
747        let name = secret.name.clone();
748        Ok(self.secrets.get(&name).unwrap())
749    }
750
751    pub fn remove_regions_from_replication(
752        &mut self,
753        secret_id: &str,
754        regions: &[String],
755    ) -> Result<&Secret, SecretsManagerError> {
756        let secret = self.find_secret_mut(secret_id)?;
757        secret
758            .replication_status
759            .retain(|r| !regions.contains(&r.region));
760        secret.last_changed_date = Utc::now();
761        let name = secret.name.clone();
762        Ok(self.secrets.get(&name).unwrap())
763    }
764
765    fn find_secret(&self, secret_id: &str) -> Result<&Secret, SecretsManagerError> {
766        // Try by name first
767        if let Some(secret) = self.secrets.get(secret_id) {
768            return Ok(secret);
769        }
770        // Try by ARN
771        for secret in self.secrets.values() {
772            if secret.arn == secret_id {
773                return Ok(secret);
774            }
775        }
776        Err(SecretsManagerError::SecretNotFound)
777    }
778
779    fn find_secret_mut(&mut self, secret_id: &str) -> Result<&mut Secret, SecretsManagerError> {
780        let name = self.resolve_name(secret_id)?;
781        Ok(self.secrets.get_mut(&name).unwrap())
782    }
783
784    fn resolve_name(&self, secret_id: &str) -> Result<String, SecretsManagerError> {
785        if self.secrets.contains_key(secret_id) {
786            return Ok(secret_id.to_string());
787        }
788        for secret in self.secrets.values() {
789            if secret.arn == secret_id {
790                return Ok(secret.name.clone());
791            }
792        }
793        Err(SecretsManagerError::SecretNotFound)
794    }
795}