scratchstack_aspen/resource/
arn.rs1use {
2 crate::{eval::regex_from_glob, AspenError, Context, PolicyVersion},
3 scratchstack_arn::Arn,
4 std::{
5 fmt::{Display, Formatter, Result as FmtResult},
6 str::FromStr,
7 },
8};
9
10const PARTITION_START: usize = 4;
11
12#[derive(Debug, Clone, Eq, Hash, PartialEq)]
26pub struct ResourceArn {
27 arn: String,
28 service_start: usize,
29 region_start: usize,
30 account_id_start: usize,
31 resource_start: usize,
32}
33
34impl ResourceArn {
35 pub fn new(partition: &str, service: &str, region: &str, account_id: &str, resource: &str) -> Self {
43 let arn = format!("arn:{partition}:{service}:{region}:{account_id}:{resource}");
44 let service_start = PARTITION_START + partition.len() + 1;
45 let region_start = service_start + service.len() + 1;
46 let account_id_start = region_start + region.len() + 1;
47 let resource_start = account_id_start + account_id.len() + 1;
48
49 Self {
50 arn,
51 service_start,
52 region_start,
53 account_id_start,
54 resource_start,
55 }
56 }
57
58 #[inline]
60 pub fn partition_pattern(&self) -> &str {
61 &self.arn[PARTITION_START..self.service_start - 1]
62 }
63
64 #[inline]
66 pub fn service_pattern(&self) -> &str {
67 &self.arn[self.service_start..self.region_start - 1]
68 }
69
70 #[inline]
72 pub fn region_pattern(&self) -> &str {
73 &self.arn[self.region_start..self.account_id_start - 1]
74 }
75
76 #[inline]
78 pub fn account_id_pattern(&self) -> &str {
79 &self.arn[self.account_id_start..self.resource_start - 1]
80 }
81
82 #[inline]
84 pub fn resource_pattern(&self) -> &str {
85 &self.arn[self.resource_start..]
86 }
87
88 pub fn matches(&self, context: &Context, pv: PolicyVersion, candidate: &Arn) -> Result<bool, AspenError> {
111 let partition_pattern = self.partition_pattern();
112 let service_pattern = self.service_pattern();
113 let region_pattern = self.region_pattern();
114 let account_id_pattern = self.account_id_pattern();
115 let resource_pattern = self.resource_pattern();
116
117 let partition = regex_from_glob(partition_pattern, false);
118 let service = regex_from_glob(service_pattern, false);
119 let region = regex_from_glob(region_pattern, false);
120 let account_id = regex_from_glob(account_id_pattern, false);
121 let resource = context.matcher(resource_pattern, pv, false)?;
122
123 let partition_match = partition.is_match(candidate.partition());
124 let service_match = service.is_match(candidate.service());
125 let region_match = region.is_match(candidate.region());
126 let account_id_match = account_id.is_match(candidate.account_id());
127 let resource_match = resource.is_match(candidate.resource());
128 let result = partition_match && service_match && region_match && account_id_match && resource_match;
129
130 log::trace!("arn_pattern_matches: pattern={:?}, candidate={} -> partition={:?} ({}) service={:?} ({}) region={:?} ({}) account_id={:?} ({}) resource={:?} vs {:?} ({}) -> result={}", self, candidate, partition, partition_match, service, service_match, region, region_match, account_id, account_id_match, resource, candidate.resource(), resource_match, result);
131
132 Ok(result)
133 }
134}
135
136impl FromStr for ResourceArn {
137 type Err = AspenError;
138
139 fn from_str(s: &str) -> Result<Self, Self::Err> {
141 let parts: Vec<&str> = s.splitn(6, ':').collect();
142 if parts.len() != 6 || parts[0] != "arn" {
143 return Err(AspenError::InvalidResource(s.to_string()));
144 }
145
146 let arn = s.to_string();
147 let service_start = PARTITION_START + parts[1].len() + 1;
148 let region_start = service_start + parts[2].len() + 1;
149 let account_id_start = region_start + parts[3].len() + 1;
150 let resource_start = account_id_start + parts[4].len() + 1;
151
152 Ok(Self {
153 arn,
154 service_start,
155 region_start,
156 account_id_start,
157 resource_start,
158 })
159 }
160}
161
162impl Display for ResourceArn {
163 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
164 f.write_str(&self.arn)
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use {
171 super::ResourceArn,
172 crate::AspenError,
173 pretty_assertions::{assert_eq, assert_ne},
174 std::{collections::hash_map::DefaultHasher, hash::Hash, str::FromStr},
175 };
176
177 #[test_log::test]
178 fn check_arn_pattern_derived() {
179 let pat1a = ResourceArn::from_str("arn:*:ec2:us-*-1:123456789012:instance/*").unwrap();
180 let pat1b = ResourceArn::new("*", "ec2", "us-*-1", "123456789012", "instance/*");
181 let pat1c = pat1a.clone();
182 let pat2 = ResourceArn::from_str("arn:aws:ec2:us-east-1:123456789012:instance/*").unwrap();
183 let pat3 = ResourceArn::from_str("arn:aws:ec*:us-*-1::*").unwrap();
184
185 assert_eq!(pat1a, pat1b);
186 assert_ne!(pat1a, pat2);
187 assert_eq!(pat1c, pat1b);
188
189 assert_eq!(pat1a.partition_pattern(), "*");
190 assert_eq!(pat1a.service_pattern(), "ec2");
191 assert_eq!(pat1a.region_pattern(), "us-*-1");
192 assert_eq!(pat1a.account_id_pattern(), "123456789012");
193 assert_eq!(pat1a.resource_pattern(), "instance/*");
194
195 let mut h2 = DefaultHasher::new();
197 pat3.hash(&mut h2);
198
199 _ = format!("{pat3:?}");
201
202 assert_eq!(pat3.to_string(), "arn:aws:ec*:us-*-1::*".to_string());
204 }
205
206 #[test_log::test]
207 fn check_arn_pattern_components() {
208 let pat = ResourceArn::from_str("arn:aws:ec*:us-*-1::*").unwrap();
209 assert_eq!(pat.partition_pattern(), "aws");
210 assert_eq!(pat.service_pattern(), "ec*");
211 assert_eq!(pat.region_pattern(), "us-*-1");
212 assert_eq!(pat.account_id_pattern(), "");
213 assert_eq!(pat.resource_pattern(), "*");
214 }
215
216 #[test_log::test]
217 fn check_malformed_patterns() {
218 let wrong_parts =
219 vec!["arn", "arn:aw*", "arn:aw*:e?2", "arn:aw*:e?2:us-*-1", "arn:aw*:e?2:us-*-1:123456789012"];
220 for wrong_part in wrong_parts {
221 assert_eq!(
222 ResourceArn::from_str(wrong_part).unwrap_err().to_string(),
223 format!("Invalid resource: {wrong_part}")
224 );
225 }
226
227 let err =
228 ResourceArn::from_str("https:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0").unwrap_err();
229 assert_eq!(
230 err,
231 AspenError::InvalidResource(
232 "https:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0".to_string()
233 )
234 );
235 }
236}