rusticity_term/
iam.rs

1use crate::common::t;
2use crate::common::{format_duration_seconds, ColumnId, UTC_TIMESTAMP_WIDTH};
3use crate::ui::table::Column;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7pub fn init(i18n: &mut HashMap<String, String>) {
8    for col in UserColumn::all() {
9        i18n.entry(col.id().to_string())
10            .or_insert_with(|| col.default_name().to_string());
11    }
12    for col in GroupColumn::all() {
13        i18n.entry(col.id().to_string())
14            .or_insert_with(|| col.default_name().to_string());
15    }
16    for col in RoleColumn::all() {
17        i18n.entry(col.id().to_string())
18            .or_insert_with(|| col.default_name().to_string());
19    }
20}
21
22pub fn format_arn(account_id: &str, resource_type: &str, resource_name: &str) -> String {
23    format!(
24        "arn:aws:iam::{}:{}/{}",
25        account_id, resource_type, resource_name
26    )
27}
28
29pub fn console_url_users(_region: &str) -> String {
30    "https://console.aws.amazon.com/iam/home#/users".to_string()
31}
32
33pub fn console_url_user_detail(region: &str, user_name: &str, section: &str) -> String {
34    format!(
35        "https://{}.console.aws.amazon.com/iam/home?region={}#/users/details/{}?section={}",
36        region, region, user_name, section
37    )
38}
39
40pub fn console_url_roles(_region: &str) -> String {
41    "https://console.aws.amazon.com/iam/home#/roles".to_string()
42}
43
44pub fn console_url_role_detail(region: &str, role_name: &str, section: &str) -> String {
45    format!(
46        "https://{}.console.aws.amazon.com/iam/home?region={}#/roles/details/{}?section={}",
47        region, region, role_name, section
48    )
49}
50
51pub fn console_url_role_policy(region: &str, role_name: &str, policy_name: &str) -> String {
52    format!(
53        "https://{}.console.aws.amazon.com/iam/home?region={}#/roles/details/{}/editPolicy/{}?step=addPermissions",
54        region, region, role_name, policy_name
55    )
56}
57
58pub fn console_url_groups(region: &str) -> String {
59    format!(
60        "https://{}.console.aws.amazon.com/iam/home?region={}#/groups",
61        region, region
62    )
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct IamUser {
67    pub user_name: String,
68    pub path: String,
69    pub groups: String,
70    pub last_activity: String,
71    pub mfa: String,
72    pub password_age: String,
73    pub console_last_sign_in: String,
74    pub access_key_id: String,
75    pub active_key_age: String,
76    pub access_key_last_used: String,
77    pub arn: String,
78    pub creation_time: String,
79    pub console_access: String,
80    pub signing_certs: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct IamRole {
85    pub role_name: String,
86    pub path: String,
87    pub trusted_entities: String,
88    pub last_activity: String,
89    pub arn: String,
90    pub creation_time: String,
91    pub description: String,
92    pub max_session_duration: Option<i32>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct IamGroup {
97    pub group_name: String,
98    pub path: String,
99    pub users: String,
100    pub permissions: String,
101    pub creation_time: String,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct Policy {
106    pub policy_name: String,
107    pub policy_type: String,
108    pub attached_via: String,
109    pub attached_entities: String,
110    pub description: String,
111    pub creation_time: String,
112    pub edited_time: String,
113    pub policy_arn: Option<String>,
114}
115
116#[derive(Debug, Clone)]
117pub struct RoleTag {
118    pub key: String,
119    pub value: String,
120}
121
122#[derive(Debug, Clone)]
123pub struct UserTag {
124    pub key: String,
125    pub value: String,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct UserGroup {
130    pub group_name: String,
131    pub attached_policies: String,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct GroupUser {
136    pub user_name: String,
137    pub groups: String,
138    pub last_activity: String,
139    pub creation_time: String,
140}
141
142#[derive(Debug, Clone)]
143pub struct LastAccessedService {
144    pub service: String,
145    pub policies_granting: String,
146    pub last_accessed: String,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq)]
150pub enum UserColumn {
151    UserName,
152    Path,
153    Groups,
154    LastActivity,
155    Mfa,
156    PasswordAge,
157    ConsoleLastSignIn,
158    AccessKeyId,
159    ActiveKeyAge,
160    AccessKeyLastUsed,
161    Arn,
162    CreationTime,
163    ConsoleAccess,
164    SigningCerts,
165}
166
167impl UserColumn {
168    pub fn from_id(id: ColumnId) -> Option<Self> {
169        match id {
170            "column.iam.user.user_name" => Some(Self::UserName),
171            "column.iam.user.path" => Some(Self::Path),
172            "column.iam.user.groups" => Some(Self::Groups),
173            "column.iam.user.last_activity" => Some(Self::LastActivity),
174            "column.iam.user.mfa" => Some(Self::Mfa),
175            "column.iam.user.password_age" => Some(Self::PasswordAge),
176            "column.iam.user.console_last_sign_in" => Some(Self::ConsoleLastSignIn),
177            "column.iam.user.access_key_id" => Some(Self::AccessKeyId),
178            "column.iam.user.active_key_age" => Some(Self::ActiveKeyAge),
179            "column.iam.user.access_key_last_used" => Some(Self::AccessKeyLastUsed),
180            "column.iam.user.arn" => Some(Self::Arn),
181            "column.iam.user.creation_time" => Some(Self::CreationTime),
182            "column.iam.user.console_access" => Some(Self::ConsoleAccess),
183            "column.iam.user.signing_certs" => Some(Self::SigningCerts),
184            _ => None,
185        }
186    }
187
188    pub fn all() -> [UserColumn; 14] {
189        [
190            Self::UserName,
191            Self::Path,
192            Self::Groups,
193            Self::LastActivity,
194            Self::Mfa,
195            Self::PasswordAge,
196            Self::ConsoleLastSignIn,
197            Self::AccessKeyId,
198            Self::ActiveKeyAge,
199            Self::AccessKeyLastUsed,
200            Self::Arn,
201            Self::CreationTime,
202            Self::ConsoleAccess,
203            Self::SigningCerts,
204        ]
205    }
206
207    pub fn ids() -> Vec<ColumnId> {
208        Self::all().iter().map(|c| c.id()).collect()
209    }
210
211    pub fn visible() -> Vec<ColumnId> {
212        vec![
213            Self::UserName.id(),
214            Self::Path.id(),
215            Self::Groups.id(),
216            Self::LastActivity.id(),
217            Self::Mfa.id(),
218            Self::PasswordAge.id(),
219            Self::ConsoleLastSignIn.id(),
220            Self::AccessKeyId.id(),
221            Self::ActiveKeyAge.id(),
222            Self::AccessKeyLastUsed.id(),
223            Self::Arn.id(),
224        ]
225    }
226}
227
228#[derive(Debug, Clone, Copy)]
229pub enum GroupColumn {
230    GroupName,
231    Path,
232    Users,
233    Permissions,
234    CreationTime,
235}
236
237impl GroupColumn {
238    pub fn all() -> [GroupColumn; 5] {
239        [
240            Self::GroupName,
241            Self::Path,
242            Self::Users,
243            Self::Permissions,
244            Self::CreationTime,
245        ]
246    }
247}
248
249#[derive(Debug, Clone, Copy)]
250pub enum RoleColumn {
251    RoleName,
252    Path,
253    TrustedEntities,
254    LastActivity,
255    Arn,
256    CreationTime,
257    Description,
258    MaxSessionDuration,
259}
260
261impl RoleColumn {
262    pub fn id(&self) -> ColumnId {
263        match self {
264            Self::RoleName => "column.iam.role.role_name",
265            Self::Path => "column.iam.role.path",
266            Self::TrustedEntities => "column.iam.role.trusted_entities",
267            Self::LastActivity => "column.iam.role.last_activity",
268            Self::Arn => "column.iam.role.arn",
269            Self::CreationTime => "column.iam.role.creation_time",
270            Self::Description => "column.iam.role.description",
271            Self::MaxSessionDuration => "column.iam.role.max_session_duration",
272        }
273    }
274
275    pub fn default_name(&self) -> &'static str {
276        match self {
277            Self::RoleName => "Role name",
278            Self::Path => "Path",
279            Self::TrustedEntities => "Trusted entities",
280            Self::LastActivity => "Last activity",
281            Self::Arn => "ARN",
282            Self::CreationTime => "Creation time",
283            Self::Description => "Description",
284            Self::MaxSessionDuration => "Max session duration",
285        }
286    }
287
288    pub fn all() -> [RoleColumn; 8] {
289        [
290            Self::RoleName,
291            Self::Path,
292            Self::TrustedEntities,
293            Self::LastActivity,
294            Self::Arn,
295            Self::CreationTime,
296            Self::Description,
297            Self::MaxSessionDuration,
298        ]
299    }
300}
301
302#[derive(Debug, Clone, Copy)]
303pub enum GroupUserColumn {
304    UserName,
305    Groups,
306    LastActivity,
307    CreationTime,
308}
309
310#[derive(Debug, Clone, Copy)]
311pub enum PolicyColumn {
312    PolicyName,
313    Type,
314    AttachedVia,
315    AttachedEntities,
316    Description,
317    CreationTime,
318    EditedTime,
319}
320
321#[derive(Debug, Clone, Copy)]
322pub enum TagColumn {
323    Key,
324    Value,
325}
326
327#[derive(Debug, Clone, Copy)]
328pub enum UserGroupColumn {
329    GroupName,
330    AttachedPolicies,
331}
332
333#[derive(Debug, Clone, Copy)]
334pub enum LastAccessedServiceColumn {
335    Service,
336    PoliciesGranting,
337    LastAccessed,
338}
339
340impl<'a> Column<&'a IamUser> for UserColumn {
341    fn id(&self) -> &'static str {
342        match self {
343            Self::UserName => "column.iam.user.user_name",
344            Self::Path => "column.iam.user.path",
345            Self::Groups => "column.iam.user.groups",
346            Self::LastActivity => "column.iam.user.last_activity",
347            Self::Mfa => "column.iam.user.mfa",
348            Self::PasswordAge => "column.iam.user.password_age",
349            Self::ConsoleLastSignIn => "column.iam.user.console_last_sign_in",
350            Self::AccessKeyId => "column.iam.user.access_key_id",
351            Self::ActiveKeyAge => "column.iam.user.active_key_age",
352            Self::AccessKeyLastUsed => "column.iam.user.access_key_last_used",
353            Self::Arn => "column.iam.user.arn",
354            Self::CreationTime => "column.iam.user.creation_time",
355            Self::ConsoleAccess => "column.iam.user.console_access",
356            Self::SigningCerts => "column.iam.user.signing_certs",
357        }
358    }
359
360    fn default_name(&self) -> &'static str {
361        match self {
362            Self::UserName => "User name",
363            Self::Path => "Path",
364            Self::Groups => "Groups",
365            Self::LastActivity => "Last activity",
366            Self::Mfa => "MFA",
367            Self::PasswordAge => "Password age",
368            Self::ConsoleLastSignIn => "Console last sign-in",
369            Self::AccessKeyId => "Access key ID",
370            Self::ActiveKeyAge => "Active key age",
371            Self::AccessKeyLastUsed => "Access key last used",
372            Self::Arn => "ARN",
373            Self::CreationTime => "Creation time",
374            Self::ConsoleAccess => "Console access",
375            Self::SigningCerts => "Signing certificates",
376        }
377    }
378
379    fn name(&self) -> &str {
380        let key = self.id();
381        let translated = t(key);
382        if translated == key {
383            self.default_name()
384        } else {
385            Box::leak(translated.into_boxed_str())
386        }
387    }
388
389    fn width(&self) -> u16 {
390        let custom = match self {
391            Self::UserName => 20,
392            Self::Path => 15,
393            Self::Groups => 20,
394            Self::LastActivity => 20,
395            Self::Mfa => 10,
396            Self::PasswordAge => 15,
397            Self::ConsoleLastSignIn => 25,
398            Self::AccessKeyId => 25,
399            Self::ActiveKeyAge => 18,
400            Self::AccessKeyLastUsed => UTC_TIMESTAMP_WIDTH as usize,
401            Self::Arn => 50,
402            Self::CreationTime => 30,
403            Self::ConsoleAccess => 15,
404            Self::SigningCerts => 15,
405        };
406        self.name().len().max(custom) as u16
407    }
408
409    fn render(&self, item: &&'a IamUser) -> (String, ratatui::style::Style) {
410        let text = match self {
411            Self::UserName => item.user_name.clone(),
412            Self::Path => item.path.clone(),
413            Self::Groups => item.groups.clone(),
414            Self::LastActivity => item.last_activity.clone(),
415            Self::Mfa => item.mfa.clone(),
416            Self::PasswordAge => item.password_age.clone(),
417            Self::ConsoleLastSignIn => item.console_last_sign_in.clone(),
418            Self::AccessKeyId => item.access_key_id.clone(),
419            Self::ActiveKeyAge => item.active_key_age.clone(),
420            Self::AccessKeyLastUsed => item.access_key_last_used.clone(),
421            Self::Arn => item.arn.clone(),
422            Self::CreationTime => item.creation_time.clone(),
423            Self::ConsoleAccess => item.console_access.clone(),
424            Self::SigningCerts => item.signing_certs.clone(),
425        };
426        (text, ratatui::style::Style::default())
427    }
428}
429
430impl Column<IamGroup> for GroupColumn {
431    fn id(&self) -> &'static str {
432        match self {
433            Self::GroupName => "column.iam.group.group_name",
434            Self::Path => "column.iam.group.path",
435            Self::Users => "column.iam.group.users",
436            Self::Permissions => "column.iam.group.permissions",
437            Self::CreationTime => "column.iam.group.creation_time",
438        }
439    }
440
441    fn default_name(&self) -> &'static str {
442        match self {
443            Self::GroupName => "Group name",
444            Self::Path => "Path",
445            Self::Users => "Users",
446            Self::Permissions => "Permissions",
447            Self::CreationTime => "Creation time",
448        }
449    }
450
451    fn width(&self) -> u16 {
452        let custom = match self {
453            Self::GroupName => 20,
454            Self::Path => 15,
455            Self::Users => 10,
456            Self::Permissions => 20,
457            Self::CreationTime => 30,
458        };
459        self.name().len().max(custom) as u16
460    }
461
462    fn render(&self, item: &IamGroup) -> (String, ratatui::style::Style) {
463        use ratatui::style::{Color, Style};
464        match self {
465            Self::GroupName => (item.group_name.clone(), Style::default()),
466            Self::Permissions if item.permissions == "Defined" => (
467                format!("✅ {}", item.permissions),
468                Style::default().fg(Color::Green),
469            ),
470            Self::Path => (item.path.clone(), Style::default()),
471            Self::Users => (item.users.clone(), Style::default()),
472            Self::Permissions => (item.permissions.clone(), Style::default()),
473            Self::CreationTime => (item.creation_time.clone(), Style::default()),
474        }
475    }
476}
477
478impl Column<IamRole> for RoleColumn {
479    fn name(&self) -> &str {
480        match self {
481            Self::RoleName => "Role name",
482            Self::Path => "Path",
483            Self::TrustedEntities => "Trusted entities",
484            Self::LastActivity => "Last activity",
485            Self::Arn => "ARN",
486            Self::CreationTime => "Creation time",
487            Self::Description => "Description",
488            Self::MaxSessionDuration => "Max CLI/API session",
489        }
490    }
491
492    fn width(&self) -> u16 {
493        let custom = match self {
494            Self::RoleName => 30,
495            Self::Path => 15,
496            Self::TrustedEntities => 30,
497            Self::LastActivity => 20,
498            Self::Arn => 50,
499            Self::CreationTime => 30,
500            Self::Description => 40,
501            Self::MaxSessionDuration => 22,
502        };
503        self.name().len().max(custom) as u16
504    }
505
506    fn render(&self, item: &IamRole) -> (String, ratatui::style::Style) {
507        let text = match self {
508            Self::RoleName => item.role_name.clone(),
509            Self::Path => item.path.clone(),
510            Self::TrustedEntities => item.trusted_entities.clone(),
511            Self::LastActivity => item.last_activity.clone(),
512            Self::Arn => item.arn.clone(),
513            Self::CreationTime => item.creation_time.clone(),
514            Self::Description => item.description.clone(),
515            Self::MaxSessionDuration => item
516                .max_session_duration
517                .map(format_duration_seconds)
518                .unwrap_or_default(),
519        };
520        (text, ratatui::style::Style::default())
521    }
522}
523
524impl Column<GroupUser> for GroupUserColumn {
525    fn name(&self) -> &str {
526        match self {
527            Self::UserName => "User name",
528            Self::Groups => "Groups",
529            Self::LastActivity => "Last activity",
530            Self::CreationTime => "Creation time",
531        }
532    }
533
534    fn width(&self) -> u16 {
535        let custom = match self {
536            Self::UserName => 20,
537            Self::Groups => 20,
538            Self::LastActivity => 20,
539            Self::CreationTime => 30,
540        };
541        self.name().len().max(custom) as u16
542    }
543
544    fn render(&self, item: &GroupUser) -> (String, ratatui::style::Style) {
545        let text = match self {
546            Self::UserName => item.user_name.clone(),
547            Self::Groups => item.groups.clone(),
548            Self::LastActivity => item.last_activity.clone(),
549            Self::CreationTime => item.creation_time.clone(),
550        };
551        (text, ratatui::style::Style::default())
552    }
553}
554
555impl Column<Policy> for PolicyColumn {
556    fn name(&self) -> &str {
557        match self {
558            Self::PolicyName => "Policy name",
559            Self::Type => "Type",
560            Self::AttachedVia => "Attached via",
561            Self::AttachedEntities => "Attached entities",
562            Self::Description => "Description",
563            Self::CreationTime => "Creation time",
564            Self::EditedTime => "Edited time",
565        }
566    }
567
568    fn width(&self) -> u16 {
569        match self {
570            Self::PolicyName => 30,
571            Self::Type => 15,
572            Self::AttachedVia => 20,
573            Self::AttachedEntities => 20,
574            Self::Description => 40,
575            Self::CreationTime => 30,
576            Self::EditedTime => 30,
577        }
578    }
579
580    fn render(&self, item: &Policy) -> (String, ratatui::style::Style) {
581        let text = match self {
582            Self::PolicyName => item.policy_name.clone(),
583            Self::Type => item.policy_type.clone(),
584            Self::AttachedVia => item.attached_via.clone(),
585            Self::AttachedEntities => item.attached_entities.clone(),
586            Self::Description => item.description.clone(),
587            Self::CreationTime => item.creation_time.clone(),
588            Self::EditedTime => item.edited_time.clone(),
589        };
590        (text, ratatui::style::Style::default())
591    }
592}
593
594impl Column<RoleTag> for TagColumn {
595    fn name(&self) -> &str {
596        match self {
597            Self::Key => "Key",
598            Self::Value => "Value",
599        }
600    }
601
602    fn width(&self) -> u16 {
603        match self {
604            Self::Key => 30,
605            Self::Value => 70,
606        }
607    }
608
609    fn render(&self, item: &RoleTag) -> (String, ratatui::style::Style) {
610        let text = match self {
611            Self::Key => item.key.clone(),
612            Self::Value => item.value.clone(),
613        };
614        (text, ratatui::style::Style::default())
615    }
616}
617
618impl Column<UserTag> for TagColumn {
619    fn name(&self) -> &str {
620        match self {
621            Self::Key => "Key",
622            Self::Value => "Value",
623        }
624    }
625
626    fn width(&self) -> u16 {
627        match self {
628            Self::Key => 30,
629            Self::Value => 70,
630        }
631    }
632
633    fn render(&self, item: &UserTag) -> (String, ratatui::style::Style) {
634        let text = match self {
635            Self::Key => item.key.clone(),
636            Self::Value => item.value.clone(),
637        };
638        (text, ratatui::style::Style::default())
639    }
640}
641
642impl Column<UserGroup> for UserGroupColumn {
643    fn name(&self) -> &str {
644        match self {
645            Self::GroupName => "Group name",
646            Self::AttachedPolicies => "Attached policies",
647        }
648    }
649
650    fn width(&self) -> u16 {
651        match self {
652            Self::GroupName => 40,
653            Self::AttachedPolicies => 60,
654        }
655    }
656
657    fn render(&self, item: &UserGroup) -> (String, ratatui::style::Style) {
658        let text = match self {
659            Self::GroupName => item.group_name.clone(),
660            Self::AttachedPolicies => item.attached_policies.clone(),
661        };
662        (text, ratatui::style::Style::default())
663    }
664}
665
666impl Column<LastAccessedService> for LastAccessedServiceColumn {
667    fn name(&self) -> &str {
668        match self {
669            Self::Service => "Service",
670            Self::PoliciesGranting => "Policies granting permissions",
671            Self::LastAccessed => "Last accessed",
672        }
673    }
674
675    fn width(&self) -> u16 {
676        match self {
677            Self::Service => 30,
678            Self::PoliciesGranting => 40,
679            Self::LastAccessed => 30,
680        }
681    }
682
683    fn render(&self, item: &LastAccessedService) -> (String, ratatui::style::Style) {
684        let text = match self {
685            Self::Service => item.service.clone(),
686            Self::PoliciesGranting => item.policies_granting.clone(),
687            Self::LastAccessed => item.last_accessed.clone(),
688        };
689        (text, ratatui::style::Style::default())
690    }
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696    use crate::common::CyclicEnum;
697    use crate::ui::iam::{GroupTab, State, UserTab};
698
699    #[test]
700    fn test_user_group_creation() {
701        let group = UserGroup {
702            group_name: "Developers".to_string(),
703            attached_policies: "AmazonS3ReadOnlyAccess, AmazonEC2ReadOnlyAccess".to_string(),
704        };
705        assert_eq!(group.group_name, "Developers");
706        assert_eq!(
707            group.attached_policies,
708            "AmazonS3ReadOnlyAccess, AmazonEC2ReadOnlyAccess"
709        );
710    }
711
712    #[test]
713    fn test_iam_state_user_group_memberships_initialization() {
714        let state = State::new();
715        assert_eq!(state.user_group_memberships.items.len(), 0);
716        assert_eq!(state.user_group_memberships.selected, 0);
717        assert_eq!(state.user_group_memberships.filter, "");
718    }
719
720    #[test]
721    fn test_user_tab_groups() {
722        let tab = UserTab::Permissions;
723        assert_eq!(tab.next(), UserTab::Groups);
724        assert_eq!(UserTab::Groups.name(), "Groups");
725    }
726
727    #[test]
728    fn test_group_tab_navigation() {
729        let tab = GroupTab::Users;
730        assert_eq!(tab.next(), GroupTab::Permissions);
731        assert_eq!(tab.next().next(), GroupTab::AccessAdvisor);
732        assert_eq!(tab.next().next().next(), GroupTab::Users);
733    }
734
735    #[test]
736    fn test_group_tab_names() {
737        assert_eq!(GroupTab::Users.name(), "Users");
738        assert_eq!(GroupTab::Permissions.name(), "Permissions");
739        assert_eq!(GroupTab::AccessAdvisor.name(), "Access Advisor");
740    }
741
742    #[test]
743    fn test_iam_state_group_tab_initialization() {
744        let state = State::new();
745        assert_eq!(state.group_tab, GroupTab::Users);
746    }
747}