Skip to main content

winterbaume_accessanalyzer/
state.rs

1use std::collections::HashMap;
2
3use chrono::Utc;
4use thiserror::Error;
5
6use crate::types::{Analyzer, ArchiveRule, CriterionValue};
7
8#[derive(Debug, Default)]
9pub struct AccessAnalyzerState {
10    /// Analyzers keyed by name.
11    pub analyzers: HashMap<String, AnalyzerState>,
12}
13
14/// Internal state for a single analyzer, including its archive rules.
15#[derive(Debug, Clone)]
16pub struct AnalyzerState {
17    pub analyzer: Analyzer,
18    pub archive_rules: HashMap<String, ArchiveRule>,
19}
20
21/// Domain-specific error enum. Contains no HTTP status codes or AWS error type strings.
22#[derive(Debug, Error)]
23pub enum AccessAnalyzerError {
24    #[error("Analyzer {name} already exists.")]
25    AnalyzerAlreadyExists { name: String },
26
27    #[error("Analyzer {name} not found.")]
28    AnalyzerNotFound { name: String },
29
30    #[error("Archive rule {rule_name} already exists for analyzer {analyzer_name}.")]
31    ArchiveRuleAlreadyExists {
32        analyzer_name: String,
33        rule_name: String,
34    },
35
36    #[error("Archive rule {rule_name} not found for analyzer {analyzer_name}.")]
37    ArchiveRuleNotFound {
38        analyzer_name: String,
39        rule_name: String,
40    },
41
42    #[error("Resource {arn} not found.")]
43    ResourceNotFound { arn: String },
44
45    #[error("{message}")]
46    Validation { message: String },
47}
48
49impl AccessAnalyzerState {
50    // -------------------------------------------------------------------------
51    // Analyzers
52    // -------------------------------------------------------------------------
53
54    pub fn create_analyzer(
55        &mut self,
56        name: &str,
57        analyzer_type: &str,
58        tags: HashMap<String, String>,
59        region: &str,
60        account_id: &str,
61    ) -> Result<&Analyzer, AccessAnalyzerError> {
62        if self.analyzers.contains_key(name) {
63            return Err(AccessAnalyzerError::AnalyzerAlreadyExists {
64                name: name.to_string(),
65            });
66        }
67
68        let arn = format!("arn:aws:access-analyzer:{region}:{account_id}:analyzer/{name}");
69        let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
70
71        let analyzer = Analyzer {
72            arn,
73            name: name.to_string(),
74            analyzer_type: analyzer_type.to_string(),
75            status: "ACTIVE".to_string(),
76            created_at: now,
77            tags,
78        };
79
80        let state = AnalyzerState {
81            analyzer,
82            archive_rules: HashMap::new(),
83        };
84
85        self.analyzers.insert(name.to_string(), state);
86        Ok(&self.analyzers.get(name).unwrap().analyzer)
87    }
88
89    pub fn get_analyzer(&self, name: &str) -> Result<&AnalyzerState, AccessAnalyzerError> {
90        self.analyzers
91            .get(name)
92            .ok_or_else(|| AccessAnalyzerError::AnalyzerNotFound {
93                name: name.to_string(),
94            })
95    }
96
97    pub fn delete_analyzer(&mut self, name: &str) -> Result<(), AccessAnalyzerError> {
98        if self.analyzers.remove(name).is_none() {
99            return Err(AccessAnalyzerError::AnalyzerNotFound {
100                name: name.to_string(),
101            });
102        }
103        Ok(())
104    }
105
106    pub fn list_analyzers(&self, type_filter: Option<&str>) -> Vec<&Analyzer> {
107        self.analyzers
108            .values()
109            .filter(|s| {
110                type_filter.is_none() || type_filter == Some(s.analyzer.analyzer_type.as_str())
111            })
112            .map(|s| &s.analyzer)
113            .collect()
114    }
115
116    // -------------------------------------------------------------------------
117    // Archive Rules
118    // -------------------------------------------------------------------------
119
120    pub fn create_archive_rule(
121        &mut self,
122        analyzer_name: &str,
123        rule_name: &str,
124        filter: HashMap<String, CriterionValue>,
125    ) -> Result<(), AccessAnalyzerError> {
126        let analyzer_state = self.analyzers.get_mut(analyzer_name).ok_or_else(|| {
127            AccessAnalyzerError::AnalyzerNotFound {
128                name: analyzer_name.to_string(),
129            }
130        })?;
131
132        if analyzer_state.archive_rules.contains_key(rule_name) {
133            return Err(AccessAnalyzerError::ArchiveRuleAlreadyExists {
134                analyzer_name: analyzer_name.to_string(),
135                rule_name: rule_name.to_string(),
136            });
137        }
138
139        let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
140        let rule = ArchiveRule {
141            rule_name: rule_name.to_string(),
142            filter,
143            created_at: now.clone(),
144            updated_at: now,
145        };
146
147        analyzer_state
148            .archive_rules
149            .insert(rule_name.to_string(), rule);
150        Ok(())
151    }
152
153    pub fn get_archive_rule(
154        &self,
155        analyzer_name: &str,
156        rule_name: &str,
157    ) -> Result<&ArchiveRule, AccessAnalyzerError> {
158        let analyzer_state = self.analyzers.get(analyzer_name).ok_or_else(|| {
159            AccessAnalyzerError::AnalyzerNotFound {
160                name: analyzer_name.to_string(),
161            }
162        })?;
163
164        analyzer_state.archive_rules.get(rule_name).ok_or_else(|| {
165            AccessAnalyzerError::ArchiveRuleNotFound {
166                analyzer_name: analyzer_name.to_string(),
167                rule_name: rule_name.to_string(),
168            }
169        })
170    }
171
172    pub fn delete_archive_rule(
173        &mut self,
174        analyzer_name: &str,
175        rule_name: &str,
176    ) -> Result<(), AccessAnalyzerError> {
177        let analyzer_state = self.analyzers.get_mut(analyzer_name).ok_or_else(|| {
178            AccessAnalyzerError::AnalyzerNotFound {
179                name: analyzer_name.to_string(),
180            }
181        })?;
182
183        if analyzer_state.archive_rules.remove(rule_name).is_none() {
184            return Err(AccessAnalyzerError::ArchiveRuleNotFound {
185                analyzer_name: analyzer_name.to_string(),
186                rule_name: rule_name.to_string(),
187            });
188        }
189        Ok(())
190    }
191
192    pub fn list_archive_rules(
193        &self,
194        analyzer_name: &str,
195    ) -> Result<Vec<&ArchiveRule>, AccessAnalyzerError> {
196        let analyzer_state = self.analyzers.get(analyzer_name).ok_or_else(|| {
197            AccessAnalyzerError::AnalyzerNotFound {
198                name: analyzer_name.to_string(),
199            }
200        })?;
201
202        Ok(analyzer_state.archive_rules.values().collect())
203    }
204
205    // -------------------------------------------------------------------------
206    // Tags
207    // -------------------------------------------------------------------------
208
209    pub fn tag_resource(
210        &mut self,
211        resource_arn: &str,
212        tags: HashMap<String, String>,
213    ) -> Result<(), AccessAnalyzerError> {
214        // Find analyzer by ARN
215        if let Some(state) = self
216            .analyzers
217            .values_mut()
218            .find(|s| s.analyzer.arn == resource_arn)
219        {
220            state.analyzer.tags.extend(tags);
221            return Ok(());
222        }
223        Err(AccessAnalyzerError::ResourceNotFound {
224            arn: resource_arn.to_string(),
225        })
226    }
227
228    pub fn untag_resource(
229        &mut self,
230        resource_arn: &str,
231        tag_keys: &[String],
232    ) -> Result<(), AccessAnalyzerError> {
233        if let Some(state) = self
234            .analyzers
235            .values_mut()
236            .find(|s| s.analyzer.arn == resource_arn)
237        {
238            for key in tag_keys {
239                state.analyzer.tags.remove(key);
240            }
241            return Ok(());
242        }
243        Err(AccessAnalyzerError::ResourceNotFound {
244            arn: resource_arn.to_string(),
245        })
246    }
247
248    pub fn list_tags_for_resource(
249        &self,
250        resource_arn: &str,
251    ) -> Result<&HashMap<String, String>, AccessAnalyzerError> {
252        if let Some(state) = self
253            .analyzers
254            .values()
255            .find(|s| s.analyzer.arn == resource_arn)
256        {
257            return Ok(&state.analyzer.tags);
258        }
259        Err(AccessAnalyzerError::ResourceNotFound {
260            arn: resource_arn.to_string(),
261        })
262    }
263}