iam_rs/policy/
condition.rs

1use crate::{
2    core::Operator,
3    validation::{Validate, ValidationContext, ValidationResult, ValidationError, helpers},
4};
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use std::collections::{BTreeMap, HashMap};
7
8/// Represents a single condition in an IAM policy
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct Condition {
11    /// The condition operator (e.g., StringEquals, DateGreaterThan)
12    pub operator: Operator,
13    /// The condition key (e.g., "aws:username", "s3:prefix")
14    pub key: String,
15    /// The condition value(s)
16    pub value: serde_json::Value,
17}
18
19/// Represents a condition block in an IAM policy
20/// This is a collection of conditions grouped by operator
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ConditionBlock {
23    /// Map of operators to their key-value pairs
24    pub conditions: HashMap<Operator, HashMap<String, serde_json::Value>>,
25}
26
27impl Serialize for ConditionBlock {
28    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
29    where
30        S: Serializer,
31    {
32        // Convert the HashMap<Operator, ...> to BTreeMap<String, ...> for ordered serialization
33        // Also, sort the keys within each condition (e.g., inside StringEquals)
34        let ordered_map: BTreeMap<String, BTreeMap<String, &serde_json::Value>> = self
35            .conditions
36            .iter()
37            .map(|(op, conditions)| {
38                let inner_ordered: BTreeMap<String, &serde_json::Value> = conditions
39                    .iter()
40                    .map(|(k, v)| (k.clone(), v))
41                    .collect();
42                (op.as_str().to_string(), inner_ordered)
43            })
44            .collect();
45
46        ordered_map.serialize(serializer)
47    }
48}
49
50impl<'de> Deserialize<'de> for ConditionBlock {
51    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
52    where
53        D: Deserializer<'de>,
54    {
55        let string_map: HashMap<String, HashMap<String, serde_json::Value>> =
56            HashMap::deserialize(deserializer)?;
57
58        let mut conditions = HashMap::new();
59
60        for (op_str, condition_map) in string_map {
61            let operator = op_str.parse::<Operator>().map_err(|e| {
62                serde::de::Error::custom(format!("Invalid operator '{}': {}", op_str, e))
63            })?;
64            conditions.insert(operator, condition_map);
65        }
66
67        Ok(ConditionBlock { conditions })
68    }
69}
70
71impl Condition {
72    /// Creates a new condition
73    pub fn new<K: Into<String>>(operator: Operator, key: K, value: serde_json::Value) -> Self {
74        Self {
75            operator,
76            key: key.into(),
77            value,
78        }
79    }
80
81    /// Creates a condition with a string value
82    pub fn string<K: Into<String>, V: Into<String>>(operator: Operator, key: K, value: V) -> Self {
83        Self::new(operator, key, serde_json::Value::String(value.into()))
84    }
85
86    /// Creates a condition with a boolean value
87    pub fn boolean<K: Into<String>>(operator: Operator, key: K, value: bool) -> Self {
88        Self::new(operator, key, serde_json::Value::Bool(value))
89    }
90
91    /// Creates a condition with a numeric value
92    pub fn number<K: Into<String>>(operator: Operator, key: K, value: i64) -> Self {
93        Self::new(
94            operator,
95            key,
96            serde_json::Value::Number(serde_json::Number::from(value)),
97        )
98    }
99
100    /// Creates a condition with an array of string values
101    pub fn string_array<K: Into<String>>(operator: Operator, key: K, values: Vec<String>) -> Self {
102        let json_values: Vec<serde_json::Value> =
103            values.into_iter().map(serde_json::Value::String).collect();
104        Self::new(operator, key, serde_json::Value::Array(json_values))
105    }
106}
107
108impl ConditionBlock {
109    /// Creates a new empty condition block
110    pub fn new() -> Self {
111        Self {
112            conditions: HashMap::new(),
113        }
114    }
115
116    /// Adds a condition to the block
117    pub fn add_condition(&mut self, condition: Condition) {
118        let operator_map = self
119            .conditions
120            .entry(condition.operator)
121            .or_insert_with(HashMap::new);
122        operator_map.insert(condition.key, condition.value);
123    }
124
125    /// Adds a condition using the builder pattern
126    pub fn with_condition(mut self, condition: Condition) -> Self {
127        self.add_condition(condition);
128        self
129    }
130
131    /// Adds a condition directly with operator, key, and value
132    pub fn with_condition_direct<K: Into<String>>(
133        mut self,
134        operator: Operator,
135        key: K,
136        value: serde_json::Value,
137    ) -> Self {
138        let condition = Condition::new(operator, key, value);
139        self.add_condition(condition);
140        self
141    }
142
143    /// Gets all conditions for a specific operator
144    pub fn get_conditions_for_operator(
145        &self,
146        operator: &Operator,
147    ) -> Option<&HashMap<String, serde_json::Value>> {
148        self.conditions.get(operator)
149    }
150
151    /// Gets a specific condition value
152    pub fn get_condition_value(
153        &self,
154        operator: &Operator,
155        key: &str,
156    ) -> Option<&serde_json::Value> {
157        self.conditions.get(operator)?.get(key)
158    }
159
160    /// Checks if a condition exists
161    pub fn has_condition(&self, operator: &Operator, key: &str) -> bool {
162        self.conditions
163            .get(operator)
164            .map(|map| map.contains_key(key))
165            .unwrap_or(false)
166    }
167
168    /// Gets all operators used in this condition block
169    pub fn operators(&self) -> Vec<&Operator> {
170        self.conditions.keys().collect()
171    }
172
173    /// Checks if the condition block is empty
174    pub fn is_empty(&self) -> bool {
175        self.conditions.is_empty()
176    }
177
178    /// Converts to the legacy HashMap format for backward compatibility
179    pub fn to_legacy_format(&self) -> HashMap<String, HashMap<String, serde_json::Value>> {
180        self.conditions
181            .iter()
182            .map(|(op, conditions)| (op.as_str().to_string(), conditions.clone()))
183            .collect()
184    }
185
186    /// Creates a condition block from the legacy HashMap format
187    pub fn from_legacy_format(
188        legacy: HashMap<String, HashMap<String, serde_json::Value>>,
189    ) -> Result<Self, String> {
190        let mut conditions = HashMap::new();
191
192        for (op_str, condition_map) in legacy {
193            let operator = op_str
194                .parse::<Operator>()
195                .map_err(|e| format!("Invalid operator '{}': {}", op_str, e))?;
196            conditions.insert(operator, condition_map);
197        }
198
199        Ok(Self { conditions })
200    }
201}
202
203impl Default for ConditionBlock {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209impl Validate for Condition {
210    fn validate(&self, context: &mut ValidationContext) -> ValidationResult {
211        context.with_segment("Condition", |ctx| {
212            let mut results = Vec::new();
213
214            // Validate that key is not empty
215            results.push(helpers::validate_non_empty(&self.key, "key", ctx));
216
217            // Validate that the operator and value are compatible
218            match &self.value {
219                serde_json::Value::Null => {
220                    results.push(Err(ValidationError::InvalidCondition {
221                        operator: self.operator.as_str().to_string(),
222                        key: self.key.clone(),
223                        reason: "Condition value cannot be null".to_string(),
224                    }));
225                }
226                serde_json::Value::Array(arr) => {
227                    if arr.is_empty() {
228                        results.push(Err(ValidationError::InvalidCondition {
229                            operator: self.operator.as_str().to_string(),
230                            key: self.key.clone(),
231                            reason: "Condition value array cannot be empty".to_string(),
232                        }));
233                    }
234                    
235                    // Check if operator supports multiple values
236                    if !self.operator.supports_multiple_values() && arr.len() > 1 {
237                        results.push(Err(ValidationError::InvalidCondition {
238                            operator: self.operator.as_str().to_string(),
239                            key: self.key.clone(),
240                            reason: format!("Operator {} does not support multiple values", self.operator.as_str()),
241                        }));
242                    }
243                }
244                _ => {} // Single values are generally OK
245            }
246
247            // Validate operator-specific rules
248            match self.operator.category() {
249                    "String" => {
250                        // String operators should have string values
251                        match &self.value {
252                            serde_json::Value::String(_) => {},
253                            serde_json::Value::Array(arr) => {
254                                for (i, val) in arr.iter().enumerate() {
255                                    if !val.is_string() {
256                                        results.push(Err(ValidationError::InvalidCondition {
257                                            operator: self.operator.as_str().to_string(),
258                                            key: self.key.clone(),
259                                            reason: format!("String operator requires string values, found {} at index {}", val, i),
260                                        }));
261                                    }
262                                }
263                            },
264                            _ => {
265                                results.push(Err(ValidationError::InvalidCondition {
266                                    operator: self.operator.as_str().to_string(),
267                                    key: self.key.clone(),
268                                    reason: "String operator requires string value(s)".to_string(),
269                                }));
270                            }
271                        }
272                    },
273                    "Numeric" => {
274                        // Numeric operators should have numeric values
275                        match &self.value {
276                            serde_json::Value::Number(_) => {},
277                            serde_json::Value::String(s) => {
278                                // Allow string representation of numbers
279                                if s.parse::<f64>().is_err() {
280                                    results.push(Err(ValidationError::InvalidCondition {
281                                        operator: self.operator.as_str().to_string(),
282                                        key: self.key.clone(),
283                                        reason: format!("Numeric operator requires numeric value, found non-numeric string: {}", s),
284                                    }));
285                                }
286                            },
287                            serde_json::Value::Array(arr) => {
288                                for (i, val) in arr.iter().enumerate() {
289                                    match val {
290                                        serde_json::Value::Number(_) => {},
291                                        serde_json::Value::String(s) => {
292                                            if s.parse::<f64>().is_err() {
293                                                results.push(Err(ValidationError::InvalidCondition {
294                                                    operator: self.operator.as_str().to_string(),
295                                                    key: self.key.clone(),
296                                                    reason: format!("Numeric operator requires numeric values, found non-numeric string at index {}: {}", i, s),
297                                                }));
298                                            }
299                                        },
300                                        _ => {
301                                            results.push(Err(ValidationError::InvalidCondition {
302                                                operator: self.operator.as_str().to_string(),
303                                                key: self.key.clone(),
304                                                reason: format!("Numeric operator requires numeric values, found {} at index {}", val, i),
305                                            }));
306                                        }
307                                    }
308                                }
309                            },
310                            _ => {
311                                results.push(Err(ValidationError::InvalidCondition {
312                                    operator: self.operator.as_str().to_string(),
313                                    key: self.key.clone(),
314                                    reason: "Numeric operator requires numeric value(s)".to_string(),
315                                }));
316                            }
317                        }
318                    },
319                    "Date" => {
320                        // Date operators should have valid date strings
321                        match &self.value {
322                            serde_json::Value::String(s) => {
323                                // Basic ISO 8601 format check
324                                if !s.contains('T') && !s.contains('-') {
325                                    results.push(Err(ValidationError::InvalidCondition {
326                                        operator: self.operator.as_str().to_string(),
327                                        key: self.key.clone(),
328                                        reason: format!("Date operator requires ISO 8601 date format, found: {}", s),
329                                    }));
330                                }
331                            },
332                            _ => {
333                                results.push(Err(ValidationError::InvalidCondition {
334                                    operator: self.operator.as_str().to_string(),
335                                    key: self.key.clone(),
336                                    reason: "Date operator requires string date value".to_string(),
337                                }));
338                            }
339                        }
340                    },
341                    "Boolean" => {
342                        // Boolean operators should have boolean values
343                        match &self.value {
344                            serde_json::Value::Bool(_) => {},
345                            serde_json::Value::String(s) => {
346                                if !matches!(s.as_str(), "true" | "false") {
347                                    results.push(Err(ValidationError::InvalidCondition {
348                                        operator: self.operator.as_str().to_string(),
349                                        key: self.key.clone(),
350                                        reason: format!("Boolean operator requires boolean value, found: {}", s),
351                                    }));
352                                }
353                            },
354                            _ => {
355                                results.push(Err(ValidationError::InvalidCondition {
356                                    operator: self.operator.as_str().to_string(),
357                                    key: self.key.clone(),
358                                    reason: "Boolean operator requires boolean value".to_string(),
359                                }));
360                            }
361                        }
362                    },
363                    _ => {} // Other categories are more flexible
364                }
365
366            helpers::collect_errors(results)
367        })
368    }
369}
370
371impl Validate for ConditionBlock {
372    fn validate(&self, context: &mut ValidationContext) -> ValidationResult {
373        context.with_segment("ConditionBlock", |ctx| {
374            if self.conditions.is_empty() {
375                return Err(ValidationError::InvalidValue {
376                    field: "Condition".to_string(),
377                    value: "{}".to_string(),
378                    reason: "Condition block cannot be empty".to_string(),
379                });
380            }
381
382            let mut results = Vec::new();
383
384            for (operator, condition_map) in &self.conditions {
385                ctx.with_segment(&operator.as_str(), |op_ctx| {
386                    if condition_map.is_empty() {
387                        results.push(Err(ValidationError::InvalidValue {
388                            field: "Condition operator".to_string(),
389                            value: operator.as_str().to_string(),
390                            reason: "Condition operator cannot have empty condition map".to_string(),
391                        }));
392                        return;
393                    }
394
395                    for (key, value) in condition_map {
396                        op_ctx.with_segment(key, |key_ctx| {
397                            let condition = Condition {
398                                operator: operator.clone(),
399                                key: key.clone(),
400                                value: value.clone(),
401                            };
402                            results.push(condition.validate(key_ctx));
403                        });
404                    }
405                });
406            }
407
408            helpers::collect_errors(results)
409        })
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416    use serde_json::json;
417
418    #[test]
419    fn test_condition_creation() {
420        let condition = Condition::string(Operator::StringEquals, "aws:username", "john");
421
422        assert_eq!(condition.operator, Operator::StringEquals);
423        assert_eq!(condition.key, "aws:username");
424        assert_eq!(condition.value, json!("john"));
425    }
426
427    #[test]
428    fn test_condition_block() {
429        let block = ConditionBlock::new()
430            .with_condition(Condition::string(
431                Operator::StringEquals,
432                "aws:username",
433                "john",
434            ))
435            .with_condition(Condition::boolean(
436                Operator::Bool,
437                "aws:SecureTransport",
438                true,
439            ));
440
441        assert!(block.has_condition(&Operator::StringEquals, "aws:username"));
442        assert!(block.has_condition(&Operator::Bool, "aws:SecureTransport"));
443        assert!(!block.has_condition(&Operator::StringEquals, "nonexistent"));
444
445        let username = block.get_condition_value(&Operator::StringEquals, "aws:username");
446        assert_eq!(username, Some(&json!("john")));
447    }
448
449    #[test]
450    fn test_legacy_format_conversion() {
451        let mut legacy = HashMap::new();
452        let mut string_conditions = HashMap::new();
453        string_conditions.insert("aws:username".to_string(), json!("john"));
454        legacy.insert("StringEquals".to_string(), string_conditions);
455
456        let block = ConditionBlock::from_legacy_format(legacy.clone()).unwrap();
457        assert!(block.has_condition(&Operator::StringEquals, "aws:username"));
458
459        let converted_back = block.to_legacy_format();
460        assert_eq!(converted_back, legacy);
461    }
462
463    #[test]
464    fn test_condition_serialization() {
465        let condition = Condition::string(Operator::StringEquals, "aws:username", "john");
466
467        let json = serde_json::to_string(&condition).unwrap();
468        let deserialized: Condition = serde_json::from_str(&json).unwrap();
469
470        assert_eq!(condition, deserialized);
471    }
472
473    #[test]
474    fn test_condition_block_serialization() {
475        let block = ConditionBlock::new()
476            .with_condition(Condition::string_array(
477                Operator::StringEquals,
478                "aws:PrincipalTag/department",
479                vec!["finance".to_string(), "hr".to_string(), "legal".to_string()],
480            ))
481            .with_condition(Condition::string_array(
482                Operator::ArnLike,
483                "aws:PrincipalArn",
484                vec![
485                    "arn:aws:iam::222222222222:user/Ana".to_string(),
486                    "arn:aws:iam::222222222222:user/Mary".to_string(),
487                ],
488            ));
489
490        let json = serde_json::to_string_pretty(&block).unwrap();
491        println!("Current serialization:\n{}", json);
492
493        // Test that it can be deserialized back
494        let deserialized: ConditionBlock = serde_json::from_str(&json).unwrap();
495        assert_eq!(block, deserialized);
496    }
497}