use crate::{
OperatorType,
core::IAMOperator,
validation::{Validate, ValidationContext, ValidationError, ValidationResult, helpers},
};
use serde::{Deserialize, Serialize, Serializer};
use std::collections::{BTreeMap, HashMap};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum ConditionValue {
Boolean(bool),
Number(i64),
String(String),
StringList(Vec<String>),
}
impl ConditionValue {
#[must_use]
pub fn is_string(&self) -> bool {
matches!(self, ConditionValue::String(_))
}
#[must_use]
pub fn is_boolean(&self) -> bool {
matches!(self, ConditionValue::Boolean(_))
}
#[must_use]
pub fn is_number(&self) -> bool {
matches!(self, ConditionValue::Number(_))
}
#[must_use]
pub fn is_string_list(&self) -> bool {
matches!(self, ConditionValue::StringList(_))
}
#[must_use]
pub fn is_array(&self) -> bool {
matches!(self, ConditionValue::StringList(_))
}
#[must_use]
pub fn len(&self) -> usize {
match self {
ConditionValue::StringList(list) => list.len(),
_ => 1,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
match self {
ConditionValue::StringList(list) => list.is_empty(),
_ => false,
}
}
#[must_use]
pub fn to_json_value(&self) -> serde_json::Value {
match self {
ConditionValue::Boolean(b) => serde_json::Value::Bool(*b),
ConditionValue::Number(n) => serde_json::Value::Number((*n).into()),
ConditionValue::String(s) => serde_json::Value::String(s.clone()),
ConditionValue::StringList(list) => serde_json::Value::Array(
list.iter()
.map(|s| serde_json::Value::String(s.clone()))
.collect(),
),
}
}
pub fn from_json_value(value: serde_json::Value) -> Result<Self, String> {
match value {
serde_json::Value::Bool(b) => Ok(ConditionValue::Boolean(b)),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(ConditionValue::Number(i))
} else {
Err(format!("Unsupported number format: {n}"))
}
}
serde_json::Value::String(s) => Ok(ConditionValue::String(s)),
serde_json::Value::Array(arr) => {
let mut strings = Vec::new();
for item in arr {
if let serde_json::Value::String(s) = item {
strings.push(s);
} else {
return Err(format!("Array must contain only strings, found: {item:?}"));
}
}
Ok(ConditionValue::StringList(strings))
}
_ => Err(format!("Unsupported JSON value type: {value:?}")),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct Condition {
pub operator: IAMOperator,
pub key: String,
pub value: ConditionValue,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ConditionBlock {
#[serde(flatten)]
pub conditions: HashMap<IAMOperator, HashMap<String, ConditionValue>>,
}
impl Serialize for ConditionBlock {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let ordered_map: BTreeMap<String, BTreeMap<String, &ConditionValue>> = self
.conditions
.iter()
.map(|(op, conditions)| {
let inner_ordered: BTreeMap<String, &ConditionValue> =
conditions.iter().map(|(k, v)| (k.clone(), v)).collect();
(op.as_str().to_string(), inner_ordered)
})
.collect();
ordered_map.serialize(serializer)
}
}
impl Condition {
pub fn new<K: Into<String>>(operator: IAMOperator, key: K, value: ConditionValue) -> Self {
Self {
operator,
key: key.into(),
value,
}
}
pub fn string<K: Into<String>, V: Into<String>>(
operator: IAMOperator,
key: K,
value: V,
) -> Self {
Self::new(operator, key, ConditionValue::String(value.into()))
}
pub fn boolean<K: Into<String>>(operator: IAMOperator, key: K, value: bool) -> Self {
Self::new(operator, key, ConditionValue::Boolean(value))
}
pub fn number<K: Into<String>>(operator: IAMOperator, key: K, value: i64) -> Self {
Self::new(operator, key, ConditionValue::Number(value))
}
pub fn string_array<K: Into<String>>(
operator: IAMOperator,
key: K,
values: Vec<String>,
) -> Self {
Self::new(operator, key, ConditionValue::StringList(values))
}
}
impl ConditionBlock {
#[must_use]
pub fn new() -> Self {
Self {
conditions: HashMap::new(),
}
}
pub fn add_condition(&mut self, condition: Condition) {
let operator_map = self.conditions.entry(condition.operator).or_default();
operator_map.insert(condition.key, condition.value);
}
#[must_use]
pub fn with_condition(mut self, condition: Condition) -> Self {
self.add_condition(condition);
self
}
#[must_use]
pub fn with_condition_direct<K: Into<String>>(
mut self,
operator: IAMOperator,
key: K,
value: ConditionValue,
) -> Self {
let condition = Condition::new(operator, key, value);
self.add_condition(condition);
self
}
#[must_use]
pub fn get_conditions_for_operator(
&self,
operator: &IAMOperator,
) -> Option<&HashMap<String, ConditionValue>> {
self.conditions.get(operator)
}
#[must_use]
pub fn get_condition_value(
&self,
operator: &IAMOperator,
key: &str,
) -> Option<&ConditionValue> {
self.conditions.get(operator)?.get(key)
}
#[must_use]
pub fn has_condition(&self, operator: &IAMOperator, key: &str) -> bool {
self.conditions
.get(operator)
.is_some_and(|map| map.contains_key(key))
}
#[must_use]
pub fn operators(&self) -> Vec<&IAMOperator> {
self.conditions.keys().collect()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.conditions.is_empty()
}
#[must_use]
pub fn to_legacy_format(&self) -> HashMap<String, HashMap<String, serde_json::Value>> {
self.conditions
.iter()
.map(|(op, conditions)| {
let json_conditions = conditions
.iter()
.map(|(k, v)| (k.clone(), v.to_json_value()))
.collect();
(op.as_str().to_string(), json_conditions)
})
.collect()
}
pub fn from_legacy_format(
legacy: HashMap<String, HashMap<String, serde_json::Value>>,
) -> Result<Self, String> {
let mut conditions = HashMap::new();
for (op_str, condition_map) in legacy {
let operator = op_str
.parse::<IAMOperator>()
.map_err(|e| format!("Invalid operator '{op_str}': {e}"))?;
let mut converted_conditions = HashMap::new();
for (key, value) in condition_map {
let condition_value = ConditionValue::from_json_value(value)
.map_err(|e| format!("Invalid condition value for key '{key}': {e}"))?;
converted_conditions.insert(key, condition_value);
}
conditions.insert(operator, converted_conditions);
}
Ok(Self { conditions })
}
}
impl Default for ConditionBlock {
fn default() -> Self {
Self::new()
}
}
impl Validate for Condition {
#[allow(clippy::too_many_lines)]
fn validate(&self, context: &mut ValidationContext) -> ValidationResult {
context.with_segment("Condition", |ctx| {
let mut results = Vec::new();
results.push(helpers::validate_non_empty(&self.key, "key", ctx));
#[allow(clippy::single_match)]
match &self.value {
ConditionValue::StringList(arr) => {
if arr.is_empty() {
results.push(Err(ValidationError::InvalidCondition {
operator: self.operator.as_str().to_string(),
key: self.key.clone(),
reason: "Condition value array cannot be empty".to_string(),
}));
}
if !self.operator.supports_multiple_values() && arr.len() > 1 {
results.push(Err(ValidationError::InvalidCondition {
operator: self.operator.as_str().to_string(),
key: self.key.clone(),
reason: format!("Operator {} does not support multiple values", self.operator.as_str()),
}));
}
}
_ => {} }
match self.operator.category() {
OperatorType::String => {
match &self.value {
ConditionValue::String(_) => {},
ConditionValue::StringList(arr) => {
if arr.is_empty() {
results.push(Err(ValidationError::InvalidCondition {
operator: self.operator.as_str().to_string(),
key: self.key.clone(),
reason: "String operator requires non-empty string array".to_string(),
}));
}
},
_ => {
results.push(Err(ValidationError::InvalidCondition {
operator: self.operator.as_str().to_string(),
key: self.key.clone(),
reason: "String operator requires string value(s)".to_string(),
}));
}
}
},
OperatorType::Numeric => {
#[allow(clippy::match_wildcard_for_single_variants)]
match &self.value {
ConditionValue::Number(_) => {},
ConditionValue::String(s) => {
if s.parse::<f64>().is_err() {
results.push(Err(ValidationError::InvalidCondition {
operator: self.operator.as_str().to_string(),
key: self.key.clone(),
reason: format!("Numeric operator requires numeric value, found non-numeric string: {s}"),
}));
}
},
ConditionValue::StringList(arr) => {
for (i, s) in arr.iter().enumerate() {
if s.parse::<f64>().is_err() {
results.push(Err(ValidationError::InvalidCondition {
operator: self.operator.as_str().to_string(),
key: self.key.clone(),
reason: format!("Numeric operator requires numeric values, found non-numeric string at index {i}: {s}"),
}));
}
}
},
_ => {
results.push(Err(ValidationError::InvalidCondition {
operator: self.operator.as_str().to_string(),
key: self.key.clone(),
reason: "Numeric operator requires numeric value(s)".to_string(),
}));
}
}
},
OperatorType::Date => {
match &self.value {
ConditionValue::String(s) => {
if !s.contains('T') && !s.contains('-') {
results.push(Err(ValidationError::InvalidCondition {
operator: self.operator.as_str().to_string(),
key: self.key.clone(),
reason: format!("Date operator requires ISO 8601 date format, found: {s}"),
}));
}
},
_ => {
results.push(Err(ValidationError::InvalidCondition {
operator: self.operator.as_str().to_string(),
key: self.key.clone(),
reason: "Date operator requires string date value".to_string(),
}));
}
}
},
OperatorType::Boolean => {
match &self.value {
ConditionValue::Boolean(_) => {},
ConditionValue::String(s) => {
if !matches!(s.as_str(), "true" | "false") {
results.push(Err(ValidationError::InvalidCondition {
operator: self.operator.as_str().to_string(),
key: self.key.clone(),
reason: format!("Boolean operator requires boolean value, found: {s}"),
}));
}
},
_ => {
results.push(Err(ValidationError::InvalidCondition {
operator: self.operator.as_str().to_string(),
key: self.key.clone(),
reason: "Boolean operator requires boolean value".to_string(),
}));
}
}
},
_ => {} }
helpers::collect_errors(results)
})
}
}
impl Validate for ConditionBlock {
fn validate(&self, context: &mut ValidationContext) -> ValidationResult {
context.with_segment("ConditionBlock", |ctx| {
if self.conditions.is_empty() {
return Err(ValidationError::InvalidValue {
field: "Condition".to_string(),
value: "{}".to_string(),
reason: "Condition block cannot be empty".to_string(),
});
}
let mut results = Vec::new();
for (operator, condition_map) in &self.conditions {
ctx.with_segment(operator.as_str(), |op_ctx| {
if condition_map.is_empty() {
results.push(Err(ValidationError::InvalidValue {
field: "Condition operator".to_string(),
value: operator.as_str().to_string(),
reason: "Condition operator cannot have empty condition map"
.to_string(),
}));
return;
}
for (key, value) in condition_map {
op_ctx.with_segment(key, |key_ctx| {
let condition = Condition {
operator: operator.clone(),
key: key.clone(),
value: value.clone(),
};
results.push(condition.validate(key_ctx));
});
}
});
}
helpers::collect_errors(results)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_condition_creation() {
let condition = Condition::string(IAMOperator::StringEquals, "aws:username", "john");
assert_eq!(condition.operator, IAMOperator::StringEquals);
assert_eq!(condition.key, "aws:username");
assert_eq!(condition.value, ConditionValue::String("john".to_string()));
}
#[test]
fn test_condition_block() {
let block = ConditionBlock::new()
.with_condition(Condition::string(
IAMOperator::StringEquals,
"aws:username",
"john",
))
.with_condition(Condition::boolean(
IAMOperator::Bool,
"aws:SecureTransport",
true,
));
assert!(block.has_condition(&IAMOperator::StringEquals, "aws:username"));
assert!(block.has_condition(&IAMOperator::Bool, "aws:SecureTransport"));
assert!(!block.has_condition(&IAMOperator::StringEquals, "nonexistent"));
let username = block.get_condition_value(&IAMOperator::StringEquals, "aws:username");
assert_eq!(username, Some(&ConditionValue::String("john".to_string())));
}
#[test]
fn test_legacy_format_conversion() {
let mut legacy = HashMap::new();
let mut string_conditions = HashMap::new();
string_conditions.insert("aws:username".to_string(), serde_json::json!("john"));
legacy.insert("StringEquals".to_string(), string_conditions);
let block = ConditionBlock::from_legacy_format(legacy.clone()).unwrap();
assert!(block.has_condition(&IAMOperator::StringEquals, "aws:username"));
let converted_back = block.to_legacy_format();
assert_eq!(converted_back, legacy);
}
#[test]
fn test_condition_serialization() {
let condition = Condition::string(IAMOperator::StringEquals, "aws:username", "john");
let json = serde_json::to_string(&condition).unwrap();
let deserialized: Condition = serde_json::from_str(&json).unwrap();
assert_eq!(condition, deserialized);
}
#[test]
fn test_condition_block_serialization() {
let block = ConditionBlock::new()
.with_condition(Condition::string_array(
IAMOperator::StringEquals,
"aws:PrincipalTag/department",
vec!["finance".to_string(), "hr".to_string(), "legal".to_string()],
))
.with_condition(Condition::string_array(
IAMOperator::ArnLike,
"aws:PrincipalArn",
vec![
"arn:aws:iam::222222222222:user/Ana".to_string(),
"arn:aws:iam::222222222222:user/Mary".to_string(),
],
));
let json = serde_json::to_string_pretty(&block).unwrap();
println!("Current serialization:\n{json}");
let deserialized: ConditionBlock = serde_json::from_str(&json).unwrap();
assert_eq!(block, deserialized);
}
}