reverse-diagrams 1.3.4__py3-none-any.whl → 1.3.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,496 @@
1
+ """AWS IAM Identity Center (SSO) plugin for generating identity and access diagrams."""
2
+ import logging
3
+ from typing import Dict, Any, List
4
+ from pathlib import Path
5
+
6
+ from src.plugins.base import AWSServicePlugin, PluginMetadata
7
+ from src.aws.client_manager import AWSClientManager
8
+ from src.models import DiagramConfig
9
+ from src.utils.concurrent import get_concurrent_processor
10
+ from src.utils.progress import get_progress_tracker
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class IdentityCenterPlugin(AWSServicePlugin):
16
+ """Plugin for AWS IAM Identity Center (SSO) diagram generation."""
17
+
18
+ @property
19
+ def metadata(self) -> PluginMetadata:
20
+ """Get plugin metadata."""
21
+ return PluginMetadata(
22
+ name="identity-center",
23
+ version="1.0.0",
24
+ description="Generate diagrams for AWS IAM Identity Center (SSO) groups, users, and permissions",
25
+ author="Reverse Diagrams Team",
26
+ aws_services=["sso-admin", "identitystore", "organizations"],
27
+ dependencies=[]
28
+ )
29
+
30
+ def collect_data(self, client_manager: AWSClientManager, region: str, **kwargs) -> Dict[str, Any]:
31
+ """
32
+ Collect AWS IAM Identity Center data.
33
+
34
+ Args:
35
+ client_manager: AWS client manager
36
+ region: AWS region
37
+ **kwargs: Additional parameters
38
+
39
+ Returns:
40
+ Dictionary containing Identity Center data
41
+ """
42
+ logger.debug(f"Collecting AWS IAM Identity Center data from region {region}")
43
+ progress = get_progress_tracker()
44
+
45
+ data = {
46
+ "region": region,
47
+ "sso_instances": [],
48
+ "identity_store_id": "",
49
+ "instance_arn": "",
50
+ "groups": [],
51
+ "users": [],
52
+ "group_memberships": [],
53
+ "permission_sets": [],
54
+ "permission_set_details": {},
55
+ "account_assignments": [],
56
+ "accounts": [],
57
+ "final_account_assignments": {}
58
+ }
59
+
60
+ try:
61
+ # Get SSO instances
62
+ progress.show_success("🔐 Getting Identity Store Instance Info")
63
+ instances_response = client_manager.call_api("sso-admin", "list_instances")
64
+ data["sso_instances"] = instances_response.get("Instances", [])
65
+
66
+ if not data["sso_instances"]:
67
+ raise ValueError("No SSO instances found")
68
+
69
+ data["identity_store_id"] = data["sso_instances"][0]["IdentityStoreId"]
70
+ data["instance_arn"] = data["sso_instances"][0]["InstanceArn"]
71
+
72
+ logger.debug(f"Using Identity Store ID: {data['identity_store_id']}")
73
+
74
+ # Get groups
75
+ progress.show_success("👥 Listing Groups")
76
+ data["groups"] = self._list_groups(client_manager, data["identity_store_id"])
77
+ logger.debug(f"Found {len(data['groups'])} groups")
78
+
79
+ # Get users
80
+ progress.show_success("👤 Listing Users")
81
+ data["users"] = self._list_users(client_manager, data["identity_store_id"])
82
+ logger.debug(f"Found {len(data['users'])} users")
83
+
84
+ # Get group memberships
85
+ progress.show_success("🔗 Getting Group Memberships")
86
+ data["group_memberships"] = self._get_group_memberships(
87
+ client_manager, data["identity_store_id"], data["groups"]
88
+ )
89
+
90
+ # Complete group members with user information
91
+ data["group_memberships"] = self._complete_group_members(
92
+ data["group_memberships"], data["users"]
93
+ )
94
+
95
+ # Get permission sets
96
+ progress.show_success("🛡️ Listing Permission Sets")
97
+ data["permission_sets"] = self._list_permission_sets(
98
+ client_manager, data["instance_arn"]
99
+ )
100
+ logger.debug(f"Found {len(data['permission_sets'])} permission sets")
101
+
102
+ # Get permission set details
103
+ data["permission_set_details"] = self._get_permission_set_details(
104
+ client_manager, data["instance_arn"], data["permission_sets"]
105
+ )
106
+
107
+ # Get organization accounts
108
+ progress.show_success("🏢 Getting Organization Accounts")
109
+ data["accounts"] = self._list_organization_accounts(client_manager)
110
+ logger.debug(f"Found {len(data['accounts'])} accounts")
111
+
112
+ # Get account assignments
113
+ progress.show_success("📋 Getting Account Assignments")
114
+ data["account_assignments"] = self._get_account_assignments(
115
+ client_manager,
116
+ data["instance_arn"],
117
+ data["accounts"],
118
+ data["permission_sets"]
119
+ )
120
+
121
+ # Add user and group information to assignments
122
+ data["account_assignments"] = self._enrich_account_assignments(
123
+ data["account_assignments"],
124
+ data["group_memberships"],
125
+ data["users"],
126
+ data["permission_set_details"]
127
+ )
128
+
129
+ # Create final account assignments structure
130
+ data["final_account_assignments"] = self._organize_account_assignments(
131
+ data["accounts"], data["account_assignments"]
132
+ )
133
+
134
+ progress.show_summary(
135
+ "Identity Center Summary",
136
+ [
137
+ f"Identity Store ID: {data['identity_store_id']}",
138
+ f"Groups: {len(data['groups'])}",
139
+ f"Users: {len(data['users'])}",
140
+ f"Permission Sets: {len(data['permission_sets'])}",
141
+ f"Account Assignments: {len(data['account_assignments'])}"
142
+ ]
143
+ )
144
+
145
+ except Exception as e:
146
+ logger.error(f"Failed to collect Identity Center data: {e}")
147
+ raise
148
+
149
+ return data
150
+
151
+ def _list_groups(self, client_manager: AWSClientManager, identity_store_id: str) -> List[Dict[str, Any]]:
152
+ """List all groups in the identity store."""
153
+ try:
154
+ groups = client_manager.paginate_api_call(
155
+ "identitystore",
156
+ "list_groups",
157
+ "Groups",
158
+ IdentityStoreId=identity_store_id
159
+ )
160
+ return groups
161
+ except Exception as e:
162
+ logger.error(f"Failed to list groups: {e}")
163
+ return []
164
+
165
+ def _list_users(self, client_manager: AWSClientManager, identity_store_id: str) -> List[Dict[str, Any]]:
166
+ """List all users in the identity store."""
167
+ try:
168
+ users = client_manager.paginate_api_call(
169
+ "identitystore",
170
+ "list_users",
171
+ "Users",
172
+ IdentityStoreId=identity_store_id
173
+ )
174
+ return users
175
+ except Exception as e:
176
+ logger.error(f"Failed to list users: {e}")
177
+ return []
178
+
179
+ def _get_group_memberships(
180
+ self,
181
+ client_manager: AWSClientManager,
182
+ identity_store_id: str,
183
+ groups: List[Dict[str, Any]]
184
+ ) -> List[Dict[str, Any]]:
185
+ """Get group memberships for all groups."""
186
+ group_memberships = []
187
+ progress = get_progress_tracker()
188
+
189
+ with progress.track_operation(f"Getting memberships for {len(groups)} groups", total=len(groups)) as task_id:
190
+ for group in groups:
191
+ try:
192
+ memberships = client_manager.paginate_api_call(
193
+ "identitystore",
194
+ "list_group_memberships",
195
+ "GroupMemberships",
196
+ IdentityStoreId=identity_store_id,
197
+ GroupId=group["GroupId"]
198
+ )
199
+
200
+ group_memberships.append({
201
+ "group_id": group["GroupId"],
202
+ "group_name": group["DisplayName"],
203
+ "members": memberships
204
+ })
205
+
206
+ progress.update_progress(task_id)
207
+
208
+ except Exception as e:
209
+ logger.warning(f"Failed to get memberships for group {group['GroupId']}: {e}")
210
+ group_memberships.append({
211
+ "group_id": group["GroupId"],
212
+ "group_name": group["DisplayName"],
213
+ "members": []
214
+ })
215
+
216
+ return group_memberships
217
+
218
+ def _complete_group_members(
219
+ self,
220
+ group_memberships: List[Dict[str, Any]],
221
+ users: List[Dict[str, Any]]
222
+ ) -> List[Dict[str, Any]]:
223
+ """Complete group member information with user details."""
224
+ user_lookup = {user["UserId"]: user for user in users}
225
+
226
+ for group_membership in group_memberships:
227
+ for member in group_membership["members"]:
228
+ user_id = member.get("MemberId", {}).get("UserId")
229
+ if user_id and user_id in user_lookup:
230
+ user = user_lookup[user_id]
231
+ member["MemberId"]["UserName"] = user.get("UserName", "Unknown")
232
+
233
+ return group_memberships
234
+
235
+ def _list_permission_sets(self, client_manager: AWSClientManager, instance_arn: str) -> List[str]:
236
+ """List all permission sets."""
237
+ try:
238
+ permission_sets = client_manager.paginate_api_call(
239
+ "sso-admin",
240
+ "list_permission_sets",
241
+ "PermissionSets",
242
+ InstanceArn=instance_arn
243
+ )
244
+ return permission_sets
245
+ except Exception as e:
246
+ logger.error(f"Failed to list permission sets: {e}")
247
+ return []
248
+
249
+ def _get_permission_set_details(
250
+ self,
251
+ client_manager: AWSClientManager,
252
+ instance_arn: str,
253
+ permission_sets: List[str]
254
+ ) -> Dict[str, str]:
255
+ """Get detailed information for permission sets."""
256
+ permission_set_details = {}
257
+ progress = get_progress_tracker()
258
+
259
+ with progress.track_operation(f"Getting details for {len(permission_sets)} permission sets", total=len(permission_sets)) as task_id:
260
+ for permission_set_arn in permission_sets:
261
+ try:
262
+ response = client_manager.call_api(
263
+ "sso-admin",
264
+ "describe_permission_set",
265
+ InstanceArn=instance_arn,
266
+ PermissionSetArn=permission_set_arn
267
+ )
268
+
269
+ permission_set = response.get("PermissionSet", {})
270
+ permission_set_details[permission_set_arn] = permission_set.get("Name", "Unknown")
271
+
272
+ progress.update_progress(task_id)
273
+
274
+ except Exception as e:
275
+ logger.warning(f"Failed to describe permission set {permission_set_arn}: {e}")
276
+ permission_set_details[permission_set_arn] = "Unknown"
277
+
278
+ return permission_set_details
279
+
280
+ def _list_organization_accounts(self, client_manager: AWSClientManager) -> List[Dict[str, Any]]:
281
+ """List organization accounts."""
282
+ try:
283
+ accounts = client_manager.paginate_api_call(
284
+ "organizations",
285
+ "list_accounts",
286
+ "Accounts"
287
+ )
288
+ return accounts
289
+ except Exception as e:
290
+ logger.warning(f"Failed to list organization accounts: {e}")
291
+ return []
292
+
293
+ def _get_account_assignments(
294
+ self,
295
+ client_manager: AWSClientManager,
296
+ instance_arn: str,
297
+ accounts: List[Dict[str, Any]],
298
+ permission_sets: List[str]
299
+ ) -> List[Dict[str, Any]]:
300
+ """Get account assignments for all accounts and permission sets."""
301
+ all_assignments = []
302
+ progress = get_progress_tracker()
303
+
304
+ total_operations = len(accounts) * len(permission_sets)
305
+
306
+ with progress.track_operation(f"Getting account assignments", total=total_operations) as task_id:
307
+ for account in accounts:
308
+ for permission_set_arn in permission_sets:
309
+ try:
310
+ assignments = client_manager.paginate_api_call(
311
+ "sso-admin",
312
+ "list_account_assignments",
313
+ "AccountAssignments",
314
+ InstanceArn=instance_arn,
315
+ AccountId=account["Id"],
316
+ PermissionSetArn=permission_set_arn
317
+ )
318
+
319
+ all_assignments.extend(assignments)
320
+ progress.update_progress(task_id)
321
+
322
+ except Exception as e:
323
+ logger.debug(f"No assignments for account {account['Id']} and permission set {permission_set_arn}: {e}")
324
+ progress.update_progress(task_id)
325
+
326
+ return all_assignments
327
+
328
+ def _enrich_account_assignments(
329
+ self,
330
+ account_assignments: List[Dict[str, Any]],
331
+ group_memberships: List[Dict[str, Any]],
332
+ users: List[Dict[str, Any]],
333
+ permission_set_details: Dict[str, str]
334
+ ) -> List[Dict[str, Any]]:
335
+ """Add user and group information to account assignments."""
336
+ # Create lookup dictionaries
337
+ group_lookup = {group["group_id"]: group for group in group_memberships}
338
+ user_lookup = {user["UserId"]: user for user in users}
339
+
340
+ progress = get_progress_tracker()
341
+
342
+ with progress.track_operation(f"Enriching {len(account_assignments)} assignments", total=len(account_assignments)) as task_id:
343
+ for assignment in account_assignments:
344
+ # Add permission set name
345
+ permission_set_arn = assignment.get("PermissionSetArn", "")
346
+ assignment["PermissionSetName"] = permission_set_details.get(permission_set_arn, "Unknown")
347
+
348
+ # Add group or user information
349
+ principal_type = assignment.get("PrincipalType", "")
350
+ principal_id = assignment.get("PrincipalId", "")
351
+
352
+ if principal_type == "GROUP" and principal_id in group_lookup:
353
+ assignment["GroupName"] = group_lookup[principal_id]["group_name"]
354
+ elif principal_type == "USER" and principal_id in user_lookup:
355
+ assignment["UserName"] = user_lookup[principal_id].get("UserName", "Unknown")
356
+
357
+ progress.update_progress(task_id)
358
+
359
+ return account_assignments
360
+
361
+ def _organize_account_assignments(
362
+ self,
363
+ accounts: List[Dict[str, Any]],
364
+ account_assignments: List[Dict[str, Any]]
365
+ ) -> Dict[str, List[Dict[str, Any]]]:
366
+ """Organize account assignments by account name."""
367
+ account_lookup = {account["Id"]: account["Name"] for account in accounts}
368
+ organized_assignments = {}
369
+
370
+ for assignment in account_assignments:
371
+ account_id = assignment.get("AccountId", "")
372
+ account_name = account_lookup.get(account_id, account_id)
373
+
374
+ if account_name not in organized_assignments:
375
+ organized_assignments[account_name] = []
376
+
377
+ organized_assignments[account_name].append(assignment)
378
+
379
+ return organized_assignments
380
+
381
+ def generate_diagram_code(self, data: Dict[str, Any], config: DiagramConfig) -> str:
382
+ """
383
+ Generate diagram code for AWS IAM Identity Center.
384
+
385
+ Args:
386
+ data: Identity Center data collected from AWS
387
+ config: Diagram configuration
388
+
389
+ Returns:
390
+ Python code for generating Identity Center diagram
391
+ """
392
+ logger.debug("Generating AWS IAM Identity Center diagram code")
393
+
394
+ code_lines = [
395
+ "from diagrams import Diagram, Cluster, Edge",
396
+ "from diagrams.aws.management import Organizations, OrganizationsAccount, OrganizationsOrganizationalUnit",
397
+ "from diagrams.aws.general import Users, User",
398
+ "from diagrams.aws.security import IAMPermissions",
399
+ "",
400
+ f'with Diagram("{config.title}", show=False, direction="{config.direction}"):'
401
+ ]
402
+
403
+ # Get data
404
+ final_assignments = data.get("final_account_assignments", {})
405
+ group_memberships = data.get("group_memberships", [])
406
+
407
+ # Create group lookup for members
408
+ group_lookup = {group["group_name"]: group for group in group_memberships}
409
+
410
+ # Generate account assignment clusters
411
+ for account_name, assignments in final_assignments.items():
412
+ if not assignments:
413
+ continue
414
+
415
+ code_lines.append(f" with Cluster('Account: {account_name}'):")
416
+
417
+ # Group assignments by group/user
418
+ processed_principals = set()
419
+
420
+ for assignment in assignments:
421
+ principal_key = None
422
+
423
+ if "GroupName" in assignment and assignment["GroupName"] not in processed_principals:
424
+ group_name = assignment["GroupName"]
425
+ principal_key = f"group_{self._format_name_for_code(group_name)}"
426
+ processed_principals.add(group_name)
427
+
428
+ code_lines.append(f" with Cluster('Group: {group_name}'):")
429
+ code_lines.append(f" {principal_key} = Users(\"{self._split_long_name(group_name)}\")")
430
+ code_lines.append(f" {principal_key} \\")
431
+ code_lines.append(f" - Edge(color=\"brown\", style=\"dotted\", label=\"Permissions Set\") \\")
432
+ code_lines.append(f" - IAMPermissions(\"{self._split_long_name(assignment['PermissionSetName'])}\")")
433
+
434
+ # Add group members if available
435
+ if group_name in group_lookup:
436
+ members = group_lookup[group_name]["members"]
437
+ if members:
438
+ member_names = [m.get("MemberId", {}).get("UserName", "Unknown") for m in members]
439
+ members_code = self._create_users_list(member_names)
440
+ code_lines.append(f" members_{self._format_name_for_code(group_name)} = {members_code}")
441
+ code_lines.append(f" {principal_key} \\")
442
+ code_lines.append(f" - Edge(color=\"darkgreen\", style=\"dotted\", label=\"Member\") \\")
443
+ code_lines.append(f" - members_{self._format_name_for_code(group_name)}")
444
+
445
+ elif "UserName" in assignment and assignment["UserName"] not in processed_principals:
446
+ user_name = assignment["UserName"]
447
+ principal_key = f"user_{self._format_name_for_code(user_name)}"
448
+ processed_principals.add(user_name)
449
+
450
+ code_lines.append(f" with Cluster('User: {user_name}'):")
451
+ code_lines.append(f" {principal_key} = User(\"{self._split_long_name(user_name)}\")")
452
+ code_lines.append(f" {principal_key} \\")
453
+ code_lines.append(f" - Edge(color=\"brown\", style=\"dotted\") \\")
454
+ code_lines.append(f" - IAMPermissions(\"{self._split_long_name(assignment['PermissionSetName'])}\")")
455
+
456
+ return "\n".join(code_lines)
457
+
458
+ def _format_name_for_code(self, name: str) -> str:
459
+ """Format name for use in Python code."""
460
+ import re
461
+ # Remove special characters and spaces
462
+ formatted = re.sub(r'[^\w]', '', name)
463
+ return formatted if formatted else "Unknown"
464
+
465
+ def _split_long_name(self, name: str) -> str:
466
+ """Split long names for better display."""
467
+ if len(name) > 17:
468
+ return name[:16] + "\\n" + name[16:]
469
+ return name
470
+
471
+ def _create_users_list(self, user_names: List[str]) -> str:
472
+ """Create a list of User objects for diagram code."""
473
+ if not user_names:
474
+ return "[]"
475
+
476
+ user_objects = []
477
+ for user_name in user_names[:5]: # Limit to 5 users to avoid clutter
478
+ user_objects.append(f'User("{self._split_long_name(user_name)}")')
479
+
480
+ if len(user_names) > 5:
481
+ user_objects.append(f'User("... and {len(user_names) - 5} more")')
482
+
483
+ return "[" + ", ".join(user_objects) + "]"
484
+
485
+ def get_required_permissions(self) -> List[str]:
486
+ """Get required AWS permissions for Identity Center plugin."""
487
+ return [
488
+ "sso:ListInstances",
489
+ "sso:ListPermissionSets",
490
+ "sso:DescribePermissionSet",
491
+ "sso:ListAccountAssignments",
492
+ "identitystore:ListGroups",
493
+ "identitystore:ListUsers",
494
+ "identitystore:ListGroupMemberships",
495
+ "organizations:ListAccounts"
496
+ ]