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}