scratchstack_aspen/principal/
specified.rs

1use {
2    super::AwsPrincipal,
3    crate::{display_json, from_str_json, serutil::StringLikeList},
4    derive_builder::Builder,
5    scratchstack_aws_principal::{Principal as PrincipalActor, PrincipalIdentity, PrincipalSource},
6    serde::{Deserialize, Serialize},
7};
8
9/// A non-wildcard principal statement in an Aspen policy.
10///
11/// SpecifiedPrincipal structs are immutable. To construct this programmatically, use [SpecifiedPrincipalBuilder].
12#[derive(Builder, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
13pub struct SpecifiedPrincipal {
14    /// AWS principals specified in the statement.
15    #[builder(setter(into, strip_option), default)]
16    #[serde(rename = "AWS", skip_serializing_if = "Option::is_none")]
17    aws: Option<StringLikeList<AwsPrincipal>>,
18
19    /// Canonical users specified in the statement.
20    #[builder(setter(into, strip_option), default)]
21    #[serde(rename = "CanonicalUser", skip_serializing_if = "Option::is_none")]
22    canonical_user: Option<StringLikeList<String>>,
23
24    /// Federated users specified in the statement.
25    #[builder(setter(into, strip_option), default)]
26    #[serde(rename = "Federated", skip_serializing_if = "Option::is_none")]
27    federated: Option<StringLikeList<String>>,
28
29    /// Services specified in the statement.
30    #[builder(setter(into, strip_option), default)]
31    #[serde(rename = "Service", skip_serializing_if = "Option::is_none")]
32    service: Option<StringLikeList<String>>,
33}
34
35display_json!(SpecifiedPrincipal);
36from_str_json!(SpecifiedPrincipal);
37
38impl SpecifiedPrincipal {
39    /// Create a [SpecifiedPrincipalBuilder] to programmatically construct a [SpecifiedPrincipal].
40    #[inline]
41    pub fn builder() -> SpecifiedPrincipalBuilder {
42        SpecifiedPrincipalBuilder::default()
43    }
44
45    /// Returns the AWS principals specified in the statement.
46    #[inline]
47    pub fn aws(&self) -> Option<&StringLikeList<AwsPrincipal>> {
48        self.aws.as_ref()
49    }
50
51    /// Returns the canonical users specified in the statement.
52    #[inline]
53    pub fn canonical_user(&self) -> Option<&StringLikeList<String>> {
54        self.canonical_user.as_ref()
55    }
56
57    /// Returns the federated users specified in the statement.
58    #[inline]
59    pub fn federated(&self) -> Option<&StringLikeList<String>> {
60        self.federated.as_ref()
61    }
62
63    /// Returns the services specified in the statement.
64    #[inline]
65    pub fn service(&self) -> Option<&StringLikeList<String>> {
66        self.service.as_ref()
67    }
68
69    /// Indicates whether this specified principal matches an identity from the [PrincipalActor].
70    pub fn matches(&self, actor: &PrincipalActor) -> bool {
71        for identity in actor.iter() {
72            let source = identity.source();
73            match source {
74                PrincipalSource::Aws => {
75                    if let Some(aws_ids) = self.aws() {
76                        for aws_id in aws_ids.iter() {
77                            if aws_id.matches(identity) {
78                                return true;
79                            }
80                        }
81                    }
82                }
83                PrincipalSource::CanonicalUser => {
84                    if let PrincipalIdentity::CanonicalUser(identity) = identity {
85                        if let Some(canonical_users) = self.canonical_user() {
86                            for canonical_user in canonical_users.iter() {
87                                if canonical_user == identity.canonical_user_id() {
88                                    return true;
89                                }
90                            }
91                        }
92                    }
93                }
94                PrincipalSource::Federated => {
95                    if let PrincipalIdentity::FederatedUser(identity) = identity {
96                        if let Some(federated) = self.federated() {
97                            for federated in federated.iter() {
98                                if federated == identity.user_name() {
99                                    return true;
100                                }
101                            }
102                        }
103                    }
104                }
105                PrincipalSource::Service => {
106                    if let PrincipalIdentity::Service(identity) = identity {
107                        if let Some(services) = self.service() {
108                            for service in services.iter() {
109                                if service == identity.global_dns_name().as_str()
110                                    || service == identity.regional_dns_name().as_str()
111                                {
112                                    return true;
113                                }
114                            }
115                        }
116                    }
117                }
118            }
119        }
120
121        false
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use {
128        super::SpecifiedPrincipal,
129        scratchstack_aws_principal::{
130            CanonicalUser, FederatedUser, Principal as PrincipalActor, PrincipalIdentity, Service, User,
131        },
132        std::str::FromStr,
133    };
134
135    #[test_log::test]
136    fn test_deserialize_basic1() {
137        let sp = SpecifiedPrincipal::from_str(
138            r#"
139            {
140                "AWS": ["123456789012", "arn:aws:iam::123456789012:user/dacut"],
141                "CanonicalUser": ["df22d4799ef444d6434c676951d8b390145f2fc5f9107140d0e4b733ad40516d"],
142                "Federated": ["dacut@kanga.org"],
143                "Service": ["ec2.amazonaws.com", "lambda.amazonaws.com"]
144            }
145        "#,
146        )
147        .unwrap();
148
149        assert!(sp.aws.is_some());
150        assert!(sp.canonical_user.is_some());
151        assert!(sp.federated.is_some());
152        assert!(sp.service.is_some());
153    }
154
155    #[test_log::test]
156    fn test_matches() {
157        let sp = SpecifiedPrincipal::from_str(
158            r#"
159            {
160                "AWS": ["arn:aws:iam::123456789012:user/dacut"],
161                "CanonicalUser": ["df22d4799ef444d6434c676951d8b390145f2fc5f9107140d0e4b733ad40516d"],
162                "Federated": ["dacut@kanga.org"],
163                "Service": ["ec2.amazonaws.com", "lambda.amazonaws.com"]
164            }
165        "#,
166        )
167        .unwrap();
168
169        let user = User::new("aws", "123456789012", "/", "dacut").unwrap();
170        let canonical_user =
171            CanonicalUser::new("df22d4799ef444d6434c676951d8b390145f2fc5f9107140d0e4b733ad40516d").unwrap();
172        let federated_user = FederatedUser::new("aws", "123456789012", "dacut@kanga.org").unwrap();
173        let lambda_regional = Service::new("lambda", Some("us-east-1".to_string()), "amazonaws.com").unwrap();
174
175        assert!(sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(user.clone())])));
176        assert!(sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(canonical_user.clone())])));
177        assert!(sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(federated_user.clone())])));
178        assert!(sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(lambda_regional.clone())])));
179
180        let user_wrong_partition = User::new("aws-us-gov", "123456789012", "/", "dacut").unwrap();
181        assert!(!sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(user_wrong_partition)])));
182
183        let wrong_canonical_user =
184            CanonicalUser::new("0000000000000000000000000000000000000000000000000000000000000000").unwrap();
185        assert!(!sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(wrong_canonical_user)])));
186
187        let wrong_federated_user = FederatedUser::new("aws", "123456789012", "evildoer@kanga.org").unwrap();
188        assert!(!sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(wrong_federated_user)])));
189
190        let wrong_service = Service::new("s3", None, "amazonaws.com").unwrap();
191        assert!(!sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(wrong_service)])));
192
193        let empty = SpecifiedPrincipal::builder().build().unwrap();
194        assert!(!empty.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(user)])));
195        assert!(!empty.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(canonical_user)])));
196        assert!(!empty.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(federated_user)])));
197        assert!(!empty.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(lambda_regional)])));
198    }
199}