rusticity_term/
iam.rs

1use crate::common::UTC_TIMESTAMP_WIDTH;
2use serde::{Deserialize, Serialize};
3
4pub fn format_arn(account_id: &str, resource_type: &str, resource_name: &str) -> String {
5    format!(
6        "arn:aws:iam::{}:{}/{}",
7        account_id, resource_type, resource_name
8    )
9}
10
11pub fn console_url_users(_region: &str) -> String {
12    "https://console.aws.amazon.com/iam/home#/users".to_string()
13}
14
15pub fn console_url_user_detail(region: &str, user_name: &str, section: &str) -> String {
16    format!(
17        "https://{}.console.aws.amazon.com/iam/home?region={}#/users/details/{}?section={}",
18        region, region, user_name, section
19    )
20}
21
22pub fn console_url_roles(_region: &str) -> String {
23    "https://console.aws.amazon.com/iam/home#/roles".to_string()
24}
25
26pub fn console_url_role_detail(region: &str, role_name: &str, section: &str) -> String {
27    format!(
28        "https://{}.console.aws.amazon.com/iam/home?region={}#/roles/details/{}?section={}",
29        region, region, role_name, section
30    )
31}
32
33pub fn console_url_role_policy(region: &str, role_name: &str, policy_name: &str) -> String {
34    format!(
35        "https://{}.console.aws.amazon.com/iam/home?region={}#/roles/details/{}/editPolicy/{}?step=addPermissions",
36        region, region, role_name, policy_name
37    )
38}
39
40pub fn console_url_groups(region: &str) -> String {
41    format!(
42        "https://{}.console.aws.amazon.com/iam/home?region={}#/groups",
43        region, region
44    )
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct IamUser {
49    pub user_name: String,
50    pub path: String,
51    pub groups: String,
52    pub last_activity: String,
53    pub mfa: String,
54    pub password_age: String,
55    pub console_last_sign_in: String,
56    pub access_key_id: String,
57    pub active_key_age: String,
58    pub access_key_last_used: String,
59    pub arn: String,
60    pub creation_time: String,
61    pub console_access: String,
62    pub signing_certs: String,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct IamRole {
67    pub role_name: String,
68    pub path: String,
69    pub trusted_entities: String,
70    pub last_activity: String,
71    pub arn: String,
72    pub creation_time: String,
73    pub description: String,
74    pub max_session_duration: String,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct IamGroup {
79    pub group_name: String,
80    pub path: String,
81    pub users: String,
82    pub permissions: String,
83    pub creation_time: String,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct Policy {
88    pub policy_name: String,
89    pub policy_type: String,
90    pub attached_via: String,
91    pub attached_entities: String,
92    pub description: String,
93    pub creation_time: String,
94    pub edited_time: String,
95    pub policy_arn: Option<String>,
96}
97
98#[derive(Debug, Clone)]
99pub struct RoleTag {
100    pub key: String,
101    pub value: String,
102}
103
104#[derive(Debug, Clone)]
105pub struct UserTag {
106    pub key: String,
107    pub value: String,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct UserGroup {
112    pub group_name: String,
113    pub attached_policies: String,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct GroupUser {
118    pub user_name: String,
119    pub groups: String,
120    pub last_activity: String,
121    pub creation_time: String,
122}
123
124#[derive(Debug, Clone)]
125pub struct LastAccessedService {
126    pub service: String,
127    pub policies_granting: String,
128    pub last_accessed: String,
129}
130
131#[derive(Debug, Clone, Copy)]
132pub enum UserColumn {
133    UserName,
134    Path,
135    Groups,
136    LastActivity,
137    Mfa,
138    PasswordAge,
139    ConsoleLastSignIn,
140    AccessKeyId,
141    ActiveKeyAge,
142    AccessKeyLastUsed,
143    Arn,
144    CreationTime,
145    ConsoleAccess,
146    SigningCerts,
147}
148
149#[derive(Debug, Clone, Copy)]
150pub enum GroupColumn {
151    GroupName,
152    Path,
153    Users,
154    Permissions,
155    CreationTime,
156}
157
158#[derive(Debug, Clone, Copy)]
159pub enum RoleColumn {
160    RoleName,
161    Path,
162    TrustedEntities,
163    LastActivity,
164    Arn,
165    CreationTime,
166    Description,
167    MaxSessionDuration,
168}
169
170#[derive(Debug, Clone, Copy)]
171pub enum GroupUserColumn {
172    UserName,
173    Groups,
174    LastActivity,
175    CreationTime,
176}
177
178impl UserColumn {
179    pub fn to_column(self) -> Box<dyn for<'a> crate::ui::table::Column<&'a IamUser>> {
180        use crate::ui::table::Column as TableColumn;
181        use ratatui::style::Style;
182
183        struct UserCol(UserColumn);
184        impl<'a> TableColumn<&'a IamUser> for UserCol {
185            fn name(&self) -> &str {
186                match self.0 {
187                    UserColumn::UserName => "User name",
188                    UserColumn::Path => "Path",
189                    UserColumn::Groups => "Groups",
190                    UserColumn::LastActivity => "Last activity",
191                    UserColumn::Mfa => "MFA",
192                    UserColumn::PasswordAge => "Password age",
193                    UserColumn::ConsoleLastSignIn => "Console last sign-in",
194                    UserColumn::AccessKeyId => "Access key ID",
195                    UserColumn::ActiveKeyAge => "Active key age",
196                    UserColumn::AccessKeyLastUsed => "Access key last used",
197                    UserColumn::Arn => "ARN",
198                    UserColumn::CreationTime => "Creation time",
199                    UserColumn::ConsoleAccess => "Console access",
200                    UserColumn::SigningCerts => "Signing certs",
201                }
202            }
203            fn width(&self) -> u16 {
204                let custom = match self.0 {
205                    UserColumn::UserName => 20,
206                    UserColumn::Path => 15,
207                    UserColumn::Groups => 20,
208                    UserColumn::LastActivity => 20,
209                    UserColumn::Mfa => 10,
210                    UserColumn::PasswordAge => 15,
211                    UserColumn::ConsoleLastSignIn => 25,
212                    UserColumn::AccessKeyId => 25,
213                    UserColumn::ActiveKeyAge => 18,
214                    UserColumn::AccessKeyLastUsed => UTC_TIMESTAMP_WIDTH as usize,
215                    UserColumn::Arn => 50,
216                    UserColumn::CreationTime => 30,
217                    UserColumn::ConsoleAccess => 15,
218                    UserColumn::SigningCerts => 15,
219                };
220                self.name().len().max(custom) as u16
221            }
222            fn render(&self, item: &&'a IamUser) -> (String, Style) {
223                let value = match self.0 {
224                    UserColumn::UserName => {
225                        return (item.user_name.clone(), Style::default());
226                    }
227                    UserColumn::Path => &item.path,
228                    UserColumn::Groups => &item.groups,
229                    UserColumn::LastActivity => &item.last_activity,
230                    UserColumn::Mfa => &item.mfa,
231                    UserColumn::PasswordAge => &item.password_age,
232                    UserColumn::ConsoleLastSignIn => &item.console_last_sign_in,
233                    UserColumn::AccessKeyId => &item.access_key_id,
234                    UserColumn::ActiveKeyAge => &item.active_key_age,
235                    UserColumn::AccessKeyLastUsed => &item.access_key_last_used,
236                    UserColumn::Arn => &item.arn,
237                    UserColumn::CreationTime => &item.creation_time,
238                    UserColumn::ConsoleAccess => &item.console_access,
239                    UserColumn::SigningCerts => &item.signing_certs,
240                };
241                (value.clone(), Style::default())
242            }
243        }
244        Box::new(UserCol(self))
245    }
246}
247
248impl GroupColumn {
249    pub fn to_column(self) -> Box<dyn crate::ui::table::Column<IamGroup>> {
250        use crate::ui::table::Column as TableColumn;
251        use ratatui::style::Style;
252
253        struct GroupCol(GroupColumn);
254        impl TableColumn<IamGroup> for GroupCol {
255            fn name(&self) -> &str {
256                match self.0 {
257                    GroupColumn::GroupName => "Group name",
258                    GroupColumn::Path => "Path",
259                    GroupColumn::Users => "Users",
260                    GroupColumn::Permissions => "Permissions",
261                    GroupColumn::CreationTime => "Creation time",
262                }
263            }
264            fn width(&self) -> u16 {
265                let custom = match self.0 {
266                    GroupColumn::GroupName => 20,
267                    GroupColumn::Path => 15,
268                    GroupColumn::Users => 10,
269                    GroupColumn::Permissions => 20,
270                    GroupColumn::CreationTime => 30,
271                };
272                self.name().len().max(custom) as u16
273            }
274            fn render(&self, item: &IamGroup) -> (String, Style) {
275                use ratatui::style::Color;
276                match self.0 {
277                    GroupColumn::GroupName => (item.group_name.clone(), Style::default()),
278                    GroupColumn::Permissions if item.permissions == "Defined" => (
279                        format!("✅ {}", item.permissions),
280                        Style::default().fg(Color::Green),
281                    ),
282                    GroupColumn::Path => (item.path.clone(), Style::default()),
283                    GroupColumn::Users => (item.users.clone(), Style::default()),
284                    GroupColumn::Permissions => (item.permissions.clone(), Style::default()),
285                    GroupColumn::CreationTime => (item.creation_time.clone(), Style::default()),
286                }
287            }
288        }
289        Box::new(GroupCol(self))
290    }
291}
292
293impl RoleColumn {
294    pub fn to_column(self) -> Box<dyn crate::ui::table::Column<IamRole>> {
295        use crate::ui::table::Column as TableColumn;
296        use ratatui::style::Style;
297
298        struct RoleCol(RoleColumn);
299        impl TableColumn<IamRole> for RoleCol {
300            fn name(&self) -> &str {
301                match self.0 {
302                    RoleColumn::RoleName => "Role name",
303                    RoleColumn::Path => "Path",
304                    RoleColumn::TrustedEntities => "Trusted entities",
305                    RoleColumn::LastActivity => "Last activity",
306                    RoleColumn::Arn => "ARN",
307                    RoleColumn::CreationTime => "Creation time",
308                    RoleColumn::Description => "Description",
309                    RoleColumn::MaxSessionDuration => "Max CLI/API session",
310                }
311            }
312            fn width(&self) -> u16 {
313                let custom = match self.0 {
314                    RoleColumn::RoleName => 30,
315                    RoleColumn::Path => 15,
316                    RoleColumn::TrustedEntities => 30,
317                    RoleColumn::LastActivity => 20,
318                    RoleColumn::Arn => 50,
319                    RoleColumn::CreationTime => 30,
320                    RoleColumn::Description => 40,
321                    RoleColumn::MaxSessionDuration => 22,
322                };
323                self.name().len().max(custom) as u16
324            }
325            fn render(&self, item: &IamRole) -> (String, Style) {
326                let value = match self.0 {
327                    RoleColumn::RoleName => {
328                        return (item.role_name.clone(), Style::default());
329                    }
330                    RoleColumn::Path => &item.path,
331                    RoleColumn::TrustedEntities => &item.trusted_entities,
332                    RoleColumn::LastActivity => &item.last_activity,
333                    RoleColumn::Arn => &item.arn,
334                    RoleColumn::CreationTime => &item.creation_time,
335                    RoleColumn::Description => &item.description,
336                    RoleColumn::MaxSessionDuration => &item.max_session_duration,
337                };
338                (value.clone(), Style::default())
339            }
340        }
341        Box::new(RoleCol(self))
342    }
343}
344
345impl GroupUserColumn {
346    pub fn to_column(self) -> Box<dyn crate::ui::table::Column<GroupUser>> {
347        use crate::ui::table::Column as TableColumn;
348        use ratatui::style::Style;
349
350        struct GroupUserCol(GroupUserColumn);
351        impl TableColumn<GroupUser> for GroupUserCol {
352            fn name(&self) -> &str {
353                match self.0 {
354                    GroupUserColumn::UserName => "User name",
355                    GroupUserColumn::Groups => "Groups",
356                    GroupUserColumn::LastActivity => "Last activity",
357                    GroupUserColumn::CreationTime => "Creation time",
358                }
359            }
360            fn width(&self) -> u16 {
361                let custom = match self.0 {
362                    GroupUserColumn::UserName => 20,
363                    GroupUserColumn::Groups => 20,
364                    GroupUserColumn::LastActivity => 20,
365                    GroupUserColumn::CreationTime => 30,
366                };
367                self.name().len().max(custom) as u16
368            }
369            fn render(&self, item: &GroupUser) -> (String, Style) {
370                match self.0 {
371                    GroupUserColumn::UserName => (item.user_name.clone(), Style::default()),
372                    GroupUserColumn::Groups => (item.groups.clone(), Style::default()),
373                    GroupUserColumn::LastActivity => (item.last_activity.clone(), Style::default()),
374                    GroupUserColumn::CreationTime => (item.creation_time.clone(), Style::default()),
375                }
376            }
377        }
378        Box::new(GroupUserCol(self))
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385    use crate::common::CyclicEnum;
386    use crate::ui::iam::{GroupTab, State, UserTab};
387
388    #[test]
389    fn test_user_group_creation() {
390        let group = UserGroup {
391            group_name: "Developers".to_string(),
392            attached_policies: "AmazonS3ReadOnlyAccess, AmazonEC2ReadOnlyAccess".to_string(),
393        };
394        assert_eq!(group.group_name, "Developers");
395        assert_eq!(
396            group.attached_policies,
397            "AmazonS3ReadOnlyAccess, AmazonEC2ReadOnlyAccess"
398        );
399    }
400
401    #[test]
402    fn test_iam_state_user_group_memberships_initialization() {
403        let state = State::new();
404        assert_eq!(state.user_group_memberships.items.len(), 0);
405        assert_eq!(state.user_group_memberships.selected, 0);
406        assert_eq!(state.user_group_memberships.filter, "");
407    }
408
409    #[test]
410    fn test_user_tab_groups() {
411        let tab = UserTab::Permissions;
412        assert_eq!(tab.next(), UserTab::Groups);
413        assert_eq!(UserTab::Groups.name(), "Groups");
414    }
415
416    #[test]
417    fn test_group_tab_navigation() {
418        let tab = GroupTab::Users;
419        assert_eq!(tab.next(), GroupTab::Permissions);
420        assert_eq!(tab.next().next(), GroupTab::AccessAdvisor);
421        assert_eq!(tab.next().next().next(), GroupTab::Users);
422    }
423
424    #[test]
425    fn test_group_tab_names() {
426        assert_eq!(GroupTab::Users.name(), "Users");
427        assert_eq!(GroupTab::Permissions.name(), "Permissions");
428        assert_eq!(GroupTab::AccessAdvisor.name(), "Access Advisor");
429    }
430
431    #[test]
432    fn test_iam_state_group_tab_initialization() {
433        let state = State::new();
434        assert_eq!(state.group_tab, GroupTab::Users);
435    }
436}