django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__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.
Files changed (120) hide show
  1. django_nativemojo-0.1.15.dist-info/METADATA +136 -0
  2. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +531 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/models/group.py +25 -7
  10. mojo/apps/account/models/member.py +15 -4
  11. mojo/apps/account/models/user.py +197 -20
  12. mojo/apps/account/rest/group.py +1 -0
  13. mojo/apps/account/rest/user.py +6 -2
  14. mojo/apps/aws/rest/__init__.py +1 -0
  15. mojo/apps/aws/rest/s3.py +64 -0
  16. mojo/apps/fileman/README.md +8 -8
  17. mojo/apps/fileman/backends/base.py +76 -70
  18. mojo/apps/fileman/backends/filesystem.py +86 -86
  19. mojo/apps/fileman/backends/s3.py +200 -108
  20. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  21. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  22. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  23. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  24. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  25. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  26. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  27. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  28. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  29. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  30. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  31. mojo/apps/fileman/models/__init__.py +1 -5
  32. mojo/apps/fileman/models/file.py +204 -58
  33. mojo/apps/fileman/models/manager.py +161 -31
  34. mojo/apps/fileman/models/rendition.py +118 -0
  35. mojo/apps/fileman/renderer/__init__.py +111 -0
  36. mojo/apps/fileman/renderer/audio.py +403 -0
  37. mojo/apps/fileman/renderer/base.py +205 -0
  38. mojo/apps/fileman/renderer/document.py +404 -0
  39. mojo/apps/fileman/renderer/image.py +222 -0
  40. mojo/apps/fileman/renderer/utils.py +297 -0
  41. mojo/apps/fileman/renderer/video.py +304 -0
  42. mojo/apps/fileman/rest/__init__.py +1 -18
  43. mojo/apps/fileman/rest/upload.py +22 -32
  44. mojo/apps/fileman/signals.py +58 -0
  45. mojo/apps/fileman/tasks.py +254 -0
  46. mojo/apps/fileman/utils/__init__.py +40 -16
  47. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  48. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  49. mojo/apps/incident/models/__init__.py +1 -0
  50. mojo/apps/incident/models/history.py +36 -0
  51. mojo/apps/incident/models/incident.py +1 -1
  52. mojo/apps/incident/reporter.py +3 -1
  53. mojo/apps/incident/rest/event.py +7 -1
  54. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  55. mojo/apps/logit/models/log.py +4 -1
  56. mojo/apps/metrics/utils.py +2 -2
  57. mojo/apps/notify/handlers/ses/message.py +1 -1
  58. mojo/apps/notify/providers/aws.py +2 -2
  59. mojo/apps/tasks/__init__.py +34 -1
  60. mojo/apps/tasks/manager.py +200 -45
  61. mojo/apps/tasks/rest/tasks.py +24 -10
  62. mojo/apps/tasks/runner.py +283 -18
  63. mojo/apps/tasks/task.py +99 -0
  64. mojo/apps/tasks/tq_handlers.py +118 -0
  65. mojo/decorators/auth.py +6 -1
  66. mojo/decorators/http.py +7 -2
  67. mojo/helpers/aws/__init__.py +41 -0
  68. mojo/helpers/aws/ec2.py +804 -0
  69. mojo/helpers/aws/iam.py +748 -0
  70. mojo/helpers/aws/s3.py +451 -11
  71. mojo/helpers/aws/ses.py +483 -0
  72. mojo/helpers/aws/sns.py +461 -0
  73. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  74. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  75. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  76. mojo/helpers/dates.py +18 -0
  77. mojo/helpers/response.py +6 -2
  78. mojo/helpers/settings/__init__.py +2 -0
  79. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  80. mojo/helpers/settings/parser.py +132 -0
  81. mojo/middleware/logging.py +1 -1
  82. mojo/middleware/mojo.py +5 -0
  83. mojo/models/rest.py +261 -46
  84. mojo/models/secrets.py +13 -4
  85. mojo/serializers/__init__.py +100 -0
  86. mojo/serializers/advanced/README.md +363 -0
  87. mojo/serializers/advanced/__init__.py +247 -0
  88. mojo/serializers/advanced/formats/__init__.py +28 -0
  89. mojo/serializers/advanced/formats/csv.py +416 -0
  90. mojo/serializers/advanced/formats/excel.py +516 -0
  91. mojo/serializers/advanced/formats/json.py +239 -0
  92. mojo/serializers/advanced/formats/localizers.py +509 -0
  93. mojo/serializers/advanced/formats/response.py +485 -0
  94. mojo/serializers/advanced/serializer.py +568 -0
  95. mojo/serializers/manager.py +501 -0
  96. mojo/serializers/optimized.py +618 -0
  97. mojo/serializers/settings_example.py +322 -0
  98. mojo/serializers/{models.py → simple.py} +38 -15
  99. testit/helpers.py +21 -4
  100. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  101. mojo/apps/metrics/rest/db.py +0 -0
  102. mojo/helpers/aws/setup_email.py +0 -0
  103. mojo/ws4redis/README.md +0 -174
  104. mojo/ws4redis/__init__.py +0 -2
  105. mojo/ws4redis/client.py +0 -283
  106. mojo/ws4redis/connection.py +0 -327
  107. mojo/ws4redis/exceptions.py +0 -32
  108. mojo/ws4redis/redis.py +0 -183
  109. mojo/ws4redis/servers/base.py +0 -86
  110. mojo/ws4redis/servers/django.py +0 -171
  111. mojo/ws4redis/servers/uwsgi.py +0 -63
  112. mojo/ws4redis/settings.py +0 -45
  113. mojo/ws4redis/utf8validator.py +0 -128
  114. mojo/ws4redis/websocket.py +0 -403
  115. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
  116. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
  117. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
  118. /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
  119. /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
  120. /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
