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