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.
- django_nativemojo-0.1.15.dist-info/METADATA +136 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +531 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/models/group.py +25 -7
- mojo/apps/account/models/member.py +15 -4
- mojo/apps/account/models/user.py +197 -20
- mojo/apps/account/rest/group.py +1 -0
- mojo/apps/account/rest/user.py +6 -2
- mojo/apps/aws/rest/__init__.py +1 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +200 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +204 -58
- mojo/apps/fileman/models/manager.py +161 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +1 -1
- mojo/apps/incident/reporter.py +3 -1
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +4 -1
- mojo/apps/metrics/utils.py +2 -2
- mojo/apps/notify/handlers/ses/message.py +1 -1
- mojo/apps/notify/providers/aws.py +2 -2
- mojo/apps/tasks/__init__.py +34 -1
- mojo/apps/tasks/manager.py +200 -45
- mojo/apps/tasks/rest/tasks.py +24 -10
- mojo/apps/tasks/runner.py +283 -18
- mojo/apps/tasks/task.py +99 -0
- mojo/apps/tasks/tq_handlers.py +118 -0
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +7 -2
- mojo/helpers/aws/__init__.py +41 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/response.py +6 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/logging.py +1 -1
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +261 -46
- mojo/models/secrets.py +13 -4
- mojo/serializers/__init__.py +100 -0
- mojo/serializers/advanced/README.md +363 -0
- mojo/serializers/advanced/__init__.py +247 -0
- mojo/serializers/advanced/formats/__init__.py +28 -0
- mojo/serializers/advanced/formats/csv.py +416 -0
- mojo/serializers/advanced/formats/excel.py +516 -0
- mojo/serializers/advanced/formats/json.py +239 -0
- mojo/serializers/advanced/formats/localizers.py +509 -0
- mojo/serializers/advanced/formats/response.py +485 -0
- mojo/serializers/advanced/serializer.py +568 -0
- mojo/serializers/manager.py +501 -0
- mojo/serializers/optimized.py +618 -0
- mojo/serializers/settings_example.py +322 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- testit/helpers.py +21 -4
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
- /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
- /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
mojo/helpers/aws/iam.py
ADDED
@@ -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 ""
|