scratchstack_aspen/principal/
aws.rs

1use {
2    crate::AspenError,
3    lazy_static::lazy_static,
4    regex::Regex,
5    scratchstack_arn::Arn,
6    scratchstack_aws_principal::{PrincipalIdentity, PrincipalSource},
7    std::{
8        fmt::{Display, Formatter, Result as FmtResult},
9        str::FromStr,
10    },
11};
12
13lazy_static! {
14    static ref AWS_ACCOUNT_ID: Regex = Regex::new(r"^\d{12}$").unwrap();
15}
16
17/// An AWS account principal clause in an Aspen policy.
18///
19/// AwsPrincipal enums are immutable.
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum AwsPrincipal {
22    /// Any entity in any AWS account.
23    Any,
24
25    /// Any entity in the specified AWS account.
26    Account(String),
27
28    /// The entity specified by the given ARN.
29    Arn(Arn),
30}
31
32impl AwsPrincipal {
33    /// Indicate whether this [AwsPrincipal] matches the given [PrincipalIdentity].
34    pub fn matches(&self, identity: &PrincipalIdentity) -> bool {
35        if identity.source() != PrincipalSource::Aws {
36            return false;
37        }
38
39        match self {
40            Self::Any => true,
41            Self::Account(account_id) => {
42                let identity_arn: Arn = identity.try_into().expect("AWS principal identity must have an ARN");
43                identity_arn.account_id() == account_id
44            }
45            Self::Arn(arn) => {
46                let identity_arn: Arn = identity.try_into().expect("AWS principal identity must have an ARN");
47                match arn.resource() {
48                    "root" => {
49                        arn.partition() == identity_arn.partition()
50                            && arn.service() == identity_arn.service()
51                            && arn.region() == identity_arn.region()
52                            && arn.account_id() == identity_arn.account_id()
53                    }
54                    _ => arn == &identity_arn,
55                }
56            }
57        }
58    }
59}
60
61impl Display for AwsPrincipal {
62    fn fmt(&self, f: &mut Formatter) -> FmtResult {
63        match self {
64            Self::Account(account_id) => f.write_str(account_id),
65            Self::Any => f.write_str("*"),
66            Self::Arn(arn) => arn.fmt(f),
67        }
68    }
69}
70
71impl FromStr for AwsPrincipal {
72    type Err = AspenError;
73
74    fn from_str(s: &str) -> Result<Self, AspenError> {
75        if s == "*" {
76            Ok(Self::Any)
77        } else if AWS_ACCOUNT_ID.is_match(s) {
78            Ok(AwsPrincipal::Account(s.to_string()))
79        } else {
80            match Arn::from_str(s) {
81                Ok(arn) => Ok(AwsPrincipal::Arn(arn)),
82                Err(_) => Err(AspenError::InvalidPrincipal(s.to_string())),
83            }
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use {
91        crate::AwsPrincipal,
92        pretty_assertions::{assert_eq, assert_ne},
93        scratchstack_aws_principal::{CanonicalUser, PrincipalIdentity, Service, User},
94    };
95
96    #[allow(clippy::redundant_clone)]
97    #[test_log::test]
98    fn test_derived() {
99        // Just need to verify clone works as expected.
100        let ap1a = AwsPrincipal::Any;
101        let ap1b = AwsPrincipal::Any;
102        let ap2a = AwsPrincipal::Account("123456789012".to_string());
103        let ap2b = AwsPrincipal::Account("123456789012".to_string());
104        let ap3a = AwsPrincipal::Arn("arn:aws:iam::123456789012:root".parse().unwrap());
105        let ap3b = AwsPrincipal::Arn("arn:aws:iam::123456789012:root".parse().unwrap());
106
107        assert_eq!(ap1a, ap1b);
108        assert_eq!(ap2a, ap2b);
109        assert_eq!(ap3a, ap3b);
110        assert_ne!(ap1a, ap2a);
111        assert_ne!(ap1a, ap3a);
112        assert_ne!(ap2a, ap3a);
113
114        assert_eq!(ap1a.clone(), ap1a);
115        assert_eq!(ap2a.clone(), ap2a);
116        assert_eq!(ap3a.clone(), ap3a);
117    }
118
119    #[test_log::test]
120    fn test_matches() {
121        assert!(AwsPrincipal::Any
122            .matches(&PrincipalIdentity::from(User::new("aws", "123456789012", "/", "testuser").unwrap())));
123        assert!(AwsPrincipal::Account("123456789012".to_string())
124            .matches(&PrincipalIdentity::from(User::new("aws", "123456789012", "/", "testuser").unwrap())));
125        assert!(!AwsPrincipal::Account("567890123456".to_string())
126            .matches(&PrincipalIdentity::from(User::new("aws", "123456789012", "/", "testuser").unwrap())));
127        assert!(
128            !AwsPrincipal::Any.matches(&PrincipalIdentity::from(Service::new("iam", None, "amazonaws.com").unwrap()))
129        );
130        assert!(!AwsPrincipal::Account("123456789012".to_string())
131            .matches(&PrincipalIdentity::from(Service::new("iam", None, "amazonaws.com").unwrap())));
132        assert!(!AwsPrincipal::Any.matches(&PrincipalIdentity::from(
133            CanonicalUser::new("772183b840c93fe103e45cd24ca8b8c94425a373465c6eb535b7c4b9593811e5").unwrap()
134        )));
135
136        assert!(AwsPrincipal::Arn("arn:aws:iam::123456789012:root".parse().unwrap())
137            .matches(&PrincipalIdentity::from(User::new("aws", "123456789012", "/", "testuser").unwrap())));
138    }
139}