scratchstack_aws_principal/
federated_user.rs

1use {
2    crate::{utils::validate_name, PrincipalError},
3    scratchstack_arn::{
4        utils::{validate_account_id, validate_partition},
5        Arn,
6    },
7    std::fmt::{Display, Formatter, Result as FmtResult},
8};
9
10/// Details about an AWS IAM federated user.
11///
12/// FederatedUser structs are immutable.
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct FederatedUser {
15    /// The partition this principal exists in.
16    partition: String,
17
18    /// The account id.
19    account_id: String,
20
21    /// Name of the principal, case-insensitive.
22    user_name: String,
23}
24
25impl FederatedUser {
26    /// Create a [FederatedUser] object.
27    ///
28    /// * `partition`: The partition this principal exists in.
29    /// * `account_id`: The 12 digit account id. This must be composed of 12 ASCII digits or a
30    ///     [PrincipalError::InvalidAccountId] error will be returned.
31    /// * `user_name`: The name of the federated user. This must meet the following requirements or a
32    ///     [PrincipalError::InvalidFederatedUserName] error will be returned:
33    ///     *   The name must contain between 2 and 32 characters.
34    ///     *   The name must be composed to ASCII alphanumeric characters or one of `, - . = @ _`.
35    ///
36    /// If all of the requirements are met, a [FederatedUser] object is returned. Otherwise, a [PrincipalError] error
37    /// is returned.
38    ///
39    /// # Example
40    ///
41    /// ```
42    /// # use scratchstack_aws_principal::FederatedUser;
43    /// let federated_user = FederatedUser::new("aws", "123456789012", "user@example.com").unwrap();
44    /// assert_eq!(federated_user.partition(), "aws");
45    /// assert_eq!(federated_user.account_id(), "123456789012");
46    /// assert_eq!(federated_user.user_name(), "user@example.com");
47    /// ```
48    pub fn new(partition: &str, account_id: &str, user_name: &str) -> Result<Self, PrincipalError> {
49        validate_partition(partition)?;
50        validate_account_id(account_id)?;
51        validate_name(user_name, 32, PrincipalError::InvalidFederatedUserName)?;
52
53        if user_name.len() < 2 {
54            Err(PrincipalError::InvalidFederatedUserName(user_name.into()))
55        } else {
56            Ok(Self {
57                partition: partition.into(),
58                account_id: account_id.into(),
59                user_name: user_name.into(),
60            })
61        }
62    }
63
64    /// The partition of the user.
65    #[inline]
66    pub fn partition(&self) -> &str {
67        &self.partition
68    }
69
70    /// The account ID of the user.
71    #[inline]
72    pub fn account_id(&self) -> &str {
73        &self.account_id
74    }
75
76    /// The name of the user.
77    #[inline]
78    pub fn user_name(&self) -> &str {
79        &self.user_name
80    }
81}
82
83impl From<&FederatedUser> for Arn {
84    fn from(user: &FederatedUser) -> Arn {
85        Arn::new(&user.partition, "sts", "", &user.account_id, &format!("federated-user/{}", user.user_name)).unwrap()
86    }
87}
88
89impl Display for FederatedUser {
90    fn fmt(&self, f: &mut Formatter) -> FmtResult {
91        write!(f, "arn:{}:sts::{}:federated-user/{}", self.partition, self.account_id, self.user_name)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use {
98        super::FederatedUser,
99        crate::{PrincipalIdentity, PrincipalSource},
100        scratchstack_arn::Arn,
101        std::{
102            collections::hash_map::DefaultHasher,
103            hash::{Hash, Hasher},
104        },
105    };
106
107    #[test]
108    fn check_components() {
109        let user = FederatedUser::new("aws", "123456789012", "test-user").unwrap();
110        assert_eq!(user.partition(), "aws");
111        assert_eq!(user.account_id(), "123456789012");
112        assert_eq!(user.user_name(), "test-user");
113
114        let arn: Arn = (&user).into();
115        assert_eq!(arn.partition(), "aws");
116        assert_eq!(arn.service(), "sts");
117        assert_eq!(arn.account_id(), "123456789012");
118        assert_eq!(arn.resource(), "federated-user/test-user");
119
120        let p = PrincipalIdentity::from(user);
121        let source = p.source();
122        assert_eq!(source, PrincipalSource::Federated);
123        assert_eq!(source.to_string(), "Federated");
124    }
125
126    #[test]
127    #[allow(clippy::redundant_clone)]
128    fn check_derived() {
129        let u1a = FederatedUser::new("aws", "123456789012", "test-user1").unwrap();
130        let u1b = FederatedUser::new("aws", "123456789012", "test-user1").unwrap();
131        let u2 = FederatedUser::new("aws", "123456789012", "test-user2").unwrap();
132        let u3 = FederatedUser::new("aws", "123456789013", "test-user2").unwrap();
133        let u4 = FederatedUser::new("awt", "123456789013", "test-user2").unwrap();
134
135        assert_eq!(u1a, u1b);
136        assert_ne!(u1a, u2);
137        assert_eq!(u1a.clone(), u1a);
138
139        // Ensure we can hash a federated user.
140        let mut h1a = DefaultHasher::new();
141        let mut h1b = DefaultHasher::new();
142        let mut h2 = DefaultHasher::new();
143        u1a.hash(&mut h1a);
144        u1b.hash(&mut h1b);
145        u2.hash(&mut h2);
146        let hash1a = h1a.finish();
147        let hash1b = h1b.finish();
148        let hash2 = h2.finish();
149        assert_eq!(hash1a, hash1b);
150        assert_ne!(hash1a, hash2);
151
152        // Ensure ordering is logical.
153        assert!(u1a <= u1b);
154        assert!(u1a < u2);
155        assert!(u2 > u1a);
156        assert!(u2 < u3);
157        assert!(u3 > u2);
158        assert!(u1a < u3);
159        assert!(u3 < u4);
160
161        assert_eq!(u1a.clone().max(u2.clone()), u2);
162        assert_eq!(u1a.clone().min(u2), u1a);
163
164        // Ensure formatting is correct to an ARN.
165        assert_eq!(u1a.to_string(), "arn:aws:sts::123456789012:federated-user/test-user1");
166
167        // Ensure we can debug print the federated user.
168        let _ = format!("{u1a:?}");
169    }
170
171    #[test]
172    fn check_valid_federated_users() {
173        let f1a = FederatedUser::new("aws", "123456789012", "user@domain").unwrap();
174        let f1b = FederatedUser::new("aws", "123456789012", "user@domain").unwrap();
175        let f2 = FederatedUser::new("partition-with-32-characters1234", "123456789012", "user@domain").unwrap();
176        let f3 = FederatedUser::new("aws", "123456789012", "user@domain-with_32-characters==").unwrap();
177
178        assert_eq!(f1a, f1b);
179        assert_ne!(f1a, f2);
180        assert_eq!(f1a, f1a.clone());
181
182        assert_eq!(f1a.to_string(), "arn:aws:sts::123456789012:federated-user/user@domain");
183        assert_eq!(f2.to_string(), "arn:partition-with-32-characters1234:sts::123456789012:federated-user/user@domain");
184        assert_eq!(f3.to_string(), "arn:aws:sts::123456789012:federated-user/user@domain-with_32-characters==");
185    }
186
187    #[test]
188    fn check_invalid_federated_users() {
189        assert_eq!(
190            FederatedUser::new("", "123456789012", "user@domain",).unwrap_err().to_string(),
191            r#"Invalid partition: """#
192        );
193
194        assert_eq!(FederatedUser::new("aws", "", "user@domain",).unwrap_err().to_string(), r#"Invalid account id: """#);
195
196        assert_eq!(
197            FederatedUser::new("aws", "123456789012", "",).unwrap_err().to_string(),
198            r#"Invalid federated user name: """#
199        );
200
201        assert_eq!(
202            FederatedUser::new("aws", "123456789012", "user!name@domain",).unwrap_err().to_string(),
203            r#"Invalid federated user name: "user!name@domain""#
204        );
205
206        assert_eq!(
207            FederatedUser::new("aws", "123456789012", "u",).unwrap_err().to_string(),
208            r#"Invalid federated user name: "u""#
209        );
210
211        assert_eq!(
212            FederatedUser::new("partition-with-33-characters12345", "123456789012", "user@domain",)
213                .unwrap_err()
214                .to_string(),
215            r#"Invalid partition: "partition-with-33-characters12345""#
216        );
217
218        assert_eq!(
219            FederatedUser::new("aws", "1234567890123", "user@domain",).unwrap_err().to_string(),
220            r#"Invalid account id: "1234567890123""#
221        );
222
223        assert_eq!(
224            FederatedUser::new("aws", "123456789012", "user@domain-with-33-characters===",).unwrap_err().to_string(),
225            r#"Invalid federated user name: "user@domain-with-33-characters===""#
226        );
227    }
228}
229// end tests -- do not delete; needed for coverage.