1use crate::common::UTC_TIMESTAMP_WIDTH;
2use serde::{Deserialize, Serialize};
3
4pub fn format_arn(account_id: &str, resource_type: &str, resource_name: &str) -> String {
5 format!(
6 "arn:aws:iam::{}:{}/{}",
7 account_id, resource_type, resource_name
8 )
9}
10
11pub fn console_url_users(_region: &str) -> String {
12 "https://console.aws.amazon.com/iam/home#/users".to_string()
13}
14
15pub fn console_url_user_detail(region: &str, user_name: &str, section: &str) -> String {
16 format!(
17 "https://{}.console.aws.amazon.com/iam/home?region={}#/users/details/{}?section={}",
18 region, region, user_name, section
19 )
20}
21
22pub fn console_url_roles(_region: &str) -> String {
23 "https://console.aws.amazon.com/iam/home#/roles".to_string()
24}
25
26pub fn console_url_role_detail(region: &str, role_name: &str, section: &str) -> String {
27 format!(
28 "https://{}.console.aws.amazon.com/iam/home?region={}#/roles/details/{}?section={}",
29 region, region, role_name, section
30 )
31}
32
33pub fn console_url_role_policy(region: &str, role_name: &str, policy_name: &str) -> String {
34 format!(
35 "https://{}.console.aws.amazon.com/iam/home?region={}#/roles/details/{}/editPolicy/{}?step=addPermissions",
36 region, region, role_name, policy_name
37 )
38}
39
40pub fn console_url_groups(region: &str) -> String {
41 format!(
42 "https://{}.console.aws.amazon.com/iam/home?region={}#/groups",
43 region, region
44 )
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct IamUser {
49 pub user_name: String,
50 pub path: String,
51 pub groups: String,
52 pub last_activity: String,
53 pub mfa: String,
54 pub password_age: String,
55 pub console_last_sign_in: String,
56 pub access_key_id: String,
57 pub active_key_age: String,
58 pub access_key_last_used: String,
59 pub arn: String,
60 pub creation_time: String,
61 pub console_access: String,
62 pub signing_certs: String,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct IamRole {
67 pub role_name: String,
68 pub path: String,
69 pub trusted_entities: String,
70 pub last_activity: String,
71 pub arn: String,
72 pub creation_time: String,
73 pub description: String,
74 pub max_session_duration: String,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct IamGroup {
79 pub group_name: String,
80 pub path: String,
81 pub users: String,
82 pub permissions: String,
83 pub creation_time: String,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct Policy {
88 pub policy_name: String,
89 pub policy_type: String,
90 pub attached_via: String,
91 pub attached_entities: String,
92 pub description: String,
93 pub creation_time: String,
94 pub edited_time: String,
95 pub policy_arn: Option<String>,
96}
97
98#[derive(Debug, Clone)]
99pub struct RoleTag {
100 pub key: String,
101 pub value: String,
102}
103
104#[derive(Debug, Clone)]
105pub struct UserTag {
106 pub key: String,
107 pub value: String,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct UserGroup {
112 pub group_name: String,
113 pub attached_policies: String,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct GroupUser {
118 pub user_name: String,
119 pub groups: String,
120 pub last_activity: String,
121 pub creation_time: String,
122}
123
124#[derive(Debug, Clone)]
125pub struct LastAccessedService {
126 pub service: String,
127 pub policies_granting: String,
128 pub last_accessed: String,
129}
130
131#[derive(Debug, Clone, Copy)]
132pub enum UserColumn {
133 UserName,
134 Path,
135 Groups,
136 LastActivity,
137 Mfa,
138 PasswordAge,
139 ConsoleLastSignIn,
140 AccessKeyId,
141 ActiveKeyAge,
142 AccessKeyLastUsed,
143 Arn,
144 CreationTime,
145 ConsoleAccess,
146 SigningCerts,
147}
148
149#[derive(Debug, Clone, Copy)]
150pub enum GroupColumn {
151 GroupName,
152 Path,
153 Users,
154 Permissions,
155 CreationTime,
156}
157
158#[derive(Debug, Clone, Copy)]
159pub enum RoleColumn {
160 RoleName,
161 Path,
162 TrustedEntities,
163 LastActivity,
164 Arn,
165 CreationTime,
166 Description,
167 MaxSessionDuration,
168}
169
170#[derive(Debug, Clone, Copy)]
171pub enum GroupUserColumn {
172 UserName,
173 Groups,
174 LastActivity,
175 CreationTime,
176}
177
178impl UserColumn {
179 pub fn to_column(self) -> Box<dyn for<'a> crate::ui::table::Column<&'a IamUser>> {
180 use crate::ui::table::Column as TableColumn;
181 use ratatui::style::Style;
182
183 struct UserCol(UserColumn);
184 impl<'a> TableColumn<&'a IamUser> for UserCol {
185 fn name(&self) -> &str {
186 match self.0 {
187 UserColumn::UserName => "User name",
188 UserColumn::Path => "Path",
189 UserColumn::Groups => "Groups",
190 UserColumn::LastActivity => "Last activity",
191 UserColumn::Mfa => "MFA",
192 UserColumn::PasswordAge => "Password age",
193 UserColumn::ConsoleLastSignIn => "Console last sign-in",
194 UserColumn::AccessKeyId => "Access key ID",
195 UserColumn::ActiveKeyAge => "Active key age",
196 UserColumn::AccessKeyLastUsed => "Access key last used",
197 UserColumn::Arn => "ARN",
198 UserColumn::CreationTime => "Creation time",
199 UserColumn::ConsoleAccess => "Console access",
200 UserColumn::SigningCerts => "Signing certs",
201 }
202 }
203 fn width(&self) -> u16 {
204 let custom = match self.0 {
205 UserColumn::UserName => 20,
206 UserColumn::Path => 15,
207 UserColumn::Groups => 20,
208 UserColumn::LastActivity => 20,
209 UserColumn::Mfa => 10,
210 UserColumn::PasswordAge => 15,
211 UserColumn::ConsoleLastSignIn => 25,
212 UserColumn::AccessKeyId => 25,
213 UserColumn::ActiveKeyAge => 18,
214 UserColumn::AccessKeyLastUsed => UTC_TIMESTAMP_WIDTH as usize,
215 UserColumn::Arn => 50,
216 UserColumn::CreationTime => 30,
217 UserColumn::ConsoleAccess => 15,
218 UserColumn::SigningCerts => 15,
219 };
220 self.name().len().max(custom) as u16
221 }
222 fn render(&self, item: &&'a IamUser) -> (String, Style) {
223 let value = match self.0 {
224 UserColumn::UserName => {
225 return (item.user_name.clone(), Style::default());
226 }
227 UserColumn::Path => &item.path,
228 UserColumn::Groups => &item.groups,
229 UserColumn::LastActivity => &item.last_activity,
230 UserColumn::Mfa => &item.mfa,
231 UserColumn::PasswordAge => &item.password_age,
232 UserColumn::ConsoleLastSignIn => &item.console_last_sign_in,
233 UserColumn::AccessKeyId => &item.access_key_id,
234 UserColumn::ActiveKeyAge => &item.active_key_age,
235 UserColumn::AccessKeyLastUsed => &item.access_key_last_used,
236 UserColumn::Arn => &item.arn,
237 UserColumn::CreationTime => &item.creation_time,
238 UserColumn::ConsoleAccess => &item.console_access,
239 UserColumn::SigningCerts => &item.signing_certs,
240 };
241 (value.clone(), Style::default())
242 }
243 }
244 Box::new(UserCol(self))
245 }
246}
247
248impl GroupColumn {
249 pub fn to_column(self) -> Box<dyn crate::ui::table::Column<IamGroup>> {
250 use crate::ui::table::Column as TableColumn;
251 use ratatui::style::Style;
252
253 struct GroupCol(GroupColumn);
254 impl TableColumn<IamGroup> for GroupCol {
255 fn name(&self) -> &str {
256 match self.0 {
257 GroupColumn::GroupName => "Group name",
258 GroupColumn::Path => "Path",
259 GroupColumn::Users => "Users",
260 GroupColumn::Permissions => "Permissions",
261 GroupColumn::CreationTime => "Creation time",
262 }
263 }
264 fn width(&self) -> u16 {
265 let custom = match self.0 {
266 GroupColumn::GroupName => 20,
267 GroupColumn::Path => 15,
268 GroupColumn::Users => 10,
269 GroupColumn::Permissions => 20,
270 GroupColumn::CreationTime => 30,
271 };
272 self.name().len().max(custom) as u16
273 }
274 fn render(&self, item: &IamGroup) -> (String, Style) {
275 use ratatui::style::Color;
276 match self.0 {
277 GroupColumn::GroupName => (item.group_name.clone(), Style::default()),
278 GroupColumn::Permissions if item.permissions == "Defined" => (
279 format!("✅ {}", item.permissions),
280 Style::default().fg(Color::Green),
281 ),
282 GroupColumn::Path => (item.path.clone(), Style::default()),
283 GroupColumn::Users => (item.users.clone(), Style::default()),
284 GroupColumn::Permissions => (item.permissions.clone(), Style::default()),
285 GroupColumn::CreationTime => (item.creation_time.clone(), Style::default()),
286 }
287 }
288 }
289 Box::new(GroupCol(self))
290 }
291}
292
293impl RoleColumn {
294 pub fn to_column(self) -> Box<dyn crate::ui::table::Column<IamRole>> {
295 use crate::ui::table::Column as TableColumn;
296 use ratatui::style::Style;
297
298 struct RoleCol(RoleColumn);
299 impl TableColumn<IamRole> for RoleCol {
300 fn name(&self) -> &str {
301 match self.0 {
302 RoleColumn::RoleName => "Role name",
303 RoleColumn::Path => "Path",
304 RoleColumn::TrustedEntities => "Trusted entities",
305 RoleColumn::LastActivity => "Last activity",
306 RoleColumn::Arn => "ARN",
307 RoleColumn::CreationTime => "Creation time",
308 RoleColumn::Description => "Description",
309 RoleColumn::MaxSessionDuration => "Max CLI/API session",
310 }
311 }
312 fn width(&self) -> u16 {
313 let custom = match self.0 {
314 RoleColumn::RoleName => 30,
315 RoleColumn::Path => 15,
316 RoleColumn::TrustedEntities => 30,
317 RoleColumn::LastActivity => 20,
318 RoleColumn::Arn => 50,
319 RoleColumn::CreationTime => 30,
320 RoleColumn::Description => 40,
321 RoleColumn::MaxSessionDuration => 22,
322 };
323 self.name().len().max(custom) as u16
324 }
325 fn render(&self, item: &IamRole) -> (String, Style) {
326 let value = match self.0 {
327 RoleColumn::RoleName => {
328 return (item.role_name.clone(), Style::default());
329 }
330 RoleColumn::Path => &item.path,
331 RoleColumn::TrustedEntities => &item.trusted_entities,
332 RoleColumn::LastActivity => &item.last_activity,
333 RoleColumn::Arn => &item.arn,
334 RoleColumn::CreationTime => &item.creation_time,
335 RoleColumn::Description => &item.description,
336 RoleColumn::MaxSessionDuration => &item.max_session_duration,
337 };
338 (value.clone(), Style::default())
339 }
340 }
341 Box::new(RoleCol(self))
342 }
343}
344
345impl GroupUserColumn {
346 pub fn to_column(self) -> Box<dyn crate::ui::table::Column<GroupUser>> {
347 use crate::ui::table::Column as TableColumn;
348 use ratatui::style::Style;
349
350 struct GroupUserCol(GroupUserColumn);
351 impl TableColumn<GroupUser> for GroupUserCol {
352 fn name(&self) -> &str {
353 match self.0 {
354 GroupUserColumn::UserName => "User name",
355 GroupUserColumn::Groups => "Groups",
356 GroupUserColumn::LastActivity => "Last activity",
357 GroupUserColumn::CreationTime => "Creation time",
358 }
359 }
360 fn width(&self) -> u16 {
361 let custom = match self.0 {
362 GroupUserColumn::UserName => 20,
363 GroupUserColumn::Groups => 20,
364 GroupUserColumn::LastActivity => 20,
365 GroupUserColumn::CreationTime => 30,
366 };
367 self.name().len().max(custom) as u16
368 }
369 fn render(&self, item: &GroupUser) -> (String, Style) {
370 match self.0 {
371 GroupUserColumn::UserName => (item.user_name.clone(), Style::default()),
372 GroupUserColumn::Groups => (item.groups.clone(), Style::default()),
373 GroupUserColumn::LastActivity => (item.last_activity.clone(), Style::default()),
374 GroupUserColumn::CreationTime => (item.creation_time.clone(), Style::default()),
375 }
376 }
377 }
378 Box::new(GroupUserCol(self))
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::common::CyclicEnum;
386 use crate::ui::iam::{GroupTab, State, UserTab};
387
388 #[test]
389 fn test_user_group_creation() {
390 let group = UserGroup {
391 group_name: "Developers".to_string(),
392 attached_policies: "AmazonS3ReadOnlyAccess, AmazonEC2ReadOnlyAccess".to_string(),
393 };
394 assert_eq!(group.group_name, "Developers");
395 assert_eq!(
396 group.attached_policies,
397 "AmazonS3ReadOnlyAccess, AmazonEC2ReadOnlyAccess"
398 );
399 }
400
401 #[test]
402 fn test_iam_state_user_group_memberships_initialization() {
403 let state = State::new();
404 assert_eq!(state.user_group_memberships.items.len(), 0);
405 assert_eq!(state.user_group_memberships.selected, 0);
406 assert_eq!(state.user_group_memberships.filter, "");
407 }
408
409 #[test]
410 fn test_user_tab_groups() {
411 let tab = UserTab::Permissions;
412 assert_eq!(tab.next(), UserTab::Groups);
413 assert_eq!(UserTab::Groups.name(), "Groups");
414 }
415
416 #[test]
417 fn test_group_tab_navigation() {
418 let tab = GroupTab::Users;
419 assert_eq!(tab.next(), GroupTab::Permissions);
420 assert_eq!(tab.next().next(), GroupTab::AccessAdvisor);
421 assert_eq!(tab.next().next().next(), GroupTab::Users);
422 }
423
424 #[test]
425 fn test_group_tab_names() {
426 assert_eq!(GroupTab::Users.name(), "Users");
427 assert_eq!(GroupTab::Permissions.name(), "Permissions");
428 assert_eq!(GroupTab::AccessAdvisor.name(), "Access Advisor");
429 }
430
431 #[test]
432 fn test_iam_state_group_tab_initialization() {
433 let state = State::new();
434 assert_eq!(state.group_tab, GroupTab::Users);
435 }
436}