1use crate::{
2 OperatorType,
3 core::IAMOperator,
4 evaluation::parse_date,
5 validation::{Validate, ValidationContext, ValidationError, ValidationResult, helpers},
6};
7use serde::{Deserialize, Serialize, Serializer};
8use std::collections::{BTreeMap, HashMap};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(untagged)]
13#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
14pub enum ConditionValue {
15 Boolean(bool),
17 #[cfg_attr(feature = "utoipa", schema(value_type = f64))]
19 Number(serde_json::Number),
20 String(String),
22 StringList(Vec<String>),
24}
25
26impl ConditionValue {
27 #[must_use]
29 pub fn is_string(&self) -> bool {
30 matches!(self, ConditionValue::String(_))
31 }
32
33 #[must_use]
35 pub fn is_boolean(&self) -> bool {
36 matches!(self, ConditionValue::Boolean(_))
37 }
38
39 #[must_use]
41 pub fn is_number(&self) -> bool {
42 matches!(self, ConditionValue::Number(_))
43 }
44
45 #[must_use]
47 pub fn is_string_list(&self) -> bool {
48 matches!(self, ConditionValue::StringList(_))
49 }
50
51 #[must_use]
53 pub fn is_array(&self) -> bool {
54 matches!(self, ConditionValue::StringList(_))
55 }
56
57 #[must_use]
59 pub fn len(&self) -> usize {
60 match self {
61 ConditionValue::StringList(list) => list.len(),
62 _ => 1,
63 }
64 }
65
66 #[must_use]
68 pub fn is_empty(&self) -> bool {
69 match self {
70 ConditionValue::StringList(list) => list.is_empty(),
71 _ => false,
72 }
73 }
74
75 #[must_use]
77 pub fn to_json_value(&self) -> serde_json::Value {
78 match self {
79 ConditionValue::Boolean(b) => serde_json::Value::Bool(*b),
80 ConditionValue::Number(n) => serde_json::Value::Number(n.clone()),
81 ConditionValue::String(s) => serde_json::Value::String(s.clone()),
82 ConditionValue::StringList(list) => serde_json::Value::Array(
83 list.iter()
84 .map(|s| serde_json::Value::String(s.clone()))
85 .collect(),
86 ),
87 }
88 }
89
90 pub fn from_json_value(value: serde_json::Value) -> Result<Self, String> {
92 match value {
93 serde_json::Value::Bool(b) => Ok(ConditionValue::Boolean(b)),
94 serde_json::Value::Number(n) => Ok(ConditionValue::Number(n)),
95 serde_json::Value::String(s) => Ok(ConditionValue::String(s)),
96 serde_json::Value::Array(arr) => {
97 let mut strings = Vec::new();
98 for item in arr {
99 if let serde_json::Value::String(s) = item {
100 strings.push(s);
101 } else {
102 return Err(format!("Array must contain only strings, found: {item:?}"));
103 }
104 }
105 Ok(ConditionValue::StringList(strings))
106 }
107 _ => Err(format!("Unsupported JSON value type: {value:?}")),
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
115pub struct Condition {
116 pub operator: IAMOperator,
118 pub key: String,
120 pub value: ConditionValue,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
127#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
128pub struct ConditionBlock {
129 #[serde(flatten)]
131 pub conditions: HashMap<IAMOperator, HashMap<String, ConditionValue>>,
132}
133
134impl Serialize for ConditionBlock {
135 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
136 where
137 S: Serializer,
138 {
139 let ordered_map: BTreeMap<String, BTreeMap<String, &ConditionValue>> = self
142 .conditions
143 .iter()
144 .map(|(op, conditions)| {
145 let inner_ordered: BTreeMap<String, &ConditionValue> =
146 conditions.iter().map(|(k, v)| (k.clone(), v)).collect();
147 (op.as_str().to_string(), inner_ordered)
148 })
149 .collect();
150
151 ordered_map.serialize(serializer)
152 }
153}
154
155impl Condition {
156 pub fn new<K: Into<String>>(operator: IAMOperator, key: K, value: ConditionValue) -> Self {
158 Self {
159 operator,
160 key: key.into(),
161 value,
162 }
163 }
164
165 pub fn string<K: Into<String>, V: Into<String>>(
167 operator: IAMOperator,
168 key: K,
169 value: V,
170 ) -> Self {
171 Self::new(operator, key, ConditionValue::String(value.into()))
172 }
173
174 pub fn boolean<K: Into<String>>(operator: IAMOperator, key: K, value: bool) -> Self {
176 Self::new(operator, key, ConditionValue::Boolean(value))
177 }
178
179 pub fn number<K: Into<String>>(operator: IAMOperator, key: K, value: i64) -> Self {
181 Self::new(operator, key, ConditionValue::Number(value.into()))
182 }
183
184 pub fn float<K: Into<String>>(operator: IAMOperator, key: K, value: f64) -> Self {
190 let number = serde_json::Number::from_f64(value)
191 .expect("float value must be finite (not NaN or infinity)");
192 Self::new(operator, key, ConditionValue::Number(number))
193 }
194
195 pub fn string_array<K: Into<String>>(
197 operator: IAMOperator,
198 key: K,
199 values: Vec<String>,
200 ) -> Self {
201 Self::new(operator, key, ConditionValue::StringList(values))
202 }
203}
204
205impl ConditionBlock {
206 #[must_use]
208 pub fn new() -> Self {
209 Self {
210 conditions: HashMap::new(),
211 }
212 }
213
214 pub fn add_condition(&mut self, condition: Condition) {
216 let operator_map = self.conditions.entry(condition.operator).or_default();
217 operator_map.insert(condition.key, condition.value);
218 }
219
220 #[must_use]
222 pub fn with_condition(mut self, condition: Condition) -> Self {
223 self.add_condition(condition);
224 self
225 }
226
227 #[must_use]
229 pub fn with_condition_direct<K: Into<String>>(
230 mut self,
231 operator: IAMOperator,
232 key: K,
233 value: ConditionValue,
234 ) -> Self {
235 let condition = Condition::new(operator, key, value);
236 self.add_condition(condition);
237 self
238 }
239
240 #[must_use]
242 pub fn get_conditions_for_operator(
243 &self,
244 operator: &IAMOperator,
245 ) -> Option<&HashMap<String, ConditionValue>> {
246 self.conditions.get(operator)
247 }
248
249 #[must_use]
251 pub fn get_condition_value(
252 &self,
253 operator: &IAMOperator,
254 key: &str,
255 ) -> Option<&ConditionValue> {
256 self.conditions.get(operator)?.get(key)
257 }
258
259 #[must_use]
261 pub fn has_condition(&self, operator: &IAMOperator, key: &str) -> bool {
262 self.conditions
263 .get(operator)
264 .is_some_and(|map| map.contains_key(key))
265 }
266
267 #[must_use]
269 pub fn operators(&self) -> Vec<&IAMOperator> {
270 self.conditions.keys().collect()
271 }
272
273 #[must_use]
275 pub fn is_empty(&self) -> bool {
276 self.conditions.is_empty()
277 }
278
279 #[must_use]
281 pub fn to_legacy_format(&self) -> HashMap<String, HashMap<String, serde_json::Value>> {
282 self.conditions
283 .iter()
284 .map(|(op, conditions)| {
285 let json_conditions = conditions
286 .iter()
287 .map(|(k, v)| (k.clone(), v.to_json_value()))
288 .collect();
289 (op.as_str().to_string(), json_conditions)
290 })
291 .collect()
292 }
293
294 pub fn from_legacy_format(
300 legacy: HashMap<String, HashMap<String, serde_json::Value>>,
301 ) -> Result<Self, String> {
302 let mut conditions = HashMap::new();
303
304 for (op_str, condition_map) in legacy {
305 let operator = op_str
306 .parse::<IAMOperator>()
307 .map_err(|e| format!("Invalid operator '{op_str}': {e}"))?;
308
309 let mut converted_conditions = HashMap::new();
310 for (key, value) in condition_map {
311 let condition_value = ConditionValue::from_json_value(value)
312 .map_err(|e| format!("Invalid condition value for key '{key}': {e}"))?;
313 converted_conditions.insert(key, condition_value);
314 }
315
316 conditions.insert(operator, converted_conditions);
317 }
318
319 Ok(Self { conditions })
320 }
321}
322
323impl Default for ConditionBlock {
324 fn default() -> Self {
325 Self::new()
326 }
327}
328
329impl Validate for Condition {
330 #[allow(clippy::too_many_lines)]
331 fn validate(&self, context: &mut ValidationContext) -> ValidationResult {
332 context.with_segment("Condition", |ctx| {
333 let mut results = Vec::new();
334
335 results.push(helpers::validate_non_empty(&self.key, "key", ctx));
337
338 #[allow(clippy::single_match)]
340 match &self.value {
341 ConditionValue::StringList(arr) => {
342 if arr.is_empty() {
343 results.push(Err(ValidationError::InvalidCondition {
344 operator: self.operator.as_str().to_string(),
345 key: self.key.clone(),
346 reason: "Condition value array cannot be empty".to_string(),
347 }));
348 }
349
350 if !self.operator.supports_multiple_values() && arr.len() > 1 {
352 results.push(Err(ValidationError::InvalidCondition {
353 operator: self.operator.as_str().to_string(),
354 key: self.key.clone(),
355 reason: format!("Operator {} does not support multiple values", self.operator.as_str()),
356 }));
357 }
358 }
359 _ => {} }
361
362 match self.operator.category() {
364 OperatorType::String => {
365 match &self.value {
367 ConditionValue::String(_) => {},
368 ConditionValue::StringList(arr) => {
369 if arr.is_empty() {
370 results.push(Err(ValidationError::InvalidCondition {
371 operator: self.operator.as_str().to_string(),
372 key: self.key.clone(),
373 reason: "String operator requires non-empty string array".to_string(),
374 }));
375 }
376 },
377 _ => {
378 results.push(Err(ValidationError::InvalidCondition {
379 operator: self.operator.as_str().to_string(),
380 key: self.key.clone(),
381 reason: "String operator requires string value(s)".to_string(),
382 }));
383 }
384 }
385 },
386 OperatorType::Numeric => {
387 #[allow(clippy::match_wildcard_for_single_variants)]
389 match &self.value {
390 ConditionValue::Number(_) => {},
391 ConditionValue::String(s) => {
392 if s.parse::<f64>().is_err() {
394 results.push(Err(ValidationError::InvalidCondition {
395 operator: self.operator.as_str().to_string(),
396 key: self.key.clone(),
397 reason: format!("Numeric operator requires numeric value, found non-numeric string: {s}"),
398 }));
399 }
400 },
401 ConditionValue::StringList(arr) => {
402 for (i, s) in arr.iter().enumerate() {
403 if s.parse::<f64>().is_err() {
404 results.push(Err(ValidationError::InvalidCondition {
405 operator: self.operator.as_str().to_string(),
406 key: self.key.clone(),
407 reason: format!("Numeric operator requires numeric values, found non-numeric string at index {i}: {s}"),
408 }));
409 }
410 }
411 },
412 _ => {
413 results.push(Err(ValidationError::InvalidCondition {
414 operator: self.operator.as_str().to_string(),
415 key: self.key.clone(),
416 reason: "Numeric operator requires numeric value(s)".to_string(),
417 }));
418 }
419 }
420 },
421 OperatorType::Date => {
422 match &self.value {
424 ConditionValue::String(s) => {
425 if parse_date(s).is_err() {
429 results.push(Err(ValidationError::InvalidCondition {
430 operator: self.operator.as_str().to_string(),
431 key: self.key.clone(),
432 reason: format!("Date operator requires an ISO 8601 date/time or Unix epoch value, found: {s}"),
433 }));
434 }
435 },
436 ConditionValue::Number(n) => {
439 if parse_date(&n.to_string()).is_err() {
440 results.push(Err(ValidationError::InvalidCondition {
441 operator: self.operator.as_str().to_string(),
442 key: self.key.clone(),
443 reason: format!("Date operator requires an ISO 8601 date/time or Unix epoch value, found: {n}"),
444 }));
445 }
446 },
447 _ => {
448 results.push(Err(ValidationError::InvalidCondition {
449 operator: self.operator.as_str().to_string(),
450 key: self.key.clone(),
451 reason: "Date operator requires a string or number date value".to_string(),
452 }));
453 }
454 }
455 },
456 OperatorType::Boolean => {
457 match &self.value {
459 ConditionValue::Boolean(_) => {},
460 ConditionValue::String(s) => {
461 if !matches!(s.as_str(), "true" | "false") {
462 results.push(Err(ValidationError::InvalidCondition {
463 operator: self.operator.as_str().to_string(),
464 key: self.key.clone(),
465 reason: format!("Boolean operator requires boolean value, found: {s}"),
466 }));
467 }
468 },
469 _ => {
470 results.push(Err(ValidationError::InvalidCondition {
471 operator: self.operator.as_str().to_string(),
472 key: self.key.clone(),
473 reason: "Boolean operator requires boolean value".to_string(),
474 }));
475 }
476 }
477 },
478 _ => {} }
480
481 helpers::collect_errors(results)
482 })
483 }
484}
485
486impl Validate for ConditionBlock {
487 fn validate(&self, context: &mut ValidationContext) -> ValidationResult {
488 context.with_segment("ConditionBlock", |ctx| {
489 if self.conditions.is_empty() {
490 return Err(ValidationError::InvalidValue {
491 field: "Condition".to_string(),
492 value: "{}".to_string(),
493 reason: "Condition block cannot be empty".to_string(),
494 });
495 }
496
497 let mut results = Vec::new();
498
499 for (operator, condition_map) in &self.conditions {
500 ctx.with_segment(operator.as_str(), |op_ctx| {
501 if condition_map.is_empty() {
502 results.push(Err(ValidationError::InvalidValue {
503 field: "Condition operator".to_string(),
504 value: operator.as_str().to_string(),
505 reason: "Condition operator cannot have empty condition map"
506 .to_string(),
507 }));
508 return;
509 }
510
511 for (key, value) in condition_map {
512 op_ctx.with_segment(key, |key_ctx| {
513 let condition = Condition {
514 operator: operator.clone(),
515 key: key.clone(),
516 value: value.clone(),
517 };
518 results.push(condition.validate(key_ctx));
519 });
520 }
521 });
522 }
523
524 helpers::collect_errors(results)
525 })
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn test_condition_creation() {
535 let condition = Condition::string(IAMOperator::StringEquals, "aws:username", "john");
536
537 assert_eq!(condition.operator, IAMOperator::StringEquals);
538 assert_eq!(condition.key, "aws:username");
539 assert_eq!(condition.value, ConditionValue::String("john".to_string()));
540 }
541
542 #[test]
543 fn test_condition_block() {
544 let block = ConditionBlock::new()
545 .with_condition(Condition::string(
546 IAMOperator::StringEquals,
547 "aws:username",
548 "john",
549 ))
550 .with_condition(Condition::boolean(
551 IAMOperator::Bool,
552 "aws:SecureTransport",
553 true,
554 ));
555
556 assert!(block.has_condition(&IAMOperator::StringEquals, "aws:username"));
557 assert!(block.has_condition(&IAMOperator::Bool, "aws:SecureTransport"));
558 assert!(!block.has_condition(&IAMOperator::StringEquals, "nonexistent"));
559
560 let username = block.get_condition_value(&IAMOperator::StringEquals, "aws:username");
561 assert_eq!(username, Some(&ConditionValue::String("john".to_string())));
562 }
563
564 #[test]
565 fn test_legacy_format_conversion() {
566 let mut legacy = HashMap::new();
567 let mut string_conditions = HashMap::new();
568 string_conditions.insert("aws:username".to_string(), serde_json::json!("john"));
569 legacy.insert("StringEquals".to_string(), string_conditions);
570
571 let block = ConditionBlock::from_legacy_format(legacy.clone()).unwrap();
572 assert!(block.has_condition(&IAMOperator::StringEquals, "aws:username"));
573
574 let converted_back = block.to_legacy_format();
575 assert_eq!(converted_back, legacy);
576 }
577
578 #[test]
579 fn test_condition_serialization() {
580 let condition = Condition::string(IAMOperator::StringEquals, "aws:username", "john");
581
582 let json = serde_json::to_string(&condition).unwrap();
583 let deserialized: Condition = serde_json::from_str(&json).unwrap();
584
585 assert_eq!(condition, deserialized);
586 }
587
588 #[test]
589 fn test_condition_block_serialization() {
590 let block = ConditionBlock::new()
591 .with_condition(Condition::string_array(
592 IAMOperator::StringEquals,
593 "aws:PrincipalTag/department",
594 vec!["finance".to_string(), "hr".to_string(), "legal".to_string()],
595 ))
596 .with_condition(Condition::string_array(
597 IAMOperator::ArnLike,
598 "aws:PrincipalArn",
599 vec![
600 "arn:aws:iam::222222222222:user/Ana".to_string(),
601 "arn:aws:iam::222222222222:user/Mary".to_string(),
602 ],
603 ));
604
605 let json = serde_json::to_string_pretty(&block).unwrap();
606 println!("Current serialization:\n{json}");
607
608 let deserialized: ConditionBlock = serde_json::from_str(&json).unwrap();
610 assert_eq!(block, deserialized);
611 }
612
613 #[test]
614 fn test_condition_value_float_number() {
615 let int_value: ConditionValue = serde_json::from_str("10").unwrap();
617 assert_eq!(int_value, ConditionValue::Number(10.into()));
618 assert_eq!(serde_json::to_string(&int_value).unwrap(), "10");
619
620 let float_value: ConditionValue = serde_json::from_str("10.5").unwrap();
622 assert_eq!(
623 float_value,
624 ConditionValue::Number(serde_json::Number::from_f64(10.5).unwrap())
625 );
626 assert_eq!(serde_json::to_string(&float_value).unwrap(), "10.5");
627
628 let cv = ConditionValue::from_json_value(serde_json::json!(1234.5)).unwrap();
630 assert_eq!(cv.to_json_value(), serde_json::json!(1234.5));
631
632 let condition = Condition::float(
634 IAMOperator::NumericLessThan,
635 "aws:multifactorAuthAge",
636 1234.5,
637 );
638 assert!(condition.value.is_number());
639 }
640
641 #[test]
642 fn test_date_condition_validation() {
643 let iso = Condition::string(
647 IAMOperator::DateGreaterThan,
648 "aws:CurrentTime",
649 "2024-01-01T00:00:00Z",
650 );
651 assert!(iso.is_valid(), "ISO 8601 date should validate");
652
653 let epoch = Condition::string(
654 IAMOperator::DateGreaterThan,
655 "aws:CurrentTime",
656 "1704067200",
657 );
658 assert!(epoch.is_valid(), "Unix epoch should validate");
659
660 let fractional_epoch = Condition::string(
661 IAMOperator::DateGreaterThan,
662 "aws:CurrentTime",
663 "1704067200.5",
664 );
665 assert!(
666 fractional_epoch.is_valid(),
667 "fractional epoch should validate"
668 );
669
670 let numeric_epoch = Condition::new(
672 IAMOperator::DateGreaterThan,
673 "aws:CurrentTime",
674 ConditionValue::Number(serde_json::Number::from(1_704_067_200u64)),
675 );
676 assert!(
677 numeric_epoch.is_valid(),
678 "numeric Unix epoch should validate"
679 );
680
681 let invalid = Condition::string(
683 IAMOperator::DateGreaterThan,
684 "aws:CurrentTime",
685 "not-a-date",
686 );
687 assert!(!invalid.is_valid(), "non-date string should not validate");
688 }
689}