Skip to main content

rusticity_term/iam/
mod.rs

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