scratchstack_aws_principal/
utils.rs1use {
2 crate::PrincipalError,
3 std::fmt::{Display, Formatter, Result as FmtResult},
4};
5
6#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum IamIdPrefix {
10 AccessKey,
12
13 BearerToken,
15
16 Certificate,
18
19 ContextSpecificCredential,
21
22 Group,
24
25 InstanceProfile,
27
28 ManagedPolicy,
30
31 ManagedPolicyVersion,
35
36 PublicKey,
38
39 Role,
41
42 TemporaryAccessKey,
44
45 User,
47}
48
49impl Display for IamIdPrefix {
50 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
51 match self {
52 Self::AccessKey => f.write_str("AKIA"),
53 Self::BearerToken => f.write_str("ABIA"),
54 Self::Certificate => f.write_str("ASCA"),
55 Self::ContextSpecificCredential => f.write_str("ACCA"),
56 Self::Group => f.write_str("AGPA"),
57 Self::InstanceProfile => f.write_str("AIPA"),
58 Self::ManagedPolicy => f.write_str("ANPA"),
59 Self::ManagedPolicyVersion => f.write_str("ANVA"),
60 Self::PublicKey => f.write_str("APKA"),
61 Self::Role => f.write_str("AROA"),
62 Self::TemporaryAccessKey => f.write_str("ASIA"),
63 Self::User => f.write_str("AIDA"),
64 }
65 }
66}
67
68impl AsRef<str> for IamIdPrefix {
69 fn as_ref(&self) -> &str {
70 match self {
71 Self::AccessKey => "AKIA",
72 Self::BearerToken => "ABIA",
73 Self::Certificate => "ASCA",
74 Self::ContextSpecificCredential => "ACCA",
75 Self::Group => "AGPA",
76 Self::InstanceProfile => "AIPA",
77 Self::ManagedPolicy => "ANPA",
78 Self::ManagedPolicyVersion => "ANVA",
79 Self::PublicKey => "APKA",
80 Self::Role => "AROA",
81 Self::TemporaryAccessKey => "ASIA",
82 Self::User => "AIDA",
83 }
84 }
85}
86
87impl IamIdPrefix {
88 pub fn as_str(&self) -> &str {
90 self.as_ref()
91 }
92}
93
94pub fn validate_name<F: FnOnce(String) -> PrincipalError>(
109 name: &str,
110 max_length: usize,
111 map_err: F,
112) -> Result<(), PrincipalError> {
113 let n_bytes = name.as_bytes();
114 let n_len = n_bytes.len();
115
116 if n_len == 0 || n_len > max_length {
117 return Err(map_err(name.to_string()));
118 }
119
120 for c in n_bytes {
122 if !(c.is_ascii_alphanumeric()
123 || *c == b','
124 || *c == b'-'
125 || *c == b'.'
126 || *c == b'='
127 || *c == b'@'
128 || *c == b'_')
129 {
130 return Err(map_err(name.to_string()));
131 }
132 }
133
134 Ok(())
135}
136
137pub fn validate_identifier<F: FnOnce(String) -> PrincipalError>(
144 id: &str,
145 prefix: &str,
146 map_err: F,
147) -> Result<(), PrincipalError> {
148 if !id.starts_with(prefix) || id.len() < 20 {
149 Err(map_err(id.to_string()))
150 } else {
151 for c in id.as_bytes() {
152 if !(c.is_ascii_alphabetic() || (b'2'..=b'7').contains(c)) {
154 return Err(map_err(id.to_string()));
155 }
156 }
157
158 Ok(())
159 }
160}
161
162pub fn validate_path(path: &str) -> Result<(), PrincipalError> {
172 let p_bytes = path.as_bytes();
173 let p_len = p_bytes.len();
174
175 if p_len == 0 || p_len > 512 {
176 return Err(PrincipalError::InvalidPath(path.to_string()));
177 }
178
179 if p_bytes[0] != b'/' || p_bytes[p_len - 1] != b'/' {
181 return Err(PrincipalError::InvalidPath(path.to_string()));
182 }
183
184 for c in p_bytes {
186 if *c < 0x21 || *c > 0x7e {
187 return Err(PrincipalError::InvalidPath(path.to_string()));
188 }
189 }
190
191 Ok(())
192}
193
194pub fn validate_dns<F: FnOnce(String) -> PrincipalError>(
202 name: &str,
203 max_length: usize,
204 map_err: F,
205) -> Result<(), PrincipalError> {
206 let name_bytes = name.as_bytes();
207 if name_bytes.is_empty() || name_bytes.len() > max_length {
208 return Err(map_err(name.to_string()));
209 }
210
211 let components = name_bytes.split(|c| *c == b'.');
212
213 for component in components {
214 if component.is_empty() || component.len() > 63 {
215 return Err(map_err(name.to_string()));
216 }
217
218 let mut last = b'-';
219
220 for c in component.iter() {
221 if *c == b'-' {
222 if last == b'-' {
223 return Err(map_err(name.to_string()));
224 }
225 } else if !c.is_ascii_alphanumeric() && *c != b'_' {
226 return Err(map_err(name.to_string()));
227 }
228
229 last = *c;
230 }
231
232 if last == b'-' {
233 return Err(map_err(name.to_string()));
234 }
235 }
236
237 Ok(())
238}
239
240#[cfg(test)]
241mod test {
242 use {
243 super::{validate_dns, validate_identifier, validate_name, IamIdPrefix},
244 crate::PrincipalError,
245 std::{
246 collections::hash_map::DefaultHasher,
247 hash::{Hash, Hasher},
248 },
249 };
250
251 #[test]
252 fn check_names() {
253 validate_name("test", 32, PrincipalError::InvalidRoleName).unwrap();
254 validate_name("test,name-.with=exactly@32_chars", 32, PrincipalError::InvalidRoleName).unwrap();
255 assert_eq!(
256 validate_name("bad!name", 32, PrincipalError::InvalidRoleName).unwrap_err().to_string(),
257 r#"Invalid role name: "bad!name""#
258 );
259 }
260
261 fn validate_group_id(id: &str) -> Result<(), PrincipalError> {
262 validate_identifier(id, IamIdPrefix::Group.as_str(), PrincipalError::InvalidGroupId)
263 }
264
265 fn validate_instance_profile_id(id: &str) -> Result<(), PrincipalError> {
266 validate_identifier(id, IamIdPrefix::InstanceProfile.as_str(), PrincipalError::InvalidInstanceProfileId)
267 }
268
269 fn validate_role_id(id: &str) -> Result<(), PrincipalError> {
270 validate_identifier(id, IamIdPrefix::Role.as_str(), PrincipalError::InvalidRoleId)
271 }
272
273 fn validate_user_id(id: &str) -> Result<(), PrincipalError> {
274 validate_identifier(id, IamIdPrefix::User.as_str(), PrincipalError::InvalidUserId)
275 }
276
277 #[test]
278 fn check_identifiers() {
279 validate_group_id("AGPA234567ABCDEFGHIJ").unwrap();
280 let err = validate_group_id("AIDA234567ABCDEFGHIJ").unwrap_err();
281 assert_eq!(err.to_string(), r#"Invalid group id: "AIDA234567ABCDEFGHIJ""#);
282 let err = validate_group_id("AGPA234567ABCDEFGHI!").unwrap_err();
283 assert_eq!(err.to_string(), r#"Invalid group id: "AGPA234567ABCDEFGHI!""#);
284 let err = validate_group_id("AGPA234567ABCDEFGHI").unwrap_err();
285 assert_eq!(err.to_string(), r#"Invalid group id: "AGPA234567ABCDEFGHI""#);
286
287 validate_instance_profile_id("AIPAKLMNOPQRSTUVWXYZ").unwrap();
288 let err = validate_instance_profile_id("AKIAKLMNOPQRSTUVWXYZ").unwrap_err();
289 assert_eq!(err.to_string(), r#"Invalid instance profile id: "AKIAKLMNOPQRSTUVWXYZ""#);
290 let err = validate_instance_profile_id("AIPAKLMNOPQRSTUVWXY!").unwrap_err();
291 assert_eq!(err.to_string(), r#"Invalid instance profile id: "AIPAKLMNOPQRSTUVWXY!""#);
292 let err = validate_instance_profile_id("AIPAKLMNOPQRSTUVWXY").unwrap_err();
293 assert_eq!(err.to_string(), r#"Invalid instance profile id: "AIPAKLMNOPQRSTUVWXY""#);
294
295 validate_role_id("AROAKLMNOPQRSTUVWXYZ").unwrap();
296 let err = validate_role_id("AKIAKLMNOPQRSTUVWXYZ").unwrap_err();
297 assert_eq!(err.to_string(), r#"Invalid role id: "AKIAKLMNOPQRSTUVWXYZ""#);
298 let err = validate_role_id("AROAKLMNOPQRSTUVWXY!").unwrap_err();
299 assert_eq!(err.to_string(), r#"Invalid role id: "AROAKLMNOPQRSTUVWXY!""#);
300 let err = validate_role_id("AROAKLMNOPQRSTUVWXY").unwrap_err();
301 assert_eq!(err.to_string(), r#"Invalid role id: "AROAKLMNOPQRSTUVWXY""#);
302
303 validate_user_id("AIDAKLMNOPQRSTUVWXYZ").unwrap();
304 let err = validate_user_id("AKIAKLMNOPQRSTUVWXYZ").unwrap_err();
305 assert_eq!(err.to_string(), r#"Invalid user id: "AKIAKLMNOPQRSTUVWXYZ""#);
306 let err = validate_user_id("AIDAKLMNOPQRSTUVWXY!").unwrap_err();
307 assert_eq!(err.to_string(), r#"Invalid user id: "AIDAKLMNOPQRSTUVWXY!""#);
308 let err = validate_user_id("AIDAKLMNOPQRSTUVWXY").unwrap_err();
309 assert_eq!(err.to_string(), r#"Invalid user id: "AIDAKLMNOPQRSTUVWXY""#);
310 }
311
312 #[test]
313 fn check_id_prefix_derived() {
314 let prefixes = [
315 IamIdPrefix::AccessKey,
316 IamIdPrefix::BearerToken,
317 IamIdPrefix::Certificate,
318 IamIdPrefix::ContextSpecificCredential,
319 IamIdPrefix::Group,
320 IamIdPrefix::InstanceProfile,
321 IamIdPrefix::ManagedPolicy,
322 IamIdPrefix::ManagedPolicyVersion,
323 IamIdPrefix::PublicKey,
324 IamIdPrefix::Role,
325 IamIdPrefix::TemporaryAccessKey,
326 IamIdPrefix::User,
327 ];
328 let p1a = IamIdPrefix::AccessKey;
329 let p1b = p1a;
330 let p2 = IamIdPrefix::BearerToken;
331 assert_eq!(p1a, p1b);
332 assert_eq!(p1a, p1a.clone());
333 assert_ne!(p1a, p2);
334
335 let mut h1a = DefaultHasher::new();
337 let mut h1b = DefaultHasher::new();
338 let mut h2 = DefaultHasher::new();
339 p1a.hash(&mut h1a);
340 p1b.hash(&mut h1b);
341 p2.hash(&mut h2);
342 let hash1a = h1a.finish();
343 let hash1b = h1b.finish();
344 let hash2 = h2.finish();
345 assert_eq!(hash1a, hash1b);
346 assert_ne!(hash1a, hash2);
347
348 for i in 0..prefixes.len() {
350 for j in i + 1..prefixes.len() {
351 assert!(prefixes[i] < prefixes[j]);
352 assert!(prefixes[j] > prefixes[i]);
353 assert_eq!(prefixes[i].max(prefixes[j]), prefixes[j]);
354 }
355
356 let _ = format!("{:?}", prefixes[i]);
357 assert_eq!(prefixes[i].to_string().as_str(), prefixes[i].as_ref());
358 }
359 }
360
361 #[test]
362 fn check_access_key() {
363 assert_eq!(IamIdPrefix::AccessKey.as_ref(), "AKIA");
365 assert_eq!(format!("{}", IamIdPrefix::AccessKey).as_str(), "AKIA");
366 }
367
368 #[test]
369 fn check_dns() {
370 validate_dns("exa_mple.com", 256, PrincipalError::InvalidService).unwrap();
371 let e = validate_dns("exa_mple.com.", 256, PrincipalError::InvalidService).unwrap_err();
372 assert_eq!(e.to_string(), r#"Invalid service name: "exa_mple.com.""#);
373 let e = validate_dns("example.com", 5, PrincipalError::InvalidService).unwrap_err();
374 assert_eq!(e.to_string(), r#"Invalid service name: "example.com""#);
375 validate_dns("exam-ple.com", 256, PrincipalError::InvalidService).unwrap();
376 validate_dns("exam--ple.com", 256, PrincipalError::InvalidService).unwrap_err();
377 validate_dns("-example.com", 256, PrincipalError::InvalidService).unwrap_err();
378 validate_dns("example-.com", 256, PrincipalError::InvalidService).unwrap_err();
379 }
380}
381