scratchstack_aspen/principal/
aws.rs1use {
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#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum AwsPrincipal {
22 Any,
24
25 Account(String),
27
28 Arn(Arn),
30}
31
32impl AwsPrincipal {
33 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 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}