iam_rs/core/
principal.rs

1use crate::validation::{Validate, ValidationContext, ValidationResult, helpers};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5/// Principal type for IAM policies
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub enum PrincipalType {
8    /// AWS principals (users, roles, root accounts)
9    #[serde(rename = "AWS")]
10    Aws,
11    /// Federated principals (SAML, OIDC providers)
12    #[serde(rename = "Federated")]
13    Federated,
14    /// AWS service principals
15    #[serde(rename = "Service")]
16    Service,
17    /// S3 canonical user principals
18    #[serde(rename = "CanonicalUser")]
19    CanonicalUser,
20}
21
22impl std::fmt::Display for PrincipalType {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            PrincipalType::Aws => write!(f, "AWS"),
26            PrincipalType::Federated => write!(f, "Federated"),
27            PrincipalType::Service => write!(f, "Service"),
28            PrincipalType::CanonicalUser => write!(f, "CanonicalUser"),
29        }
30    }
31}
32
33/// Represents a principal in an IAM policy
34///
35/// <principal_block> = ("Principal" | "NotPrincipal") : ("*" | <principal_map>)
36/// <principal_map> = { <principal_map_entry>, <principal_map_entry>, ... }
37/// <principal_map_entry> = ("AWS" | "Federated" | "Service" | "CanonicalUser") :
38///     [<principal_id_string>, <principal_id_string>, ...]
39///
40/// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(untagged)]
43pub enum Principal {
44    /// Wildcard principal (*)
45    Wildcard,
46    /// Principal with service mapping (e.g., {"AWS": "arn:aws:iam::123456789012:user/username"})
47    Mapped(HashMap<PrincipalType, serde_json::Value>),
48}
49
50impl Validate for Principal {
51    fn validate(&self, context: &mut ValidationContext) -> ValidationResult {
52        context.with_segment("Principal", |ctx| {
53            match self {
54                Principal::Wildcard => {
55                    // Wildcard is always valid
56                    Ok(())
57                }
58                Principal::Mapped(map) => {
59                    if map.is_empty() {
60                        return Err(crate::validation::ValidationError::InvalidValue {
61                            field: "Principal".to_string(),
62                            value: "{}".to_string(),
63                            reason: "Principal mapping cannot be empty".to_string(),
64                        });
65                    }
66
67                    let mut results = Vec::new();
68
69                    for (key, value) in map {
70                        // Principal type key is guaranteed to be valid since it's an enum
71                        ctx.with_segment(&key.to_string(), |nested_ctx| {
72                            // Validate the principal values
73                            match value {
74                                serde_json::Value::String(s) => {
75                                    results.push(helpers::validate_principal(s, nested_ctx));
76                                }
77                                serde_json::Value::Array(arr) => {
78                                    for (i, item) in arr.iter().enumerate() {
79                                        if let serde_json::Value::String(s) = item {
80                                            nested_ctx.with_segment(
81                                                &format!("[{}]", i),
82                                                |item_ctx| {
83                                                    results.push(helpers::validate_principal(
84                                                        s, item_ctx,
85                                                    ));
86                                                },
87                                            );
88                                        } else {
89                                            results.push(Err(
90                                                crate::validation::ValidationError::InvalidValue {
91                                                    field: "Principal value".to_string(),
92                                                    value: item.to_string(),
93                                                    reason: "Principal values must be strings"
94                                                        .to_string(),
95                                                },
96                                            ));
97                                        }
98                                    }
99                                }
100                                _ => {
101                                    results.push(Err(
102                                        crate::validation::ValidationError::InvalidValue {
103                                            field: "Principal value".to_string(),
104                                            value: value.to_string(),
105                                            reason:
106                                                "Principal value must be string or array of strings"
107                                                    .to_string(),
108                                        },
109                                    ));
110                                }
111                            }
112                        });
113                    }
114
115                    helpers::collect_errors(results)
116                }
117            }
118        })
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use serde_json::json;
126
127    #[test]
128    fn test_principal_validation() {
129        let mut valid_mapped = HashMap::new();
130        valid_mapped.insert(
131            PrincipalType::Aws,
132            json!("arn:aws:iam::123456789012:user/alice"),
133        );
134        let valid_mapped = Principal::Mapped(valid_mapped);
135        assert!(valid_mapped.is_valid());
136
137        let valid_wildcard = Principal::Wildcard;
138        assert!(valid_wildcard.is_valid());
139
140        let mut another_valid_mapped = HashMap::new();
141        another_valid_mapped.insert(
142            PrincipalType::Aws,
143            json!("arn:aws:iam::123456789012:user/alice"),
144        );
145        let another_valid_mapped = Principal::Mapped(another_valid_mapped);
146        assert!(another_valid_mapped.is_valid());
147
148        let mut invalid_mapped = HashMap::new();
149        invalid_mapped.insert(PrincipalType::Aws, json!("invalid-principal"));
150        let invalid_mapped = Principal::Mapped(invalid_mapped);
151        assert!(!invalid_mapped.is_valid());
152
153        let empty_mapped = Principal::Mapped(HashMap::new());
154        assert!(!empty_mapped.is_valid());
155    }
156
157    #[test]
158    fn test_principal_mapped_validation() {
159        // Valid service principal
160        let mut service_map = HashMap::new();
161        service_map.insert(PrincipalType::Service, json!("lambda.amazonaws.com"));
162        let service_principal = Principal::Mapped(service_map);
163        assert!(service_principal.is_valid());
164
165        // Test that we can no longer create invalid principal types
166        // (This is now impossible at compile time with the enum)
167
168        // Array of principals
169        let mut array_map = HashMap::new();
170        array_map.insert(
171            PrincipalType::Aws,
172            json!([
173                "arn:aws:iam::123456789012:user/alice",
174                "arn:aws:iam::123456789012:user/bob"
175            ]),
176        );
177        let array_principal = Principal::Mapped(array_map);
178        assert!(array_principal.is_valid());
179    }
180}