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}