@@ -0,0 +1,748 @@
1
+ """
2
+ AWS IAM Helper Module
3
+
4
+ Provides simple interfaces for managing AWS IAM resources.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import boto3
10
+ import botocore
11
+ from typing import Dict, List, Optional, Union, Any
12
+
13
+ from .client import get_session
14
+ from mojo.helpers.settings import settings
15
+ from mojo.helpers import logit
16
+
17
+ logger = logit.get_logger(__name__)
18
+
19
+
20
+ class IAMBase:
21
+ """Base class for IAM resource management."""
22
+
23
+ def __init__(self, access_key: Optional[str] = None, secret_key: Optional[str] = None,
24
+ region: Optional[str] = None):
25
+ """
26
+ Initialize IAM client with credentials.
27
+
28
+ Args:
29
+ access_key: AWS access key, defaults to settings.AWS_KEY
30
+ secret_key: AWS secret key, defaults to settings.AWS_SECRET
31
+ region: AWS region, defaults to settings.AWS_REGION if available
32
+ """
33
+ self.access_key = access_key or settings.AWS_KEY
34
+ self.secret_key = secret_key or settings.AWS_SECRET
35
+ self.region = region or getattr(settings, 'AWS_REGION', 'us-east-1')
36
+
37
+ session = get_session(self.access_key, self.secret_key, self.region)
38
+ self.client = session.client('iam')
39
+ self.resource = session.resource('iam')
40
+
41
+
42
+ class IAMUser(IAMBase):
43
+ """
44
+ Simple interface for IAM user management.
45
+ """
46
+
47
+ def __init__(self, username: str, **kwargs):
48
+ """
49
+ Initialize a user manager for the specified IAM user.
50
+
51
+ Args:
52
+ username: The IAM username
53
+ **kwargs: Additional arguments for IAMBase
54
+ """
55
+ super().__init__(**kwargs)
56
+ self.username = username
57
+ self.user = self.resource.User(self.username)
58
+ self.exists = self._check_exists()
59
+
60
+ def _check_exists(self) -> bool:
61
+ """Check if the user exists."""
62
+ try:
63
+ self.user.load()
64
+ return True
65
+ except botocore.exceptions.ClientError as e:
66
+ if e.response['Error']['Code'] == 'NoSuchEntity':
67
+ return False
68
+ # If it's a different error, log and re-raise
69
+ logger.error(f"Error checking user existence: {e}")
70
+ raise
71
+
72
+ def create(self, path: str = '/', tags: Optional[List[Dict[str, str]]] = None) -> bool:
73
+ """
74
+ Create the IAM user.
75
+
76
+ Args:
77
+ path: Path for the user
78
+ tags: Optional tags for the user
79
+
80
+ Returns:
81
+ True if user was created, False if it already exists
82
+ """
83
+ if self.exists:
84
+ logger.info(f"User {self.username} already exists")
85
+ return False
86
+
87
+ create_params = {
88
+ 'UserName': self.username,
89
+ 'Path': path
90
+ }
91
+
92
+ if tags:
93
+ create_params['Tags'] = tags
94
+
95
+ try:
96
+ self.client.create_user(**create_params)
97
+ self.exists = True
98
+ return True
99
+ except botocore.exceptions.ClientError as e:
100
+ logger.error(f"Failed to create user {self.username}: {e}")
101
+ return False
102
+
103
+ def delete(self, delete_access_keys: bool = True,
104
+ delete_signing_certs: bool = True,
105
+ delete_ssh_keys: bool = True) -> bool:
106
+ """
107
+ Delete the IAM user and optionally its access keys.
108
+
109
+ Args:
110
+ delete_access_keys: Delete access keys
111
+ delete_signing_certs: Delete signing certificates
112
+ delete_ssh_keys: Delete SSH public keys
113
+
114
+ Returns:
115
+ True if successfully deleted, False otherwise
116
+ """
117
+ if not self.exists:
118
+ logger.info(f"User {self.username} does not exist")
119
+ return False
120
+
121
+ try:
122
+ # Delete access keys if requested
123
+ if delete_access_keys:
124
+ for key in list(self.user.access_keys.all()):
125
+ key.delete()
126
+
127
+ # Delete signing certificates if requested
128
+ if delete_signing_certs:
129
+ for cert in list(self.user.signing_certificates.all()):
130
+ cert.delete()
131
+
132
+ # Delete SSH public keys if requested
133
+ if delete_ssh_keys:
134
+ response = self.client.list_ssh_public_keys(UserName=self.username)
135
+ for key in response.get('SSHPublicKeys', []):
136
+ self.client.delete_ssh_public_key(
137
+ UserName=self.username,
138
+ SSHPublicKeyId=key['SSHPublicKeyId']
139
+ )
140
+
141
+ # Delete user's policies
142
+ for policy in list(self.user.attached_policies.all()):
143
+ self.user.detach_policy(PolicyArn=policy.arn)
144
+
145
+ # Delete the user
146
+ self.user.delete()
147
+ self.exists = False
148
+ return True
149
+
150
+ except botocore.exceptions.ClientError as e:
151
+ logger.error(f"Failed to delete user {self.username}: {e}")
152
+ return False
153
+
154
+ def create_access_key(self) -> Dict[str, str]:
155
+ """
156
+ Create an access key for the user.
157
+
158
+ Returns:
159
+ Dict containing 'AccessKeyId' and 'SecretAccessKey'
160
+ """
161
+ if not self.exists:
162
+ logger.warning(f"User {self.username} does not exist")
163
+ return {}
164
+
165
+ try:
166
+ response = self.client.create_access_key(UserName=self.username)
167
+ return {
168
+ 'AccessKeyId': response['AccessKey']['AccessKeyId'],
169
+ 'SecretAccessKey': response['AccessKey']['SecretAccessKey']
170
+ }
171
+ except botocore.exceptions.ClientError as e:
172
+ logger.error(f"Failed to create access key for user {self.username}: {e}")
173
+ return {}
174
+
175
+ def attach_policy(self, policy_arn: str) -> bool:
176
+ """
177
+ Attach a managed policy to the user.
178
+
179
+ Args:
180
+ policy_arn: ARN of the policy to attach
181
+
182
+ Returns:
183
+ True if successful, False otherwise
184
+ """
185
+ if not self.exists:
186
+ logger.warning(f"User {self.username} does not exist")
187
+ return False
188
+
189
+ try:
190
+ self.client.attach_user_policy(
191
+ UserName=self.username,
192
+ PolicyArn=policy_arn
193
+ )
194
+ return True
195
+ except botocore.exceptions.ClientError as e:
196
+ logger.error(f"Failed to attach policy to user {self.username}: {e}")
197
+ return False
198
+
199
+ def detach_policy(self, policy_arn: str) -> bool:
200
+ """
201
+ Detach a managed policy from the user.
202
+
203
+ Args:
204
+ policy_arn: ARN of the policy to detach
205
+
206
+ Returns:
207
+ True if successful, False otherwise
208
+ """
209
+ if not self.exists:
210
+ logger.warning(f"User {self.username} does not exist")
211
+ return False
212
+
213
+ try:
214
+ self.client.detach_user_policy(
215
+ UserName=self.username,
216
+ PolicyArn=policy_arn
217
+ )
218
+ return True
219
+ except botocore.exceptions.ClientError as e:
220
+ logger.error(f"Failed to detach policy from user {self.username}: {e}")
221
+ return False
222
+
223
+ def add_to_group(self, group_name: str) -> bool:
224
+ """
225
+ Add the user to an IAM group.
226
+
227
+ Args:
228
+ group_name: Name of the group
229
+
230
+ Returns:
231
+ True if successful, False otherwise
232
+ """
233
+ if not self.exists:
234
+ logger.warning(f"User {self.username} does not exist")
235
+ return False
236
+
237
+ try:
238
+ self.client.add_user_to_group(
239
+ UserName=self.username,
240
+ GroupName=group_name
241
+ )
242
+ return True
243
+ except botocore.exceptions.ClientError as e:
244
+ logger.error(f"Failed to add user {self.username} to group {group_name}: {e}")
245
+ return False
246
+
247
+ def list_groups(self) -> List[str]:
248
+ """
249
+ List the groups the user belongs to.
250
+
251
+ Returns:
252
+ List of group names
253
+ """
254
+ if not self.exists:
255
+ logger.warning(f"User {self.username} does not exist")
256
+ return []
257
+
258
+ try:
259
+ response = self.client.list_groups_for_user(UserName=self.username)
260
+ return [group['GroupName'] for group in response.get('Groups', [])]
261
+ except botocore.exceptions.ClientError as e:
262
+ logger.error(f"Failed to list groups for user {self.username}: {e}")
263
+ return []
264
+
265
+
266
+ class IAMPolicy(IAMBase):
267
+ """
268
+ Simple interface for IAM policy management.
269
+ """
270
+
271
+ def __init__(self, name: str, **kwargs):
272
+ """
273
+ Initialize a policy manager for the specified IAM policy.
274
+
275
+ Args:
276
+ name: The policy name
277
+ **kwargs: Additional arguments for IAMBase
278
+ """
279
+ super().__init__(**kwargs)
280
+ self.name = name
281
+ self.arn = None
282
+ self.exists = self._check_exists()
283
+
284
+ def _check_exists(self) -> bool:
285
+ """Check if the policy exists and set ARN if it does."""
286
+ try:
287
+ # List policies with this name
288
+ response = self.client.list_policies(Scope='Local', PathPrefix='/')
289
+
290
+ for policy in response.get('Policies', []):
291
+ if policy['PolicyName'] == self.name:
292
+ self.arn = policy['Arn']
293
+ return True
294
+
295
+ return False
296
+ except botocore.exceptions.ClientError as e:
297
+ logger.error(f"Error checking policy existence: {e}")
298
+ return False
299
+
300
+ def create(self, policy_document: Union[Dict, str],
301
+ description: str = '',
302
+ path: str = '/') -> bool:
303
+ """
304
+ Create an IAM policy.
305
+
306
+ Args:
307
+ policy_document: Policy document as dict or JSON string
308
+ description: Policy description
309
+ path: Policy path
310
+
311
+ Returns:
312
+ True if policy was created, False if it already exists
313
+ """
314
+ if self.exists:
315
+ logger.info(f"Policy {self.name} already exists")
316
+ return False
317
+
318
+ try:
319
+ # Convert dict to JSON string if needed
320
+ policy_doc = policy_document if isinstance(policy_document, str) else json.dumps(policy_document)
321
+
322
+ response = self.client.create_policy(
323
+ PolicyName=self.name,
324
+ PolicyDocument=policy_doc,
325
+ Description=description,
326
+ Path=path
327
+ )
328
+
329
+ self.arn = response['Policy']['Arn']
330
+ self.exists = True
331
+ return True
332
+ except botocore.exceptions.ClientError as e:
333
+ logger.error(f"Failed to create policy {self.name}: {e}")
334
+ return False
335
+
336
+ def delete(self) -> bool:
337
+ """
338
+ Delete the IAM policy.
339
+
340
+ Returns:
341
+ True if successfully deleted, False otherwise
342
+ """
343
+ if not self.exists:
344
+ logger.info(f"Policy {self.name} does not exist")
345
+ return False
346
+
347
+ try:
348
+ # Delete all versions except the default version
349
+ versions = self.client.list_policy_versions(PolicyArn=self.arn)
350
+
351
+ for version in versions.get('Versions', []):
352
+ if not version['IsDefaultVersion']:
353
+ self.client.delete_policy_version(
354
+ PolicyArn=self.arn,
355
+ VersionId=version['VersionId']
356
+ )
357
+
358
+ # Delete the policy
359
+ self.client.delete_policy(PolicyArn=self.arn)
360
+ self.exists = False
361
+ self.arn = None
362
+ return True
363
+ except botocore.exceptions.ClientError as e:
364
+ logger.error(f"Failed to delete policy {self.name}: {e}")
365
+ return False
366
+
367
+ def update(self, policy_document: Union[Dict, str]) -> bool:
368
+ """
369
+ Update the IAM policy.
370
+
371
+ Args:
372
+ policy_document: New policy document as dict or JSON string
373
+
374
+ Returns:
375
+ True if successful, False otherwise
376
+ """
377
+ if not self.exists:
378
+ logger.warning(f"Policy {self.name} does not exist")
379
+ return False
380
+
381
+ try:
382
+ # Convert dict to JSON string if needed
383
+ policy_doc = policy_document if isinstance(policy_document, str) else json.dumps(policy_document)
384
+
385
+ # Create a new policy version and set it as default
386
+ self.client.create_policy_version(
387
+ PolicyArn=self.arn,
388
+ PolicyDocument=policy_doc,
389
+ SetAsDefault=True
390
+ )
391
+ return True
392
+ except botocore.exceptions.ClientError as e:
393
+ logger.error(f"Failed to update policy {self.name}: {e}")
394
+ return False
395
+
396
+ def get_document(self) -> Dict:
397
+ """
398
+ Get the policy document.
399
+
400
+ Returns:
401
+ Policy document as a dict
402
+ """
403
+ if not self.exists:
404
+ logger.warning(f"Policy {self.name} does not exist")
405
+ return {}
406
+
407
+ try:
408
+ # Get default version ID
409
+ policy = self.client.get_policy(PolicyArn=self.arn)
410
+ default_version = policy['Policy']['DefaultVersionId']
411
+
412
+ # Get the policy version document
413
+ version = self.client.get_policy_version(
414
+ PolicyArn=self.arn,
415
+ VersionId=default_version
416
+ )
417
+
418
+ # The document is URL-encoded JSON, so we need to decode it
419
+ import urllib.parse
420
+ doc_json = urllib.parse.unquote(version['PolicyVersion']['Document'])
421
+
422
+ # Convert to dict if it's a string
423
+ if isinstance(doc_json, str):
424
+ return json.loads(doc_json)
425
+ return doc_json
426
+ except botocore.exceptions.ClientError as e:
427
+ logger.error(f"Failed to get policy document for {self.name}: {e}")
428
+ return {}
429
+
430
+ @staticmethod
431
+ def list_all_policies(scope: str = 'Local') -> List[Dict]:
432
+ """
433
+ List all IAM policies.
434
+
435
+ Args:
436
+ scope: 'All' for AWS managed + customer managed, 'Local' for customer managed only,
437
+ 'AWS' for AWS managed only
438
+
439
+ Returns:
440
+ List of policy information dictionaries
441
+ """
442
+ client = boto3.client('iam',
443
+ aws_access_key_id=settings.AWS_KEY,
444
+ aws_secret_access_key=settings.AWS_SECRET)
445
+
446
+ try:
447
+ paginator = client.get_paginator('list_policies')
448
+ policies = []
449
+
450
+ for page in paginator.paginate(Scope=scope):
451
+ policies.extend(page['Policies'])
452
+
453
+ return policies
454
+ except botocore.exceptions.ClientError as e:
455
+ logger.error(f"Failed to list policies: {e}")
456
+ return []
457
+
458
+
459
+ class IAMRole(IAMBase):
460
+ """
461
+ Simple interface for IAM role management.
462
+ """
463
+
464
+ def __init__(self, name: str, **kwargs):
465
+ """
466
+ Initialize a role manager for the specified IAM role.
467
+
468
+ Args:
469
+ name: The role name
470
+ **kwargs: Additional arguments for IAMBase
471
+ """
472
+ super().__init__(**kwargs)
473
+ self.name = name
474
+ self.role = self.resource.Role(self.name)
475
+ self.exists = self._check_exists()
476
+
477
+ def _check_exists(self) -> bool:
478
+ """Check if the role exists."""
479
+ try:
480
+ self.role.load()
481
+ return True
482
+ except botocore.exceptions.ClientError as e:
483
+ if e.response['Error']['Code'] == 'NoSuchEntity':
484
+ return False
485
+ # If it's a different error, log and re-raise
486
+ logger.error(f"Error checking role existence: {e}")
487
+ raise
488
+
489
+ def create(self, assume_role_policy_document: Union[Dict, str],
490
+ description: str = '',
491
+ path: str = '/',
492
+ max_session_duration: int = 3600,
493
+ tags: Optional[List[Dict[str, str]]] = None) -> bool:
494
+ """
495
+ Create an IAM role.
496
+
497
+ Args:
498
+ assume_role_policy_document: Trust policy document as dict or JSON string
499
+ description: Role description
500
+ path: Role path
501
+ max_session_duration: Maximum session duration in seconds (3600-43200)
502
+ tags: Optional tags for the role
503
+
504
+ Returns:
505
+ True if role was created, False if it already exists
506
+ """
507
+ if self.exists:
508
+ logger.info(f"Role {self.name} already exists")
509
+ return False
510
+
511
+ try:
512
+ # Convert dict to JSON string if needed
513
+ policy_doc = assume_role_policy_document if isinstance(assume_role_policy_document, str) else json.dumps(assume_role_policy_document)
514
+
515
+ create_params = {
516
+ 'RoleName': self.name,
517
+ 'AssumeRolePolicyDocument': policy_doc,
518
+ 'Description': description,
519
+ 'Path': path,
520
+ 'MaxSessionDuration': max_session_duration
521
+ }
522
+
523
+ if tags:
524
+ create_params['Tags'] = tags
525
+
526
+ self.client.create_role(**create_params)
527
+ self.exists = True
528
+ return True
529
+ except botocore.exceptions.ClientError as e:
530
+ logger.error(f"Failed to create role {self.name}: {e}")
531
+ return False
532
+
533
+ def delete(self) -> bool:
534
+ """
535
+ Delete the IAM role.
536
+
537
+ Returns:
538
+ True if successfully deleted, False otherwise
539
+ """
540
+ if not self.exists:
541
+ logger.info(f"Role {self.name} does not exist")
542
+ return False
543
+
544
+ try:
545
+ # Detach all policies
546
+ for policy in list(self.role.attached_policies.all()):
547
+ self.role.detach_policy(PolicyArn=policy.arn)
548
+
549
+ # Delete all inline policies
550
+ for policy_name in list(self.role.policies.all()):
551
+ policy_name.delete()
552
+
553
+ # Delete the role
554
+ self.role.delete()
555
+ self.exists = False
556
+ return True
557
+ except botocore.exceptions.ClientError as e:
558
+ logger.error(f"Failed to delete role {self.name}: {e}")
559
+ return False
560
+
561
+ def attach_policy(self, policy_arn: str) -> bool:
562
+ """
563
+ Attach a managed policy to the role.
564
+
565
+ Args:
566
+ policy_arn: ARN of the policy to attach
567
+
568
+ Returns:
569
+ True if successful, False otherwise
570
+ """
571
+ if not self.exists:
572
+ logger.warning(f"Role {self.name} does not exist")
573
+ return False
574
+
575
+ try:
576
+ self.client.attach_role_policy(
577
+ RoleName=self.name,
578
+ PolicyArn=policy_arn
579
+ )
580
+ return True
581
+ except botocore.exceptions.ClientError as e:
582
+ logger.error(f"Failed to attach policy to role {self.name}: {e}")
583
+ return False
584
+
585
+ def detach_policy(self, policy_arn: str) -> bool:
586
+ """
587
+ Detach a managed policy from the role.
588
+
589
+ Args:
590
+ policy_arn: ARN of the policy to detach
591
+
592
+ Returns:
593
+ True if successful, False otherwise
594
+ """
595
+ if not self.exists:
596
+ logger.warning(f"Role {self.name} does not exist")
597
+ return False
598
+
599
+ try:
600
+ self.client.detach_role_policy(
601
+ RoleName=self.name,
602
+ PolicyArn=policy_arn
603
+ )
604
+ return True
605
+ except botocore.exceptions.ClientError as e:
606
+ logger.error(f"Failed to detach policy from role {self.name}: {e}")
607
+ return False
608
+
609
+ def update_assume_role_policy(self, policy_document: Union[Dict, str]) -> bool:
610
+ """
611
+ Update the role's trust policy.
612
+
613
+ Args:
614
+ policy_document: New trust policy document as dict or JSON string
615
+
616
+ Returns:
617
+ True if successful, False otherwise
618
+ """
619
+ if not self.exists:
620
+ logger.warning(f"Role {self.name} does not exist")
621
+ return False
622
+
623
+ try:
624
+ # Convert dict to JSON string if needed
625
+ policy_doc = policy_document if isinstance(policy_document, str) else json.dumps(policy_document)
626
+
627
+ self.client.update_assume_role_policy(
628
+ RoleName=self.name,
629
+ PolicyDocument=policy_doc
630
+ )
631
+ return True
632
+ except botocore.exceptions.ClientError as e:
633
+ logger.error(f"Failed to update trust policy for role {self.name}: {e}")
634
+ return False
635
+
636
+ def put_inline_policy(self, policy_name: str, policy_document: Union[Dict, str]) -> bool:
637
+ """
638
+ Create or update an inline policy for the role.
639
+
640
+ Args:
641
+ policy_name: Name of the inline policy
642
+ policy_document: Policy document as dict or JSON string
643
+
644
+ Returns:
645
+ True if successful, False otherwise
646
+ """
647
+ if not self.exists:
648
+ logger.warning(f"Role {self.name} does not exist")
649
+ return False
650
+
651
+ try:
652
+ # Convert dict to JSON string if needed
653
+ policy_doc = policy_document if isinstance(policy_document, str) else json.dumps(policy_document)
654
+
655
+ self.client.put_role_policy(
656
+ RoleName=self.name,
657
+ PolicyName=policy_name,
658
+ PolicyDocument=policy_doc
659
+ )
660
+ return True
661
+ except botocore.exceptions.ClientError as e:
662
+ logger.error(f"Failed to put inline policy for role {self.name}: {e}")
663
+ return False
664
+
665
+ @staticmethod
666
+ def list_all_roles() -> List[Dict]:
667
+ """
668
+ List all IAM roles.
669
+
670
+ Returns:
671
+ List of role information dictionaries
672
+ """
673
+ client = boto3.client('iam',
674
+ aws_access_key_id=settings.AWS_KEY,
675
+ aws_secret_access_key=settings.AWS_SECRET)
676
+
677
+ try:
678
+ paginator = client.get_paginator('list_roles')
679
+ roles = []
680
+
681
+ for page in paginator.paginate():
682
+ roles.extend(page['Roles'])
683
+
684
+ return roles
685
+ except botocore.exceptions.ClientError as e:
686
+ logger.error(f"Failed to list roles: {e}")
687
+ return []
688
+
689
+
690
+ # Utility functions
691
+ def create_service_role(name: str, service: str, managed_policy_arns: Optional[List[str]] = None) -> IAMRole:
692
+ """
693
+ Create a role that can be assumed by an AWS service.
694
+
695
+ Args:
696
+ name: Role name
697
+ service: AWS service identifier (e.g., 'ec2.amazonaws.com')
698
+ managed_policy_arns: List of managed policy ARNs to attach
699
+
700
+ Returns:
701
+ IAMRole instance
702
+ """
703
+ role = IAMRole(name)
704
+
705
+ if not role.exists:
706
+ # Create trust relationship policy document
707
+ trust_policy = {
708
+ "Version": "2012-10-17",
709
+ "Statement": [
710
+ {
711
+ "Effect": "Allow",
712
+ "Principal": {
713
+ "Service": service
714
+ },
715
+ "Action": "sts:AssumeRole"
716
+ }
717
+ ]
718
+ }
719
+
720
+ role.create(
721
+ assume_role_policy_document=trust_policy,
722
+ description=f"Service role for {service}"
723
+ )
724
+
725
+ # Attach managed policies if provided
726
+ if managed_policy_arns:
727
+ for policy_arn in managed_policy_arns:
728
+ role.attach_policy(policy_arn)
729
+
730
+ return role
731
+
732
+
733
+ def get_aws_account_id() -> str:
734
+ """
735
+ Get the AWS account ID.
736
+
737
+ Returns:
738
+ AWS account ID
739
+ """
740
+ client = boto3.client('sts',
741
+ aws_access_key_id=settings.AWS_KEY,
742
+ aws_secret_access_key=settings.AWS_SECRET)
743
+
744
+ try:
745
+ return client.get_caller_identity()['Account']
746
+ except botocore.exceptions.ClientError as e:
747
+ logger.error(f"Failed to get AWS account ID: {e}")
748
+ return ""