Skip to main content

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    const ID_USER_NAME: &'static str = "column.iam.user.user_name";
169    const ID_PATH: &'static str = "column.iam.user.path";
170    const ID_GROUPS: &'static str = "column.iam.user.groups";
171    const ID_LAST_ACTIVITY: &'static str = "column.iam.user.last_activity";
172    const ID_MFA: &'static str = "column.iam.user.mfa";
173    const ID_PASSWORD_AGE: &'static str = "column.iam.user.password_age";
174    const ID_CONSOLE_LAST_SIGN_IN: &'static str = "column.iam.user.console_last_sign_in";
175    const ID_ACCESS_KEY_ID: &'static str = "column.iam.user.access_key_id";
176    const ID_ACTIVE_KEY_AGE: &'static str = "column.iam.user.active_key_age";
177    const ID_ACCESS_KEY_LAST_USED: &'static str = "column.iam.user.access_key_last_used";
178    const ID_ARN: &'static str = "column.iam.user.arn";
179    const ID_CREATION_TIME: &'static str = "column.iam.user.creation_time";
180    const ID_CONSOLE_ACCESS: &'static str = "column.iam.user.console_access";
181    const ID_SIGNING_CERTS: &'static str = "column.iam.user.signing_certs";
182
183    pub const fn id(&self) -> &'static str {
184        match self {
185            Self::UserName => Self::ID_USER_NAME,
186            Self::Path => Self::ID_PATH,
187            Self::Groups => Self::ID_GROUPS,
188            Self::LastActivity => Self::ID_LAST_ACTIVITY,
189            Self::Mfa => Self::ID_MFA,
190            Self::PasswordAge => Self::ID_PASSWORD_AGE,
191            Self::ConsoleLastSignIn => Self::ID_CONSOLE_LAST_SIGN_IN,
192            Self::AccessKeyId => Self::ID_ACCESS_KEY_ID,
193            Self::ActiveKeyAge => Self::ID_ACTIVE_KEY_AGE,
194            Self::AccessKeyLastUsed => Self::ID_ACCESS_KEY_LAST_USED,
195            Self::Arn => Self::ID_ARN,
196            Self::CreationTime => Self::ID_CREATION_TIME,
197            Self::ConsoleAccess => Self::ID_CONSOLE_ACCESS,
198            Self::SigningCerts => Self::ID_SIGNING_CERTS,
199        }
200    }
201
202    pub const fn default_name(&self) -> &'static str {
203        match self {
204            Self::UserName => "User name",
205            Self::Path => "Path",
206            Self::Groups => "Groups",
207            Self::LastActivity => "Last activity",
208            Self::Mfa => "MFA",
209            Self::PasswordAge => "Password age",
210            Self::ConsoleLastSignIn => "Console last sign-in",
211            Self::AccessKeyId => "Access key ID",
212            Self::ActiveKeyAge => "Active key age",
213            Self::AccessKeyLastUsed => "Access key last used",
214            Self::Arn => "ARN",
215            Self::CreationTime => "Creation time",
216            Self::ConsoleAccess => "Console access",
217            Self::SigningCerts => "Signing certificates",
218        }
219    }
220
221    pub fn name(&self) -> String {
222        let key = self.id();
223        let translated = t(key);
224        if translated == key {
225            self.default_name().to_string()
226        } else {
227            translated
228        }
229    }
230
231    pub fn from_id(id: &str) -> Option<Self> {
232        match id {
233            Self::ID_USER_NAME => Some(Self::UserName),
234            Self::ID_PATH => Some(Self::Path),
235            Self::ID_GROUPS => Some(Self::Groups),
236            Self::ID_LAST_ACTIVITY => Some(Self::LastActivity),
237            Self::ID_MFA => Some(Self::Mfa),
238            Self::ID_PASSWORD_AGE => Some(Self::PasswordAge),
239            Self::ID_CONSOLE_LAST_SIGN_IN => Some(Self::ConsoleLastSignIn),
240            Self::ID_ACCESS_KEY_ID => Some(Self::AccessKeyId),
241            Self::ID_ACTIVE_KEY_AGE => Some(Self::ActiveKeyAge),
242            Self::ID_ACCESS_KEY_LAST_USED => Some(Self::AccessKeyLastUsed),
243            Self::ID_ARN => Some(Self::Arn),
244            Self::ID_CREATION_TIME => Some(Self::CreationTime),
245            Self::ID_CONSOLE_ACCESS => Some(Self::ConsoleAccess),
246            Self::ID_SIGNING_CERTS => Some(Self::SigningCerts),
247            _ => None,
248        }
249    }
250
251    pub fn all() -> [UserColumn; 14] {
252        [
253            Self::UserName,
254            Self::Path,
255            Self::Groups,
256            Self::LastActivity,
257            Self::Mfa,
258            Self::PasswordAge,
259            Self::ConsoleLastSignIn,
260            Self::AccessKeyId,
261            Self::ActiveKeyAge,
262            Self::AccessKeyLastUsed,
263            Self::Arn,
264            Self::CreationTime,
265            Self::ConsoleAccess,
266            Self::SigningCerts,
267        ]
268    }
269
270    pub fn ids() -> Vec<ColumnId> {
271        Self::all().iter().map(|c| c.id()).collect()
272    }
273
274    pub fn visible() -> Vec<ColumnId> {
275        vec![
276            Self::UserName.id(),
277            Self::Path.id(),
278            Self::Groups.id(),
279            Self::LastActivity.id(),
280            Self::Mfa.id(),
281            Self::PasswordAge.id(),
282            Self::ConsoleLastSignIn.id(),
283            Self::AccessKeyId.id(),
284            Self::ActiveKeyAge.id(),
285            Self::AccessKeyLastUsed.id(),
286            Self::Arn.id(),
287        ]
288    }
289}
290
291#[derive(Debug, Clone, Copy)]
292pub enum GroupColumn {
293    GroupName,
294    Path,
295    Users,
296    Permissions,
297    CreationTime,
298}
299
300impl GroupColumn {
301    pub fn all() -> [GroupColumn; 5] {
302        [
303            Self::GroupName,
304            Self::Path,
305            Self::Users,
306            Self::Permissions,
307            Self::CreationTime,
308        ]
309    }
310}
311
312#[derive(Debug, Clone, Copy)]
313pub enum RoleColumn {
314    RoleName,
315    Path,
316    TrustedEntities,
317    LastActivity,
318    Arn,
319    CreationTime,
320    Description,
321    MaxSessionDuration,
322}
323
324impl RoleColumn {
325    const ID_ROLE_NAME: &'static str = "column.iam.role.role_name";
326    const ID_PATH: &'static str = "column.iam.role.path";
327    const ID_TRUSTED_ENTITIES: &'static str = "column.iam.role.trusted_entities";
328    const ID_LAST_ACTIVITY: &'static str = "column.iam.role.last_activity";
329    const ID_ARN: &'static str = "column.iam.role.arn";
330    const ID_CREATION_TIME: &'static str = "column.iam.role.creation_time";
331    const ID_DESCRIPTION: &'static str = "column.iam.role.description";
332    const ID_MAX_SESSION_DURATION: &'static str = "column.iam.role.max_session_duration";
333
334    pub fn from_id(id: &str) -> Option<Self> {
335        match id {
336            Self::ID_ROLE_NAME => Some(Self::RoleName),
337            Self::ID_PATH => Some(Self::Path),
338            Self::ID_TRUSTED_ENTITIES => Some(Self::TrustedEntities),
339            Self::ID_LAST_ACTIVITY => Some(Self::LastActivity),
340            Self::ID_ARN => Some(Self::Arn),
341            Self::ID_CREATION_TIME => Some(Self::CreationTime),
342            Self::ID_DESCRIPTION => Some(Self::Description),
343            Self::ID_MAX_SESSION_DURATION => Some(Self::MaxSessionDuration),
344            _ => None,
345        }
346    }
347
348    pub const fn id(&self) -> ColumnId {
349        match self {
350            Self::RoleName => Self::ID_ROLE_NAME,
351            Self::Path => Self::ID_PATH,
352            Self::TrustedEntities => Self::ID_TRUSTED_ENTITIES,
353            Self::LastActivity => Self::ID_LAST_ACTIVITY,
354            Self::Arn => Self::ID_ARN,
355            Self::CreationTime => Self::ID_CREATION_TIME,
356            Self::Description => Self::ID_DESCRIPTION,
357            Self::MaxSessionDuration => Self::ID_MAX_SESSION_DURATION,
358        }
359    }
360
361    pub fn default_name(&self) -> &'static str {
362        match self {
363            Self::RoleName => "Role name",
364            Self::Path => "Path",
365            Self::TrustedEntities => "Trusted entities",
366            Self::LastActivity => "Last activity",
367            Self::Arn => "ARN",
368            Self::CreationTime => "Creation time",
369            Self::Description => "Description",
370            Self::MaxSessionDuration => "Max session duration",
371        }
372    }
373
374    pub fn name(&self) -> String {
375        let key = self.id();
376        let translated = t(key);
377        if translated == key {
378            self.default_name().to_string()
379        } else {
380            translated
381        }
382    }
383
384    pub fn all() -> [RoleColumn; 8] {
385        [
386            Self::RoleName,
387            Self::Path,
388            Self::TrustedEntities,
389            Self::LastActivity,
390            Self::Arn,
391            Self::CreationTime,
392            Self::Description,
393            Self::MaxSessionDuration,
394        ]
395    }
396
397    pub fn ids() -> Vec<ColumnId> {
398        Self::all().iter().map(|c| c.id()).collect()
399    }
400
401    pub fn visible() -> Vec<ColumnId> {
402        vec![
403            Self::RoleName.id(),
404            Self::TrustedEntities.id(),
405            Self::CreationTime.id(),
406        ]
407    }
408}
409
410#[derive(Debug, Clone, Copy)]
411pub enum GroupUserColumn {
412    UserName,
413    Groups,
414    LastActivity,
415    CreationTime,
416}
417
418#[derive(Debug, Clone, Copy)]
419pub enum PolicyColumn {
420    PolicyName,
421    Type,
422    AttachedVia,
423    AttachedEntities,
424    Description,
425    CreationTime,
426    EditedTime,
427}
428
429#[derive(Debug, Clone, Copy)]
430pub enum TagColumn {
431    Key,
432    Value,
433}
434
435#[derive(Debug, Clone, Copy)]
436pub enum UserGroupColumn {
437    GroupName,
438    AttachedPolicies,
439}
440
441#[derive(Debug, Clone, Copy)]
442pub enum LastAccessedServiceColumn {
443    Service,
444    PoliciesGranting,
445    LastAccessed,
446}
447
448impl<'a> Column<&'a IamUser> for UserColumn {
449    fn id(&self) -> &'static str {
450        UserColumn::id(self)
451    }
452
453    fn default_name(&self) -> &'static str {
454        match self {
455            Self::UserName => "User name",
456            Self::Path => "Path",
457            Self::Groups => "Groups",
458            Self::LastActivity => "Last activity",
459            Self::Mfa => "MFA",
460            Self::PasswordAge => "Password age",
461            Self::ConsoleLastSignIn => "Console last sign-in",
462            Self::AccessKeyId => "Access key ID",
463            Self::ActiveKeyAge => "Active key age",
464            Self::AccessKeyLastUsed => "Access key last used",
465            Self::Arn => "ARN",
466            Self::CreationTime => "Creation time",
467            Self::ConsoleAccess => "Console access",
468            Self::SigningCerts => "Signing certificates",
469        }
470    }
471
472    fn name(&self) -> &str {
473        let key = self.id();
474        let translated = t(key);
475        if translated == key {
476            self.default_name()
477        } else {
478            Box::leak(translated.into_boxed_str())
479        }
480    }
481
482    fn width(&self) -> u16 {
483        let custom = match self {
484            Self::UserName => 20,
485            Self::Path => 15,
486            Self::Groups => 20,
487            Self::LastActivity => 20,
488            Self::Mfa => 10,
489            Self::PasswordAge => 15,
490            Self::ConsoleLastSignIn => 25,
491            Self::AccessKeyId => 25,
492            Self::ActiveKeyAge => 18,
493            Self::AccessKeyLastUsed => UTC_TIMESTAMP_WIDTH as usize,
494            Self::Arn => 50,
495            Self::CreationTime => 30,
496            Self::ConsoleAccess => 15,
497            Self::SigningCerts => 15,
498        };
499        self.name().len().max(custom) as u16
500    }
501
502    fn render(&self, item: &&'a IamUser) -> (String, ratatui::style::Style) {
503        let text = match self {
504            Self::UserName => item.user_name.clone(),
505            Self::Path => item.path.clone(),
506            Self::Groups => item.groups.clone(),
507            Self::LastActivity => item.last_activity.clone(),
508            Self::Mfa => item.mfa.clone(),
509            Self::PasswordAge => item.password_age.clone(),
510            Self::ConsoleLastSignIn => item.console_last_sign_in.clone(),
511            Self::AccessKeyId => item.access_key_id.clone(),
512            Self::ActiveKeyAge => item.active_key_age.clone(),
513            Self::AccessKeyLastUsed => item.access_key_last_used.clone(),
514            Self::Arn => item.arn.clone(),
515            Self::CreationTime => item.creation_time.clone(),
516            Self::ConsoleAccess => item.console_access.clone(),
517            Self::SigningCerts => item.signing_certs.clone(),
518        };
519        (text, ratatui::style::Style::default())
520    }
521}
522
523impl Column<IamGroup> for GroupColumn {
524    fn id(&self) -> &'static str {
525        match self {
526            Self::GroupName => "column.iam.group.group_name",
527            Self::Path => "column.iam.group.path",
528            Self::Users => "column.iam.group.users",
529            Self::Permissions => "column.iam.group.permissions",
530            Self::CreationTime => "column.iam.group.creation_time",
531        }
532    }
533
534    fn default_name(&self) -> &'static str {
535        match self {
536            Self::GroupName => "Group name",
537            Self::Path => "Path",
538            Self::Users => "Users",
539            Self::Permissions => "Permissions",
540            Self::CreationTime => "Creation time",
541        }
542    }
543
544    fn width(&self) -> u16 {
545        let custom = match self {
546            Self::GroupName => 20,
547            Self::Path => 15,
548            Self::Users => 10,
549            Self::Permissions => 20,
550            Self::CreationTime => 30,
551        };
552        self.name().len().max(custom) as u16
553    }
554
555    fn render(&self, item: &IamGroup) -> (String, ratatui::style::Style) {
556        use ratatui::style::{Color, Style};
557        match self {
558            Self::GroupName => (item.group_name.clone(), Style::default()),
559            Self::Permissions if item.permissions == "Defined" => (
560                format!("✅ {}", item.permissions),
561                Style::default().fg(Color::Green),
562            ),
563            Self::Path => (item.path.clone(), Style::default()),
564            Self::Users => (item.users.clone(), Style::default()),
565            Self::Permissions => (item.permissions.clone(), Style::default()),
566            Self::CreationTime => (item.creation_time.clone(), Style::default()),
567        }
568    }
569}
570
571impl Column<IamRole> for RoleColumn {
572    fn id(&self) -> &'static str {
573        match self {
574            Self::RoleName => "column.iam.role.role_name",
575            Self::Path => "column.iam.role.path",
576            Self::TrustedEntities => "column.iam.role.trusted_entities",
577            Self::LastActivity => "column.iam.role.last_activity",
578            Self::Arn => "column.iam.role.arn",
579            Self::CreationTime => "column.iam.role.creation_time",
580            Self::Description => "column.iam.role.description",
581            Self::MaxSessionDuration => "column.iam.role.max_session_duration",
582        }
583    }
584
585    fn default_name(&self) -> &'static str {
586        match self {
587            Self::RoleName => "Role name",
588            Self::Path => "Path",
589            Self::TrustedEntities => "Trusted entities",
590            Self::LastActivity => "Last activity",
591            Self::Arn => "ARN",
592            Self::CreationTime => "Creation time",
593            Self::Description => "Description",
594            Self::MaxSessionDuration => "Max CLI/API session",
595        }
596    }
597
598    fn width(&self) -> u16 {
599        let custom = match self {
600            Self::RoleName => 30,
601            Self::Path => 15,
602            Self::TrustedEntities => 30,
603            Self::LastActivity => 20,
604            Self::Arn => 50,
605            Self::CreationTime => 30,
606            Self::Description => 40,
607            Self::MaxSessionDuration => 22,
608        };
609        self.name().len().max(custom) as u16
610    }
611
612    fn render(&self, item: &IamRole) -> (String, ratatui::style::Style) {
613        let text = match self {
614            Self::RoleName => item.role_name.clone(),
615            Self::Path => item.path.clone(),
616            Self::TrustedEntities => item.trusted_entities.clone(),
617            Self::LastActivity => item.last_activity.clone(),
618            Self::Arn => item.arn.clone(),
619            Self::CreationTime => item.creation_time.clone(),
620            Self::Description => item.description.clone(),
621            Self::MaxSessionDuration => item
622                .max_session_duration
623                .map(format_duration_seconds)
624                .unwrap_or_default(),
625        };
626        (text, ratatui::style::Style::default())
627    }
628}
629
630impl Column<GroupUser> for GroupUserColumn {
631    fn name(&self) -> &str {
632        match self {
633            Self::UserName => "User name",
634            Self::Groups => "Groups",
635            Self::LastActivity => "Last activity",
636            Self::CreationTime => "Creation time",
637        }
638    }
639
640    fn width(&self) -> u16 {
641        let custom = match self {
642            Self::UserName => 20,
643            Self::Groups => 20,
644            Self::LastActivity => 20,
645            Self::CreationTime => 30,
646        };
647        self.name().len().max(custom) as u16
648    }
649
650    fn render(&self, item: &GroupUser) -> (String, ratatui::style::Style) {
651        let text = match self {
652            Self::UserName => item.user_name.clone(),
653            Self::Groups => item.groups.clone(),
654            Self::LastActivity => item.last_activity.clone(),
655            Self::CreationTime => item.creation_time.clone(),
656        };
657        (text, ratatui::style::Style::default())
658    }
659}
660
661impl Column<Policy> for PolicyColumn {
662    fn name(&self) -> &str {
663        match self {
664            Self::PolicyName => "Policy name",
665            Self::Type => "Type",
666            Self::AttachedVia => "Attached via",
667            Self::AttachedEntities => "Attached entities",
668            Self::Description => "Description",
669            Self::CreationTime => "Creation time",
670            Self::EditedTime => "Edited time",
671        }
672    }
673
674    fn width(&self) -> u16 {
675        match self {
676            Self::PolicyName => 30,
677            Self::Type => 15,
678            Self::AttachedVia => 20,
679            Self::AttachedEntities => 20,
680            Self::Description => 40,
681            Self::CreationTime => 30,
682            Self::EditedTime => 30,
683        }
684    }
685
686    fn render(&self, item: &Policy) -> (String, ratatui::style::Style) {
687        let text = match self {
688            Self::PolicyName => item.policy_name.clone(),
689            Self::Type => item.policy_type.clone(),
690            Self::AttachedVia => item.attached_via.clone(),
691            Self::AttachedEntities => item.attached_entities.clone(),
692            Self::Description => item.description.clone(),
693            Self::CreationTime => item.creation_time.clone(),
694            Self::EditedTime => item.edited_time.clone(),
695        };
696        (text, ratatui::style::Style::default())
697    }
698}
699
700impl Column<RoleTag> for TagColumn {
701    fn name(&self) -> &str {
702        match self {
703            Self::Key => "Key",
704            Self::Value => "Value",
705        }
706    }
707
708    fn width(&self) -> u16 {
709        match self {
710            Self::Key => 30,
711            Self::Value => 70,
712        }
713    }
714
715    fn render(&self, item: &RoleTag) -> (String, ratatui::style::Style) {
716        let text = match self {
717            Self::Key => item.key.clone(),
718            Self::Value => item.value.clone(),
719        };
720        (text, ratatui::style::Style::default())
721    }
722}
723
724impl Column<UserTag> for TagColumn {
725    fn name(&self) -> &str {
726        match self {
727            Self::Key => "Key",
728            Self::Value => "Value",
729        }
730    }
731
732    fn width(&self) -> u16 {
733        match self {
734            Self::Key => 30,
735            Self::Value => 70,
736        }
737    }
738
739    fn render(&self, item: &UserTag) -> (String, ratatui::style::Style) {
740        let text = match self {
741            Self::Key => item.key.clone(),
742            Self::Value => item.value.clone(),
743        };
744        (text, ratatui::style::Style::default())
745    }
746}
747
748impl Column<UserGroup> for UserGroupColumn {
749    fn name(&self) -> &str {
750        match self {
751            Self::GroupName => "Group name",
752            Self::AttachedPolicies => "Attached policies",
753        }
754    }
755
756    fn width(&self) -> u16 {
757        match self {
758            Self::GroupName => 40,
759            Self::AttachedPolicies => 60,
760        }
761    }
762
763    fn render(&self, item: &UserGroup) -> (String, ratatui::style::Style) {
764        let text = match self {
765            Self::GroupName => item.group_name.clone(),
766            Self::AttachedPolicies => item.attached_policies.clone(),
767        };
768        (text, ratatui::style::Style::default())
769    }
770}
771
772impl Column<LastAccessedService> for LastAccessedServiceColumn {
773    fn name(&self) -> &str {
774        match self {
775            Self::Service => "Service",
776            Self::PoliciesGranting => "Policies granting permissions",
777            Self::LastAccessed => "Last accessed",
778        }
779    }
780
781    fn width(&self) -> u16 {
782        match self {
783            Self::Service => 30,
784            Self::PoliciesGranting => 40,
785            Self::LastAccessed => 30,
786        }
787    }
788
789    fn render(&self, item: &LastAccessedService) -> (String, ratatui::style::Style) {
790        let text = match self {
791            Self::Service => item.service.clone(),
792            Self::PoliciesGranting => item.policies_granting.clone(),
793            Self::LastAccessed => item.last_accessed.clone(),
794        };
795        (text, ratatui::style::Style::default())
796    }
797}
798
799#[cfg(test)]
800mod tests {
801    use super::*;
802    use crate::common::CyclicEnum;
803    use crate::ui::iam::{GroupTab, State, UserTab};
804
805    #[test]
806    fn test_user_group_creation() {
807        let group = UserGroup {
808            group_name: "Developers".to_string(),
809            attached_policies: "AmazonS3ReadOnlyAccess, AmazonEC2ReadOnlyAccess".to_string(),
810        };
811        assert_eq!(group.group_name, "Developers");
812        assert_eq!(
813            group.attached_policies,
814            "AmazonS3ReadOnlyAccess, AmazonEC2ReadOnlyAccess"
815        );
816    }
817
818    #[test]
819    fn test_iam_state_user_group_memberships_initialization() {
820        let state = State::new();
821        assert_eq!(state.user_group_memberships.items.len(), 0);
822        assert_eq!(state.user_group_memberships.selected, 0);
823        assert_eq!(state.user_group_memberships.filter, "");
824    }
825
826    #[test]
827    fn test_user_tab_groups() {
828        let tab = UserTab::Permissions;
829        assert_eq!(tab.next(), UserTab::Groups);
830        assert_eq!(UserTab::Groups.name(), "Groups");
831    }
832
833    #[test]
834    fn test_group_tab_navigation() {
835        let tab = GroupTab::Users;
836        assert_eq!(tab.next(), GroupTab::Permissions);
837        assert_eq!(tab.next().next(), GroupTab::AccessAdvisor);
838        assert_eq!(tab.next().next().next(), GroupTab::Users);
839    }
840
841    #[test]
842    fn test_group_tab_names() {
843        assert_eq!(GroupTab::Users.name(), "Users");
844        assert_eq!(GroupTab::Permissions.name(), "Permissions");
845        assert_eq!(GroupTab::AccessAdvisor.name(), "Access Advisor");
846    }
847
848    #[test]
849    fn test_iam_state_group_tab_initialization() {
850        let state = State::new();
851        assert_eq!(state.group_tab, GroupTab::Users);
852    }
853}