1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
use {
    crate::AspenError,
    lazy_static::lazy_static,
    regex::Regex,
    scratchstack_arn::Arn,
    scratchstack_aws_principal::{PrincipalIdentity, PrincipalSource},
    std::{
        fmt::{Display, Formatter, Result as FmtResult},
        str::FromStr,
    },
};

lazy_static! {
    static ref AWS_ACCOUNT_ID: Regex = Regex::new(r"^\d{12}$").unwrap();
}

/// An AWS account principal clause in an Aspen policy.
///
/// AwsPrincipal enums are immutable.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AwsPrincipal {
    /// Any entity in any AWS account.
    Any,

    /// Any entity in the specified AWS account.
    Account(String),

    /// The entity specified by the given ARN.
    Arn(Arn),
}

impl AwsPrincipal {
    /// Indicate whether this [AwsPrincipal] matches the given [PrincipalIdentity].
    pub fn matches(&self, identity: &PrincipalIdentity) -> bool {
        if identity.source() != PrincipalSource::Aws {
            return false;
        }

        match self {
            Self::Any => true,
            Self::Account(account_id) => {
                let identity_arn: Arn = identity.try_into().expect("AWS principal identity must have an ARN");
                identity_arn.account_id() == account_id
            }
            Self::Arn(arn) => {
                let identity_arn: Arn = identity.try_into().expect("AWS principal identity must have an ARN");
                match arn.resource() {
                    "root" => {
                        arn.partition() == identity_arn.partition()
                            && arn.service() == identity_arn.service()
                            && arn.region() == identity_arn.region()
                            && arn.account_id() == identity_arn.account_id()
                    }
                    _ => arn == &identity_arn,
                }
            }
        }
    }
}

impl Display for AwsPrincipal {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        match self {
            Self::Account(account_id) => f.write_str(account_id),
            Self::Any => f.write_str("*"),
            Self::Arn(arn) => arn.fmt(f),
        }
    }
}

impl FromStr for AwsPrincipal {
    type Err = AspenError;

    fn from_str(s: &str) -> Result<Self, AspenError> {
        if s == "*" {
            Ok(Self::Any)
        } else if AWS_ACCOUNT_ID.is_match(s) {
            Ok(AwsPrincipal::Account(s.to_string()))
        } else {
            match Arn::from_str(s) {
                Ok(arn) => Ok(AwsPrincipal::Arn(arn)),
                Err(_) => Err(AspenError::InvalidPrincipal(s.to_string())),
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use {
        crate::AwsPrincipal,
        pretty_assertions::{assert_eq, assert_ne},
        scratchstack_aws_principal::{CanonicalUser, PrincipalIdentity, Service, User},
    };

    #[test_log::test]
    fn test_derived() {
        // Just need to verify clone works as expected.
        let ap1a = AwsPrincipal::Any;
        let ap1b = AwsPrincipal::Any;
        let ap2a = AwsPrincipal::Account("123456789012".to_string());
        let ap2b = AwsPrincipal::Account("123456789012".to_string());
        let ap3a = AwsPrincipal::Arn("arn:aws:iam::123456789012:root".parse().unwrap());
        let ap3b = AwsPrincipal::Arn("arn:aws:iam::123456789012:root".parse().unwrap());

        assert_eq!(ap1a, ap1b);
        assert_eq!(ap2a, ap2b);
        assert_eq!(ap3a, ap3b);
        assert_ne!(ap1a, ap2a);
        assert_ne!(ap1a, ap3a);
        assert_ne!(ap2a, ap3a);

        assert_eq!(ap1a.clone(), ap1a);
        assert_eq!(ap2a.clone(), ap2a);
        assert_eq!(ap3a.clone(), ap3a);
    }

    #[test_log::test]
    fn test_matches() {
        assert!(AwsPrincipal::Any
            .matches(&PrincipalIdentity::from(User::new("aws", "123456789012", "/", "testuser").unwrap())));
        assert!(AwsPrincipal::Account("123456789012".to_string())
            .matches(&PrincipalIdentity::from(User::new("aws", "123456789012", "/", "testuser").unwrap())));
        assert!(!AwsPrincipal::Account("567890123456".to_string())
            .matches(&PrincipalIdentity::from(User::new("aws", "123456789012", "/", "testuser").unwrap())));
        assert!(
            !AwsPrincipal::Any.matches(&PrincipalIdentity::from(Service::new("iam", None, "amazonaws.com").unwrap()))
        );
        assert!(!AwsPrincipal::Account("123456789012".to_string())
            .matches(&PrincipalIdentity::from(Service::new("iam", None, "amazonaws.com").unwrap())));
        assert!(!AwsPrincipal::Any.matches(&PrincipalIdentity::from(
            CanonicalUser::new("772183b840c93fe103e45cd24ca8b8c94425a373465c6eb535b7c4b9593811e5").unwrap()
        )));

        assert!(AwsPrincipal::Arn("arn:aws:iam::123456789012:root".parse().unwrap())
            .matches(&PrincipalIdentity::from(User::new("aws", "123456789012", "/", "testuser").unwrap())));
    }
}