use crate::{
Arn, ValidationError,
validation::{Validate, ValidationContext, ValidationResult},
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum PrincipalId {
String(String),
Array(Vec<String>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum Principal {
#[serde(rename = "AWS")]
Aws(PrincipalId),
#[serde(rename = "Federated")]
Federated(PrincipalId),
#[serde(rename = "Service")]
Service(PrincipalId),
#[serde(rename = "CanonicalUser")]
CanonicalUser(PrincipalId),
#[serde(rename = "*")]
Wildcard,
}
impl Principal {
#[must_use]
pub fn is_single(&self) -> bool {
#[allow(clippy::match_like_matches_macro)]
match self {
Principal::Aws(PrincipalId::String(_))
| Principal::Federated(PrincipalId::String(_))
| Principal::Service(PrincipalId::String(_))
| Principal::CanonicalUser(PrincipalId::String(_)) => true,
_ => false,
}
}
}
fn validate_domain(domain: &str) -> ValidationResult {
if domain.is_empty() || !domain.contains('.') || !domain.ends_with(|c: char| c.is_alphabetic())
{
return Err(ValidationError::InvalidPrincipal {
principal: domain.to_string(),
reason: "Principal must be a valid domain".to_string(),
});
}
Ok(())
}
impl Validate for Principal {
fn validate(&self, context: &mut ValidationContext) -> ValidationResult {
context.with_segment("Principal", |ctx| match self {
Principal::Wildcard => Ok(()),
Principal::Aws(PrincipalId::Array(ids))
| Principal::Federated(PrincipalId::Array(ids))
| Principal::Service(PrincipalId::Array(ids))
| Principal::CanonicalUser(PrincipalId::Array(ids)) => {
if ids.is_empty() {
return Err(ValidationError::InvalidPrincipal {
principal: "Empty principal array".to_string(),
reason: "Principal array cannot be empty".to_string(),
});
}
for id in ids {
let single = match self {
Principal::Aws(_) => Principal::Aws(PrincipalId::String(id.clone())),
Principal::Federated(_) => {
Principal::Federated(PrincipalId::String(id.clone()))
}
Principal::Service(_) => {
Principal::Service(PrincipalId::String(id.clone()))
}
Principal::CanonicalUser(_) => {
Principal::CanonicalUser(PrincipalId::String(id.clone()))
}
Principal::Wildcard => unreachable!(),
};
single.validate(ctx)?;
}
Ok(())
}
Principal::Aws(PrincipalId::String(id)) => {
if id.len() == 12 && id.chars().all(|c| c.is_ascii_digit()) {
return Ok(());
}
let arn = Arn::parse(id).map_err(|e| ValidationError::InvalidPrincipal {
principal: id.clone(),
reason: e.to_string(),
})?;
arn.validate(ctx)
}
Principal::Federated(PrincipalId::String(id)) => {
if id.starts_with("arn:") {
let arn = Arn::parse(id).map_err(|e| ValidationError::InvalidPrincipal {
principal: id.clone(),
reason: e.to_string(),
})?;
arn.validate(ctx)?;
} else {
validate_domain(id)?;
}
Ok(())
}
Principal::Service(PrincipalId::String(id)) => Ok(validate_domain(id)?),
Principal::CanonicalUser(PrincipalId::String(id)) => {
if id.len() == 64 && id.chars().all(|c| c.is_ascii_hexdigit()) {
return Ok(());
}
Err(ValidationError::InvalidPrincipal {
principal: id.clone(),
reason: "Canonical user ID must be a 64-character hex string".to_string(),
})
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_principal_validation() {
assert!(
Principal::Aws(PrincipalId::String(
"arn:aws:iam::123456789012:user/alice".into()
))
.is_valid()
);
assert!(!Principal::Aws(PrincipalId::String("invalid-principal".into())).is_valid());
assert!(!Principal::Aws(PrincipalId::String(String::new())).is_valid());
assert!(!Principal::Aws(PrincipalId::Array(vec![])).is_valid());
let valid_array_principal = Principal::Aws(PrincipalId::Array(vec![
"arn:aws:iam::123456789012:user/alice".into(),
"arn:aws:iam::123456789012:user/bob".into(),
]));
assert!(valid_array_principal.is_valid());
assert!(Principal::Aws(PrincipalId::String("123456789012".into())).is_valid());
assert!(Principal::Service(PrincipalId::String("ec2.amazonaws.com".into())).is_valid());
assert!(!Principal::Service(PrincipalId::String("invalid-service".into())).is_valid());
assert!(
Principal::Federated(PrincipalId::String(
"arn:aws:iam::123456789012:saml-provider/MyProvider".into()
))
.is_valid()
);
assert!(!Principal::Federated(PrincipalId::String("invalid-federated".into())).is_valid());
assert!(Principal::Federated(PrincipalId::String("example.com".into())).is_valid());
assert!(
Principal::CanonicalUser(PrincipalId::String(
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".into()
))
.is_valid()
);
assert!(
!Principal::CanonicalUser(PrincipalId::String("invalid-canonical".into())).is_valid()
);
}
}