1use {
2 crate::{
3 utils::{validate_name, validate_path},
4 PrincipalError,
5 },
6 scratchstack_arn::{
7 utils::{validate_account_id, validate_partition},
8 Arn,
9 },
10 std::{
11 fmt::{Display, Formatter, Result as FmtResult},
12 str::FromStr,
13 },
14};
15
16#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
20pub struct User {
21 partition: String,
23
24 account_id: String,
26
27 path: String,
29
30 user_name: String,
32}
33
34impl User {
35 pub fn new(partition: &str, account_id: &str, path: &str, user_name: &str) -> Result<Self, PrincipalError> {
57 validate_partition(partition)?;
58 validate_account_id(account_id)?;
59 validate_path(path)?;
60 validate_name(user_name, 64, PrincipalError::InvalidUserName)?;
61
62 Ok(Self {
63 partition: partition.into(),
64 account_id: account_id.into(),
65 path: path.into(),
66 user_name: user_name.into(),
67 })
68 }
69
70 #[inline]
72 pub fn partition(&self) -> &str {
73 &self.partition
74 }
75
76 #[inline]
78 pub fn account_id(&self) -> &str {
79 &self.account_id
80 }
81
82 #[inline]
84 pub fn path(&self) -> &str {
85 &self.path
86 }
87
88 #[inline]
90 pub fn user_name(&self) -> &str {
91 &self.user_name
92 }
93}
94
95impl From<&User> for Arn {
96 fn from(user: &User) -> Arn {
97 Arn::new(&user.partition, "iam", "", &user.account_id, &format!("user{}{}", user.path, user.user_name)).unwrap()
98 }
99}
100
101impl FromStr for User {
102 type Err = PrincipalError;
103
104 fn from_str(arn: &str) -> Result<Self, PrincipalError> {
116 let parsed_arn = Arn::from_str(arn)?;
117 Self::try_from(&parsed_arn)
118 }
119}
120
121impl TryFrom<&Arn> for User {
122 type Error = PrincipalError;
123
124 fn try_from(arn: &Arn) -> Result<Self, Self::Error> {
140 let service = arn.service();
141 let region = arn.region();
142 let resource = arn.resource();
143
144 if service != "iam" {
145 return Err(PrincipalError::InvalidService(service.to_string()));
146 }
147
148 if !region.is_empty() {
149 return Err(PrincipalError::InvalidRegion(region.to_string()));
150 }
151
152 if !resource.starts_with("user/") {
153 return Err(PrincipalError::InvalidResource(resource.to_string()));
154 }
155
156 let path_and_username = &resource[4..];
157 let last_slash = path_and_username.rfind('/').unwrap(); let path = &path_and_username[..=last_slash];
159 let user_name = &path_and_username[last_slash + 1..];
160
161 Self::new(arn.partition(), arn.account_id(), path, user_name)
162 }
163}
164
165impl Display for User {
166 fn fmt(&self, f: &mut Formatter) -> FmtResult {
167 write!(f, "arn:{}:iam::{}:user{}{}", self.partition, self.account_id, self.path, self.user_name)
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use {
174 super::User,
175 crate::{PrincipalIdentity, PrincipalSource},
176 scratchstack_arn::Arn,
177 std::{
178 collections::hash_map::DefaultHasher,
179 hash::{Hash, Hasher},
180 str::FromStr,
181 },
182 };
183
184 #[test]
185 fn check_components() {
186 let user = User::new("aws", "123456789012", "/my/path/", "user-name").unwrap();
187 assert_eq!(user.partition(), "aws");
188 assert_eq!(user.account_id(), "123456789012");
189 assert_eq!(user.path(), "/my/path/");
190 assert_eq!(user.user_name(), "user-name");
191
192 let arn: Arn = (&user).into();
193 assert_eq!(arn.partition(), "aws");
194 assert_eq!(arn.service(), "iam");
195 assert_eq!(arn.region(), "");
196 assert_eq!(arn.account_id(), "123456789012");
197 assert_eq!(arn.resource(), "user/my/path/user-name");
198
199 let p = PrincipalIdentity::from(user);
200 let source = p.source();
201 assert_eq!(source, PrincipalSource::Aws);
202 assert_eq!(source.to_string(), "AWS".to_string());
203 }
204
205 #[test]
206 fn check_derived() {
207 let u1a = User::new("aws", "123456789012", "/", "user1").unwrap();
208 let u1b = User::new("aws", "123456789012", "/", "user1").unwrap();
209 let u2 = User::new("aws", "123456789012", "/", "user2").unwrap();
210 let u3 = User::new("aws", "123456789012", "/path/", "user2").unwrap();
211 let u4 = User::new("aws", "123456789013", "/path/", "user2").unwrap();
212 let u5 = User::new("awt", "123456789013", "/path/", "user2").unwrap();
213
214 assert_eq!(u1a, u1b);
215 assert_ne!(u1a, u2);
216 assert_eq!(u1a, u1a.clone());
217
218 let mut h1a = DefaultHasher::new();
220 let mut h1b = DefaultHasher::new();
221 let mut h2 = DefaultHasher::new();
222 u1a.hash(&mut h1a);
223 u1b.hash(&mut h1b);
224 u2.hash(&mut h2);
225 let hash1a = h1a.finish();
226 let hash1b = h1b.finish();
227 let hash2 = h2.finish();
228 assert_eq!(hash1a, hash1b);
229 assert_ne!(hash1a, hash2);
230
231 assert!(u1a <= u1b);
233 assert!(u1a < u2);
234 assert!(u2 > u1a);
235 assert!(u2 < u3);
236 assert!(u3 > u2);
237 assert!(u3 > u1a);
238 assert!(u3 < u4);
239 assert!(u4 > u3);
240 assert!(u4 < u5);
241 assert!(u5 > u4);
242
243 assert!(u1a.clone().max(u2.clone()) == u2);
244 assert!(u1a.clone().min(u2.clone()) == u1a);
245
246 assert_eq!(u3.to_string(), "arn:aws:iam::123456789012:user/path/user2");
248
249 let _ = format!("{u1a:?}");
251 }
252
253 #[test]
254 fn check_valid_users() {
255 let u1a = User::new("aws", "123456789012", "/", "user-name").unwrap();
256 let u1b = User::new("aws", "123456789012", "/", "user-name").unwrap();
257 let u2 = User::new("aws", "123456789012", "/", "user-name_is@ok.with,accepted=symbols").unwrap();
258 let u3 = User::new(
259 "aws",
260 "123456789012",
261 "/!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~/",
262 "user-name",
263 )
264 .unwrap();
265 let u4 =
266 User::new("aws", "123456789012", "/", "user-name-with-64-characters====================================")
267 .unwrap();
268
269 assert_eq!(u1a, u1b);
270 assert_ne!(u1a, u2);
271 assert_eq!(u1a, u1a.clone());
272 assert_ne!(u3, u4);
273 assert_eq!(u3, u3.clone());
274
275 assert_eq!(u1a.partition(), "aws");
276 assert_eq!(u1a.account_id(), "123456789012");
277 assert_eq!(u1a.path(), "/");
278 assert_eq!(u1a.user_name(), "user-name");
279
280 assert_eq!(u1a.to_string(), "arn:aws:iam::123456789012:user/user-name");
281 assert_eq!(u2.to_string(), "arn:aws:iam::123456789012:user/user-name_is@ok.with,accepted=symbols");
282
283 User::new("aws", "123456789012", "/path/test/", "user-name").unwrap();
284 User::new("aws", "123456789012", "/path///multi-slash/test/", "user-name").unwrap();
285 User::new("aws", "123456789012", "/", "user-name").unwrap();
286
287 let _ = format!("{u3:?}");
289 }
290
291 #[test]
292 fn check_invalid_users() {
293 let err = User::new("", "123456789012", "/", "user-name").unwrap_err();
294 assert_eq!(err.to_string(), r#"Invalid partition: """#);
295 let err = User::from_str("arn::iam::123456789012:user/user-name").unwrap_err();
296 assert_eq!(err.to_string(), r#"Invalid partition: """#);
297
298 let err = User::new("aws", "", "/", "user-name").unwrap_err();
299 assert_eq!(err.to_string(), r#"Invalid account id: """#);
300
301 let err = User::new("aws", "123456789012", "", "user-name").unwrap_err();
302 assert_eq!(err.to_string(), r#"Invalid path: """#);
303
304 let err = User::new("aws", "123456789012", "/", "").unwrap_err();
305 assert_eq!(err.to_string(), r#"Invalid user name: """#);
306
307 let err =
308 User::new("aws", "123456789012", "/", "user-name-with-65-characters=====================================")
309 .unwrap_err();
310 assert_eq!(
311 err.to_string(),
312 r#"Invalid user name: "user-name-with-65-characters=====================================""#
313 );
314
315 let err = User::new("aws", "123456789012", "/", "user!name").unwrap_err();
316 assert_eq!(err.to_string(), r#"Invalid user name: "user!name""#);
317
318 let err = User::new("aws", "123456789012", "path/test/", "user-name").unwrap_err();
319 assert_eq!(err.to_string(), r#"Invalid path: "path/test/""#);
320
321 let err = User::new("aws", "123456789012", "/path/test", "user-name").unwrap_err();
322 assert_eq!(err.to_string(), r#"Invalid path: "/path/test""#);
323
324 let err = User::new("aws", "123456789012", "/path test/", "user-name").unwrap_err();
325 assert_eq!(err.to_string(), r#"Invalid path: "/path test/""#);
326
327 let err = User::from_str("arn:aws:sts::123456789012:user/user-name").unwrap_err();
328 assert_eq!(err.to_string(), r#"Invalid service name: "sts""#);
329
330 let err = User::from_str("arn:aws:iam:us-east-1:123456789012:user/user-name").unwrap_err();
331 assert_eq!(err.to_string(), r#"Invalid region: "us-east-1""#);
332
333 let err = User::from_str("arn:aws:iam::123456789012:role/user-name").unwrap_err();
334 assert_eq!(err.to_string(), r#"Invalid resource: "role/user-name""#);
335 }
336}
337