1use {
2 crate::{utils::validate_name, PrincipalError},
3 scratchstack_arn::{
4 utils::{validate_account_id, validate_partition},
5 Arn,
6 },
7 std::{
8 fmt::{Display, Formatter, Result as FmtResult},
9 str::FromStr,
10 },
11};
12
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub struct AssumedRole {
18 partition: String,
20
21 account_id: String,
23
24 role_name: String,
26
27 session_name: String,
29}
30
31impl AssumedRole {
32 pub fn new(partition: &str, account_id: &str, role_name: &str, session_name: &str) -> Result<Self, PrincipalError> {
64 validate_partition(partition)?;
65 validate_account_id(account_id)?;
66 validate_name(role_name, 64, PrincipalError::InvalidRoleName)?;
67 validate_name(session_name, 64, PrincipalError::InvalidSessionName)?;
68
69 if session_name.len() < 2 {
70 Err(PrincipalError::InvalidSessionName(session_name.into()))
71 } else {
72 Ok(Self {
73 partition: partition.into(),
74 account_id: account_id.into(),
75 role_name: role_name.into(),
76 session_name: session_name.into(),
77 })
78 }
79 }
80
81 #[inline]
83 pub fn partition(&self) -> &str {
84 &self.partition
85 }
86
87 #[inline]
89 pub fn account_id(&self) -> &str {
90 &self.account_id
91 }
92
93 #[inline]
95 pub fn role_name(&self) -> &str {
96 &self.role_name
97 }
98
99 #[inline]
101 pub fn session_name(&self) -> &str {
102 &self.session_name
103 }
104}
105
106impl FromStr for AssumedRole {
107 type Err = PrincipalError;
108
109 fn from_str(arn: &str) -> Result<Self, PrincipalError> {
120 let parsed_arn = Arn::from_str(arn)?;
121 Self::try_from(&parsed_arn)
122 }
123}
124
125impl From<&AssumedRole> for Arn {
126 fn from(role: &AssumedRole) -> Arn {
127 Arn::new(
128 &role.partition,
129 "sts",
130 "",
131 &role.account_id,
132 &format!("assumed-role/{}/{}", role.role_name, role.session_name),
133 )
134 .unwrap()
135 }
136}
137
138impl Display for AssumedRole {
139 fn fmt(&self, f: &mut Formatter) -> FmtResult {
140 write!(
141 f,
142 "arn:{}:sts::{}:assumed-role/{}/{}",
143 self.partition, self.account_id, self.role_name, self.session_name
144 )
145 }
146}
147
148impl TryFrom<&Arn> for AssumedRole {
149 type Error = PrincipalError;
150
151 fn try_from(arn: &Arn) -> Result<Self, Self::Error> {
166 let service = arn.service();
167 let region = arn.region();
168 let resource = arn.resource();
169
170 if service != "sts" {
171 return Err(PrincipalError::InvalidService(service.to_string()));
172 }
173
174 if !region.is_empty() {
175 return Err(PrincipalError::InvalidRegion(region.to_string()));
176 }
177
178 let resource_parts: Vec<&str> = resource.split('/').collect();
179 if resource_parts.len() != 3 || resource_parts[0] != "assumed-role" {
180 return Err(PrincipalError::InvalidResource(resource.to_string()));
181 }
182
183 Self::new(arn.partition(), arn.account_id(), resource_parts[1], resource_parts[2])
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use {
190 super::AssumedRole,
191 crate::{PrincipalIdentity, PrincipalSource},
192 scratchstack_arn::Arn,
193 std::{
194 collections::hash_map::DefaultHasher,
195 hash::{Hash, Hasher},
196 str::FromStr,
197 },
198 };
199
200 #[test]
201 fn check_components() {
202 let role = AssumedRole::new("aws", "123456789012", "role", "session").unwrap();
203 assert_eq!(role.partition(), "aws");
204 assert_eq!(role.account_id(), "123456789012");
205 assert_eq!(role.role_name(), "role");
206 assert_eq!(role.session_name(), "session");
207
208 let arn: Arn = (&role).into();
209 assert_eq!(arn.partition(), "aws");
210 assert_eq!(arn.service(), "sts");
211 assert_eq!(arn.region(), "");
212 assert_eq!(arn.account_id(), "123456789012");
213 assert_eq!(arn.resource(), "assumed-role/role/session");
214
215 let p = PrincipalIdentity::from(role);
216 let source = p.source();
217 assert_eq!(source, PrincipalSource::Aws);
218 assert_eq!(source.to_string(), "AWS".to_string());
219 }
220
221 #[test]
222 #[allow(clippy::redundant_clone)]
223 fn check_derived() {
224 let r1a = AssumedRole::new("aws", "123456789012", "role1", "session1").unwrap();
225 let r1b = AssumedRole::new("aws", "123456789012", "role1", "session1").unwrap();
226 let r2 = AssumedRole::new("aws", "123456789012", "role1", "session2").unwrap();
227 let r3 = AssumedRole::new("aws", "123456789012", "role2", "session2").unwrap();
228 let r4 = AssumedRole::new("aws", "123456789013", "role2", "session2").unwrap();
229 let r5 = AssumedRole::new("awt", "123456789013", "role2", "session2").unwrap();
230
231 assert_eq!(r1a, r1b);
232 assert_ne!(r1a, r2);
233 assert_eq!(r1a.clone(), r1a);
234
235 let mut h1a = DefaultHasher::new();
237 let mut h1b = DefaultHasher::new();
238 let mut h2 = DefaultHasher::new();
239 r1a.hash(&mut h1a);
240 r1b.hash(&mut h1b);
241 r2.hash(&mut h2);
242 let hash1a = h1a.finish();
243 let hash1b = h1b.finish();
244 let hash2 = h2.finish();
245 assert_eq!(hash1a, hash1b);
246 assert_ne!(hash1a, hash2);
247
248 assert!(r1a <= r1b);
250 assert!(r1a < r2);
251 assert!(r2 < r3);
252 assert!(r3 > r2);
253 assert!(r3 > r1a);
254 assert!(r3 < r4);
255 assert!(r4 > r3);
256 assert!(r4 < r5);
257 assert!(r5 > r4);
258
259 assert_eq!(r1a.clone().max(r2.clone()), r2);
260 assert_eq!(r1a.clone().min(r2), r1a);
261
262 assert_eq!(r1a.to_string(), "arn:aws:sts::123456789012:assumed-role/role1/session1");
264
265 let _ = format!("{r1a:?}");
267 }
268
269 #[test]
270 fn check_valid_assumed_roles() {
271 let r1a = AssumedRole::new("aws", "123456789012", "Role_name", "session_name").unwrap();
272 let r1b = AssumedRole::new("aws", "123456789012", "Role_name", "session_name").unwrap();
273 let r2 =
274 AssumedRole::new("aws2", "123456789012", "Role@Foo=bar,baz_=world-1234", "Session@1234,_=-,.OK").unwrap();
275
276 assert_eq!(r1a, r1b);
277 assert_ne!(r1a, r2);
278 assert!(r1a <= r1b);
279 assert!(r1a >= r1b);
280 assert_eq!(r1a.partition(), "aws");
281 assert_eq!(r1a.account_id(), "123456789012");
282 assert_eq!(r1a.role_name(), "Role_name");
283 assert_eq!(r1a.session_name(), "session_name");
284
285 assert!(r1a < r2);
286 assert!(r1a <= r2);
287 assert!(r2 > r1a);
288 assert!(r2 >= r1a);
289 assert!(r2 != r1a);
290
291 assert_eq!(r1a.to_string(), "arn:aws:sts::123456789012:assumed-role/Role_name/session_name");
292 assert_eq!(r1b.to_string(), "arn:aws:sts::123456789012:assumed-role/Role_name/session_name");
293 assert_eq!(
294 r2.to_string(),
295 "arn:aws2:sts::123456789012:assumed-role/Role@Foo=bar,baz_=world-1234/Session@1234,_=-,.OK"
296 );
297
298 let r1c = r1a.clone();
299 assert!(r1a == r1c);
300
301 AssumedRole::new("partition-with-32-characters1234", "123456789012", "role-name", "session_name").unwrap();
302
303 AssumedRole::new(
304 "aws",
305 "123456789012",
306 "role-name-with_64-characters====================================",
307 "session@1234",
308 )
309 .unwrap();
310
311 AssumedRole::new(
312 "aws",
313 "123456789012",
314 "role-name",
315 "session-name-with-64-characters=================================",
316 )
317 .unwrap();
318
319 let _ = format!("{r1a:?}");
321 }
322
323 #[test]
324 fn check_invalid_assumed_roles() {
325 let err = AssumedRole::new("", "123456789012", "role-name", "session-name").unwrap_err();
326 assert_eq!(err.to_string(), r#"Invalid partition: """#);
327
328 let err = AssumedRole::new("partition-with-33-characters12345", "123456789012", "role-name", "session_name")
329 .unwrap_err();
330 assert_eq!(err.to_string(), r#"Invalid partition: "partition-with-33-characters12345""#);
331
332 let err = AssumedRole::new("-aws", "123456789012", "role-name", "session-name").unwrap_err();
333 assert_eq!(err.to_string(), r#"Invalid partition: "-aws""#);
334 let err = AssumedRole::from_str("arn:-aws:sts::123456789012:assumed-role/role-name/session-name").unwrap_err();
335 assert_eq!(err.to_string(), r#"Invalid partition: "-aws""#);
336
337 let err = AssumedRole::new("aws-", "123456789012", "role-name", "session-name").unwrap_err();
338 assert_eq!(err.to_string(), r#"Invalid partition: "aws-""#);
339
340 let err = AssumedRole::new("aws--us", "123456789012", "role-name", "session-name").unwrap_err();
341 assert_eq!(err.to_string(), r#"Invalid partition: "aws--us""#);
342
343 let err = AssumedRole::new("aw!", "123456789012", "role-name", "session-name").unwrap_err();
344 assert_eq!(err.to_string(), r#"Invalid partition: "aw!""#);
345
346 let err = AssumedRole::new("aws", "", "role-name", "session-name").unwrap_err();
347 assert_eq!(err.to_string(), r#"Invalid account id: """#);
348
349 let err = AssumedRole::new("aws", "a23456789012", "role-name", "session-name").unwrap_err();
350 assert_eq!(err.to_string(), r#"Invalid account id: "a23456789012""#);
351
352 let err = AssumedRole::new("aws", "1234567890123", "role-name", "session-name").unwrap_err();
353 assert_eq!(err.to_string(), r#"Invalid account id: "1234567890123""#);
354
355 let err = AssumedRole::new("aws", "123456789012", "", "session-name").unwrap_err();
356 assert_eq!(err.to_string(), r#"Invalid role name: """#);
357
358 let err = AssumedRole::new(
359 "aws",
360 "123456789012",
361 "role-name-with-65-characters=====================================",
362 "session-name",
363 )
364 .unwrap_err();
365 assert_eq!(
366 err.to_string(),
367 r#"Invalid role name: "role-name-with-65-characters=====================================""#
368 );
369 let err = AssumedRole::from_str("arn:aws:sts::123456789012:assumed-role/role-name-with-65-characters=====================================/session-name")
370 .unwrap_err();
371 assert_eq!(
372 err.to_string(),
373 r#"Invalid role name: "role-name-with-65-characters=====================================""#
374 );
375
376 let err = AssumedRole::new("aws", "123456789012", "role+name", "session-name").unwrap_err();
377 assert_eq!(err.to_string(), r#"Invalid role name: "role+name""#);
378
379 let err = AssumedRole::new("aws", "123456789012", "role-name", "").unwrap_err();
380 assert_eq!(err.to_string(), r#"Invalid session name: """#);
381
382 let err = AssumedRole::new("aws", "123456789012", "role-name", "s").unwrap_err();
383 assert_eq!(err.to_string(), r#"Invalid session name: "s""#);
384
385 let err = AssumedRole::new(
386 "aws",
387 "123456789012",
388 "role-name",
389 "session-name-with-65-characters==================================",
390 )
391 .unwrap_err();
392
393 assert_eq!(
394 err.to_string(),
395 r#"Invalid session name: "session-name-with-65-characters==================================""#
396 );
397
398 let err = AssumedRole::new("aws", "123456789012", "role-name", "session+name").unwrap_err();
399 assert_eq!(err.to_string(), r#"Invalid session name: "session+name""#);
400
401 let err =
402 AssumedRole::from_str("arn:aws:iam::123456789012:assumed-role/role/role-name/session-name").unwrap_err();
403 assert_eq!(err.to_string(), r#"Invalid service name: "iam""#);
404
405 let err = AssumedRole::from_str("arn:aws:sts::123456789012:user/role/role-name/session-name").unwrap_err();
406 assert_eq!(err.to_string(), r#"Invalid resource: "user/role/role-name/session-name""#);
407 }
408}
409