granny-devops 0.4.0__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.
- granny/__init__.py +19 -0
- granny/analyze/__init__.py +6 -0
- granny/analyze/lambdas.py +59 -0
- granny/analyze/vpcs.py +57 -0
- granny/cdn/__init__.py +9 -0
- granny/cdn/bunny.py +231 -0
- granny/cli/__init__.py +0 -0
- granny/cli/analyze.py +66 -0
- granny/cli/cdn.py +210 -0
- granny/cli/create.py +94 -0
- granny/cli/credentials.py +99 -0
- granny/cli/dns.py +290 -0
- granny/cli/docker.py +165 -0
- granny/cli/edge.py +106 -0
- granny/cli/email.py +224 -0
- granny/cli/main.py +98 -0
- granny/cli/serverless.py +278 -0
- granny/cli/storage.py +249 -0
- granny/create/__init__.py +4 -0
- granny/create/auto_certificate.py +1899 -0
- granny/create/cloudfront-security-headers.js +53 -0
- granny/create/manage-dns.sh +321 -0
- granny/create/manage_mailjet_contacts.py +619 -0
- granny/create/registrars.py +363 -0
- granny/create/setup_aws_cloudfront.py +2808 -0
- granny/create/setup_bunny_edge_script.py +923 -0
- granny/create/setup_bunny_storage.py +1719 -0
- granny/create/setup_cognito_identity_pool.py +740 -0
- granny/create/setup_hetzner_bunny.py +1482 -0
- granny/create/setup_mailjet_dns.py +1103 -0
- granny/create/setup_private_cdn.py +547 -0
- granny/create/setup_s3_website.py +1512 -0
- granny/create/setup_scaleway_faas.py +1165 -0
- granny/create/setup_workmail.py +1217 -0
- granny/create/www-redirect-function.js +17 -0
- granny/credentials/__init__.py +15 -0
- granny/credentials/secrets.py +403 -0
- granny/dns/__init__.py +22 -0
- granny/dns/base.py +113 -0
- granny/dns/bunny.py +150 -0
- granny/dns/cloudflare.py +192 -0
- granny/dns/cloudns.py +162 -0
- granny/dns/desec.py +152 -0
- granny/dns/factory.py +72 -0
- granny/dns/hetzner.py +165 -0
- granny/dns/manual.py +64 -0
- granny/dns/records.py +29 -0
- granny/docker/__init__.py +5 -0
- granny/docker/build_base.py +204 -0
- granny/edge/__init__.py +5 -0
- granny/edge/bunny.py +147 -0
- granny/email/__init__.py +7 -0
- granny/email/mailjet.py +119 -0
- granny/email/mailjet_contacts.py +115 -0
- granny/email/ses_forwarding.py +281 -0
- granny/email/workmail.py +145 -0
- granny/report.py +128 -0
- granny/serverless/__init__.py +5 -0
- granny/serverless/scaleway.py +264 -0
- granny/storage/__init__.py +7 -0
- granny/storage/aws.py +113 -0
- granny/storage/bunny.py +98 -0
- granny/storage/hetzner.py +118 -0
- granny_devops-0.4.0.dist-info/METADATA +445 -0
- granny_devops-0.4.0.dist-info/RECORD +68 -0
- granny_devops-0.4.0.dist-info/WHEEL +4 -0
- granny_devops-0.4.0.dist-info/entry_points.txt +2 -0
- granny_devops-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
AWS Cognito Identity Pool Setup Script
|
|
4
|
+
|
|
5
|
+
Creates and configures an AWS Cognito Identity Pool that allows unauthenticated
|
|
6
|
+
users to upload files to a specific S3 bucket.
|
|
7
|
+
|
|
8
|
+
This script:
|
|
9
|
+
1. Creates or retrieves a Cognito Identity Pool
|
|
10
|
+
2. Sets up IAM roles for unauthenticated users
|
|
11
|
+
3. Configures S3 bucket policies
|
|
12
|
+
4. Returns the Identity Pool ID for use in client applications
|
|
13
|
+
5. Optionally exports all configuration to JSON file
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
# Create identity pool with S3 bucket access
|
|
17
|
+
python setup_cognito_identity_pool.py --bucket-name my-bucket --pool-name MyAppUploads
|
|
18
|
+
|
|
19
|
+
# Use specific AWS profile
|
|
20
|
+
python setup_cognito_identity_pool.py --bucket-name my-bucket --pool-name MyAppUploads --aws-profile production
|
|
21
|
+
|
|
22
|
+
# Specify allowed upload paths in the bucket
|
|
23
|
+
python setup_cognito_identity_pool.py --bucket-name my-bucket --pool-name MyAppUploads --upload-path uploads/
|
|
24
|
+
|
|
25
|
+
# Fetch existing identity pool ID
|
|
26
|
+
python setup_cognito_identity_pool.py --pool-name MyAppUploads --fetch-only
|
|
27
|
+
|
|
28
|
+
# Export configuration to JSON file
|
|
29
|
+
python setup_cognito_identity_pool.py --bucket-name my-bucket --pool-name MyAppUploads --export
|
|
30
|
+
|
|
31
|
+
# Export to custom directory
|
|
32
|
+
python setup_cognito_identity_pool.py --bucket-name my-bucket --pool-name MyAppUploads --export --export-dir /path/to/output
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import json
|
|
37
|
+
import logging
|
|
38
|
+
import sys
|
|
39
|
+
from datetime import datetime
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Optional, Dict, Any
|
|
42
|
+
# boto3 returns dicts for IAM policy documents, but we also handle raw strings.
|
|
43
|
+
from urllib.parse import unquote
|
|
44
|
+
|
|
45
|
+
import boto3
|
|
46
|
+
|
|
47
|
+
# Default export directory
|
|
48
|
+
DEFAULT_EXPORT_DIR = Path(__file__).parent.parent.parent / "out"
|
|
49
|
+
|
|
50
|
+
# Setup logging
|
|
51
|
+
logging.basicConfig(
|
|
52
|
+
level=logging.INFO,
|
|
53
|
+
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_arguments():
|
|
58
|
+
"""Parse command line arguments."""
|
|
59
|
+
parser = argparse.ArgumentParser(
|
|
60
|
+
description='Setup AWS Cognito Identity Pool for S3 uploads',
|
|
61
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
62
|
+
epilog="""
|
|
63
|
+
Examples:
|
|
64
|
+
# Create new identity pool with S3 access
|
|
65
|
+
python setup_cognito_identity_pool.py --bucket-name my-uploads --pool-name MyAppUploads
|
|
66
|
+
|
|
67
|
+
# Use specific AWS profile and region
|
|
68
|
+
python setup_cognito_identity_pool.py --bucket-name my-uploads --pool-name MyAppUploads --aws-profile prod --region us-west-2
|
|
69
|
+
|
|
70
|
+
# Specify upload path prefix
|
|
71
|
+
python setup_cognito_identity_pool.py --bucket-name my-uploads --pool-name MyAppUploads --upload-path public/uploads/
|
|
72
|
+
|
|
73
|
+
# Fetch existing identity pool ID
|
|
74
|
+
python setup_cognito_identity_pool.py --pool-name MyAppUploads --fetch-only
|
|
75
|
+
|
|
76
|
+
# Allow authenticated access only
|
|
77
|
+
python setup_cognito_identity_pool.py --bucket-name my-uploads --pool-name MyAppUploads --no-unauthenticated
|
|
78
|
+
|
|
79
|
+
Environment Variables:
|
|
80
|
+
AWS_PROFILE: Optional - AWS profile to use
|
|
81
|
+
AWS_REGION: Optional - AWS region (default: us-east-1)
|
|
82
|
+
"""
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
parser.add_argument('--bucket-name',
|
|
86
|
+
help='S3 bucket name for uploads (required unless --fetch-only)')
|
|
87
|
+
parser.add_argument('--pool-name', required=True,
|
|
88
|
+
help='Name for the Cognito Identity Pool')
|
|
89
|
+
parser.add_argument('--region', default='us-east-1',
|
|
90
|
+
help='AWS region (default: us-east-1)')
|
|
91
|
+
parser.add_argument('--aws-profile', default=None,
|
|
92
|
+
help='AWS profile to use for credentials')
|
|
93
|
+
parser.add_argument('--upload-path', default='uploads/',
|
|
94
|
+
help='Path prefix for uploads in the bucket (default: uploads/)')
|
|
95
|
+
parser.add_argument('--no-unauthenticated', action='store_true',
|
|
96
|
+
help='Disable unauthenticated access (only allow authenticated users)')
|
|
97
|
+
parser.add_argument('--fetch-only', action='store_true',
|
|
98
|
+
help='Only fetch existing identity pool ID, don\'t create or update')
|
|
99
|
+
parser.add_argument('--max-file-size-mb', type=int, default=10,
|
|
100
|
+
help='Maximum file size in MB for uploads (default: 10)')
|
|
101
|
+
parser.add_argument('--export', action='store_true',
|
|
102
|
+
help='Export identity pool configuration to JSON file')
|
|
103
|
+
parser.add_argument('--export-dir', type=str, default=str(DEFAULT_EXPORT_DIR),
|
|
104
|
+
help=f'Directory for exported configuration (default: {DEFAULT_EXPORT_DIR})')
|
|
105
|
+
|
|
106
|
+
args = parser.parse_args()
|
|
107
|
+
|
|
108
|
+
# Validate arguments
|
|
109
|
+
if not args.fetch_only and not args.bucket_name:
|
|
110
|
+
parser.error('--bucket-name is required unless --fetch-only is specified')
|
|
111
|
+
|
|
112
|
+
return args
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def create_aws_clients(aws_profile: Optional[str] = None, region: str = 'us-east-1') -> Dict[str, Any]:
|
|
116
|
+
"""Create AWS clients with optional profile support."""
|
|
117
|
+
if aws_profile:
|
|
118
|
+
logging.info(f"Creating AWS clients using profile: {aws_profile}")
|
|
119
|
+
session = boto3.Session(profile_name=aws_profile, region_name=region)
|
|
120
|
+
else:
|
|
121
|
+
logging.info(f"Creating AWS clients using default credentials in region: {region}")
|
|
122
|
+
session = boto3.Session(region_name=region)
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
'cognito_identity': session.client('cognito-identity'),
|
|
126
|
+
'iam': session.client('iam'),
|
|
127
|
+
's3': session.client('s3'),
|
|
128
|
+
'sts': session.client('sts')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_account_id(sts_client) -> str:
|
|
133
|
+
"""Get the AWS account ID."""
|
|
134
|
+
response = sts_client.get_caller_identity()
|
|
135
|
+
return response['Account']
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def find_identity_pool(cognito_client, pool_name: str) -> Optional[Dict[str, Any]]:
|
|
139
|
+
"""Find an existing identity pool by name."""
|
|
140
|
+
logging.info(f"Searching for existing identity pool: '{pool_name}'...")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
response = cognito_client.list_identity_pools(MaxResults=60)
|
|
144
|
+
|
|
145
|
+
for pool in response.get('IdentityPools', []):
|
|
146
|
+
if pool['IdentityPoolName'] == pool_name:
|
|
147
|
+
logging.info(f"Found existing identity pool: {pool['IdentityPoolId']}")
|
|
148
|
+
return pool
|
|
149
|
+
|
|
150
|
+
logging.info(f"No existing identity pool found with name '{pool_name}'")
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logging.error(f"Error searching for identity pool: {e}")
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def create_identity_pool(cognito_client, pool_name: str, allow_unauthenticated: bool = True) -> Dict[str, Any]:
|
|
159
|
+
"""Create a new Cognito Identity Pool."""
|
|
160
|
+
logging.info(f"Creating new identity pool: '{pool_name}'...")
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
response = cognito_client.create_identity_pool(
|
|
164
|
+
IdentityPoolName=pool_name,
|
|
165
|
+
AllowUnauthenticatedIdentities=allow_unauthenticated,
|
|
166
|
+
AllowClassicFlow=False
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
pool_id = response['IdentityPoolId']
|
|
170
|
+
logging.info(f"Identity pool created successfully: {pool_id}")
|
|
171
|
+
return response
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logging.error(f"Error creating identity pool: {e}")
|
|
175
|
+
raise
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def create_iam_role_for_cognito(iam_client, role_name: str, pool_id: str, region: str, account_id: str) -> str:
|
|
179
|
+
"""Create IAM role for Cognito Identity Pool."""
|
|
180
|
+
logging.info(f"Creating IAM role: '{role_name}'...")
|
|
181
|
+
|
|
182
|
+
# Trust policy for Cognito Identity
|
|
183
|
+
trust_policy = {
|
|
184
|
+
"Version": "2012-10-17",
|
|
185
|
+
"Statement": [
|
|
186
|
+
{
|
|
187
|
+
"Effect": "Allow",
|
|
188
|
+
"Principal": {
|
|
189
|
+
"Federated": "cognito-identity.amazonaws.com"
|
|
190
|
+
},
|
|
191
|
+
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
192
|
+
"Condition": {
|
|
193
|
+
"StringEquals": {
|
|
194
|
+
"cognito-identity.amazonaws.com:aud": pool_id
|
|
195
|
+
},
|
|
196
|
+
"ForAnyValue:StringLike": {
|
|
197
|
+
"cognito-identity.amazonaws.com:amr": "unauthenticated"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
# Try to get existing role
|
|
206
|
+
try:
|
|
207
|
+
response = iam_client.get_role(RoleName=role_name)
|
|
208
|
+
role_arn = response['Role']['Arn']
|
|
209
|
+
logging.info(f"IAM role already exists: {role_arn}")
|
|
210
|
+
return role_arn
|
|
211
|
+
except iam_client.exceptions.NoSuchEntityException:
|
|
212
|
+
# Role doesn't exist, create it
|
|
213
|
+
response = iam_client.create_role(
|
|
214
|
+
RoleName=role_name,
|
|
215
|
+
AssumeRolePolicyDocument=json.dumps(trust_policy),
|
|
216
|
+
Description=f"Role for Cognito Identity Pool {pool_id} unauthenticated users"
|
|
217
|
+
)
|
|
218
|
+
role_arn = response['Role']['Arn']
|
|
219
|
+
logging.info(f"IAM role created: {role_arn}")
|
|
220
|
+
return role_arn
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logging.error(f"Error creating IAM role: {e}")
|
|
224
|
+
raise
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _normalize_upload_path(upload_path: str) -> str:
|
|
228
|
+
"""Ensure the upload path has no leading slash and ends with / if not empty."""
|
|
229
|
+
if not upload_path:
|
|
230
|
+
return ""
|
|
231
|
+
|
|
232
|
+
normalized = upload_path.lstrip('/') # remove any leading /
|
|
233
|
+
if normalized and not normalized.endswith('/'):
|
|
234
|
+
normalized += '/'
|
|
235
|
+
return normalized
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _get_policy_arn(account_id: str, policy_name: str) -> str:
|
|
239
|
+
return f"arn:aws:iam::{account_id}:policy/{policy_name}"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _ensure_policy_capacity(iam_client, policy_arn: str):
|
|
243
|
+
"""IAM policies can only have 5 versions. Remove an old non-default version if at capacity."""
|
|
244
|
+
versions = iam_client.list_policy_versions(PolicyArn=policy_arn)['Versions']
|
|
245
|
+
if len(versions) < 5:
|
|
246
|
+
return
|
|
247
|
+
for version in versions:
|
|
248
|
+
if not version.get('IsDefaultVersion'):
|
|
249
|
+
iam_client.delete_policy_version(
|
|
250
|
+
PolicyArn=policy_arn,
|
|
251
|
+
VersionId=version['VersionId']
|
|
252
|
+
)
|
|
253
|
+
logging.info(f"Deleted old policy version {version['VersionId']} to free capacity.")
|
|
254
|
+
return
|
|
255
|
+
logging.warning("All policy versions are default; unable to free capacity automatically.")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _policy_documents_equal(existing_doc, desired_doc: Dict[str, Any]) -> bool:
|
|
259
|
+
if isinstance(existing_doc, dict):
|
|
260
|
+
return existing_doc == desired_doc
|
|
261
|
+
|
|
262
|
+
if isinstance(existing_doc, str):
|
|
263
|
+
try:
|
|
264
|
+
decoded = json.loads(unquote(existing_doc))
|
|
265
|
+
return decoded == desired_doc
|
|
266
|
+
except json.JSONDecodeError:
|
|
267
|
+
return False
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def create_or_update_s3_upload_policy(
|
|
272
|
+
iam_client,
|
|
273
|
+
policy_name: str,
|
|
274
|
+
bucket_name: str,
|
|
275
|
+
upload_path: str,
|
|
276
|
+
account_id: str
|
|
277
|
+
) -> str:
|
|
278
|
+
"""Create IAM policy for S3 uploads."""
|
|
279
|
+
logging.info(f"Creating S3 upload policy: '{policy_name}'...")
|
|
280
|
+
|
|
281
|
+
upload_path = _normalize_upload_path(upload_path)
|
|
282
|
+
|
|
283
|
+
# Policy that allows uploads to specific path
|
|
284
|
+
policy_document = {
|
|
285
|
+
"Version": "2012-10-17",
|
|
286
|
+
"Statement": [
|
|
287
|
+
{
|
|
288
|
+
"Effect": "Allow",
|
|
289
|
+
"Action": [
|
|
290
|
+
"s3:ListBucket"
|
|
291
|
+
],
|
|
292
|
+
"Resource": f"arn:aws:s3:::{bucket_name}",
|
|
293
|
+
"Condition": {
|
|
294
|
+
"StringLike": {
|
|
295
|
+
"s3:prefix": f"{upload_path}*"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"Effect": "Allow",
|
|
301
|
+
"Action": [
|
|
302
|
+
"s3:PutObject",
|
|
303
|
+
"s3:PutObjectAcl"
|
|
304
|
+
],
|
|
305
|
+
"Resource": f"arn:aws:s3:::{bucket_name}/{upload_path}*"
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"Effect": "Allow",
|
|
309
|
+
"Action": [
|
|
310
|
+
"s3:GetObject"
|
|
311
|
+
],
|
|
312
|
+
"Resource": f"arn:aws:s3:::{bucket_name}/{upload_path}*"
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
policy_arn = _get_policy_arn(account_id, policy_name)
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
policy = iam_client.get_policy(PolicyArn=policy_arn)['Policy']
|
|
321
|
+
logging.info(f"Policy already exists: {policy_arn}")
|
|
322
|
+
default_version_id = policy['DefaultVersionId']
|
|
323
|
+
|
|
324
|
+
version = iam_client.get_policy_version(
|
|
325
|
+
PolicyArn=policy_arn,
|
|
326
|
+
VersionId=default_version_id
|
|
327
|
+
)
|
|
328
|
+
existing_doc = version['PolicyVersion']['Document']
|
|
329
|
+
|
|
330
|
+
if _policy_documents_equal(existing_doc, policy_document):
|
|
331
|
+
logging.info("Existing policy document already matches desired permissions.")
|
|
332
|
+
return policy_arn
|
|
333
|
+
|
|
334
|
+
logging.info("Updating existing policy document with latest permissions.")
|
|
335
|
+
_ensure_policy_capacity(iam_client, policy_arn)
|
|
336
|
+
iam_client.create_policy_version(
|
|
337
|
+
PolicyArn=policy_arn,
|
|
338
|
+
PolicyDocument=json.dumps(policy_document),
|
|
339
|
+
SetAsDefault=True
|
|
340
|
+
)
|
|
341
|
+
return policy_arn
|
|
342
|
+
|
|
343
|
+
except iam_client.exceptions.NoSuchEntityException:
|
|
344
|
+
response = iam_client.create_policy(
|
|
345
|
+
PolicyName=policy_name,
|
|
346
|
+
PolicyDocument=json.dumps(policy_document),
|
|
347
|
+
Description=f"Allow uploads to {bucket_name}/{upload_path}"
|
|
348
|
+
)
|
|
349
|
+
policy_arn = response['Policy']['Arn']
|
|
350
|
+
logging.info(f"Policy created: {policy_arn}")
|
|
351
|
+
return policy_arn
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logging.error(f"Error creating policy: {e}")
|
|
355
|
+
raise
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def attach_policy_to_role(iam_client, role_name: str, policy_arn: str):
|
|
359
|
+
"""Attach IAM policy to role."""
|
|
360
|
+
logging.info(f"Attaching policy to role '{role_name}'...")
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
iam_client.attach_role_policy(
|
|
364
|
+
RoleName=role_name,
|
|
365
|
+
PolicyArn=policy_arn
|
|
366
|
+
)
|
|
367
|
+
logging.info("Policy attached successfully")
|
|
368
|
+
except iam_client.exceptions.EntityAlreadyExistsException:
|
|
369
|
+
logging.info("Policy already attached to role")
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logging.error(f"Error attaching policy: {e}")
|
|
372
|
+
raise
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def set_identity_pool_roles(cognito_client, pool_id: str, role_arn: str, allow_unauthenticated: bool = True):
|
|
376
|
+
"""Set IAM roles for the identity pool."""
|
|
377
|
+
logging.info(f"Setting identity pool roles for {pool_id}...")
|
|
378
|
+
|
|
379
|
+
roles = {}
|
|
380
|
+
if allow_unauthenticated:
|
|
381
|
+
roles['unauthenticated'] = role_arn
|
|
382
|
+
roles['authenticated'] = role_arn # Use same role for both for simplicity
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
cognito_client.set_identity_pool_roles(
|
|
386
|
+
IdentityPoolId=pool_id,
|
|
387
|
+
Roles=roles
|
|
388
|
+
)
|
|
389
|
+
logging.info("Identity pool roles configured successfully")
|
|
390
|
+
except Exception as e:
|
|
391
|
+
logging.error(f"Error setting identity pool roles: {e}")
|
|
392
|
+
raise
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def configure_s3_cors(s3_client, bucket_name: str):
|
|
396
|
+
"""Configure CORS on S3 bucket to allow uploads from web browsers."""
|
|
397
|
+
logging.info(f"Configuring CORS for bucket '{bucket_name}'...")
|
|
398
|
+
|
|
399
|
+
cors_configuration = {
|
|
400
|
+
'CORSRules': [
|
|
401
|
+
{
|
|
402
|
+
'AllowedHeaders': ['*'],
|
|
403
|
+
'AllowedMethods': ['GET', 'PUT', 'POST', 'DELETE', 'HEAD'],
|
|
404
|
+
'AllowedOrigins': ['*'],
|
|
405
|
+
'ExposeHeaders': ['ETag'],
|
|
406
|
+
'MaxAgeSeconds': 3000
|
|
407
|
+
}
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
s3_client.put_bucket_cors(
|
|
413
|
+
Bucket=bucket_name,
|
|
414
|
+
CORSConfiguration=cors_configuration
|
|
415
|
+
)
|
|
416
|
+
logging.info("CORS configuration applied successfully")
|
|
417
|
+
except Exception as e:
|
|
418
|
+
logging.error(f"Error configuring CORS: {e}")
|
|
419
|
+
raise
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def get_identity_pool_details(cognito_client, pool_id: str) -> Dict[str, Any]:
|
|
423
|
+
"""Get detailed information about an identity pool."""
|
|
424
|
+
try:
|
|
425
|
+
response = cognito_client.describe_identity_pool(IdentityPoolId=pool_id)
|
|
426
|
+
return response
|
|
427
|
+
except Exception as e:
|
|
428
|
+
logging.error(f"Error getting identity pool details: {e}")
|
|
429
|
+
raise
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def get_identity_pool_roles(cognito_client, pool_id: str) -> Dict[str, Any]:
|
|
433
|
+
"""Get IAM roles associated with an identity pool."""
|
|
434
|
+
try:
|
|
435
|
+
response = cognito_client.get_identity_pool_roles(IdentityPoolId=pool_id)
|
|
436
|
+
return response
|
|
437
|
+
except Exception as e:
|
|
438
|
+
logging.error(f"Error getting identity pool roles: {e}")
|
|
439
|
+
return {}
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def export_configuration(
|
|
443
|
+
export_dir: str,
|
|
444
|
+
pool_id: str,
|
|
445
|
+
pool_name: str,
|
|
446
|
+
region: str,
|
|
447
|
+
account_id: str,
|
|
448
|
+
bucket_name: Optional[str] = None,
|
|
449
|
+
upload_path: Optional[str] = None,
|
|
450
|
+
role_arn: Optional[str] = None,
|
|
451
|
+
policy_arn: Optional[str] = None,
|
|
452
|
+
allow_unauthenticated: bool = True,
|
|
453
|
+
pool_details: Optional[Dict[str, Any]] = None,
|
|
454
|
+
pool_roles: Optional[Dict[str, Any]] = None
|
|
455
|
+
) -> str:
|
|
456
|
+
"""Export identity pool configuration to JSON file."""
|
|
457
|
+
export_path = Path(export_dir)
|
|
458
|
+
export_path.mkdir(parents=True, exist_ok=True)
|
|
459
|
+
|
|
460
|
+
# Create sanitized filename from pool name
|
|
461
|
+
safe_pool_name = pool_name.replace(' ', '_').replace('-', '_').lower()
|
|
462
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
463
|
+
filename = f"cognito_identity_pool_{safe_pool_name}_{timestamp}.json"
|
|
464
|
+
filepath = export_path / filename
|
|
465
|
+
|
|
466
|
+
# Build configuration object
|
|
467
|
+
config = {
|
|
468
|
+
"metadata": {
|
|
469
|
+
"exported_at": datetime.now().isoformat(),
|
|
470
|
+
"script_version": "1.0.0",
|
|
471
|
+
"description": "AWS Cognito Identity Pool configuration for S3 uploads"
|
|
472
|
+
},
|
|
473
|
+
"identity_pool": {
|
|
474
|
+
"id": pool_id,
|
|
475
|
+
"name": pool_name,
|
|
476
|
+
"region": region,
|
|
477
|
+
"allow_unauthenticated": allow_unauthenticated,
|
|
478
|
+
"arn": f"arn:aws:cognito-identity:{region}:{account_id}:identitypool/{pool_id}"
|
|
479
|
+
},
|
|
480
|
+
"aws": {
|
|
481
|
+
"account_id": account_id,
|
|
482
|
+
"region": region
|
|
483
|
+
},
|
|
484
|
+
"iam": {
|
|
485
|
+
"role_arn": role_arn,
|
|
486
|
+
"policy_arn": policy_arn
|
|
487
|
+
},
|
|
488
|
+
"s3": {
|
|
489
|
+
"bucket_name": bucket_name,
|
|
490
|
+
"upload_path": upload_path,
|
|
491
|
+
"bucket_arn": f"arn:aws:s3:::{bucket_name}" if bucket_name else None,
|
|
492
|
+
"upload_resource_arn": f"arn:aws:s3:::{bucket_name}/{upload_path}*" if bucket_name and upload_path else None
|
|
493
|
+
},
|
|
494
|
+
"client_config": {
|
|
495
|
+
"javascript_sdk_v3": {
|
|
496
|
+
"region": region,
|
|
497
|
+
"identity_pool_id": pool_id,
|
|
498
|
+
"imports": [
|
|
499
|
+
"@aws-sdk/client-cognito-identity",
|
|
500
|
+
"@aws-sdk/credential-provider-cognito-identity",
|
|
501
|
+
"@aws-sdk/client-s3"
|
|
502
|
+
]
|
|
503
|
+
},
|
|
504
|
+
"javascript_sdk_v2": {
|
|
505
|
+
"region": region,
|
|
506
|
+
"identity_pool_id": pool_id
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
"endpoints": {
|
|
510
|
+
"cognito_identity": f"https://cognito-identity.{region}.amazonaws.com",
|
|
511
|
+
"s3": f"https://s3.{region}.amazonaws.com" if region != "us-east-1" else "https://s3.amazonaws.com",
|
|
512
|
+
"s3_bucket": f"https://{bucket_name}.s3.{region}.amazonaws.com" if bucket_name else None
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
# Add detailed pool information if available
|
|
517
|
+
if pool_details:
|
|
518
|
+
# Remove ResponseMetadata from details
|
|
519
|
+
clean_details = {k: v for k, v in pool_details.items() if k != 'ResponseMetadata'}
|
|
520
|
+
config["identity_pool_details"] = clean_details
|
|
521
|
+
|
|
522
|
+
# Add role mappings if available
|
|
523
|
+
if pool_roles:
|
|
524
|
+
clean_roles = {k: v for k, v in pool_roles.items() if k != 'ResponseMetadata'}
|
|
525
|
+
config["identity_pool_roles"] = clean_roles
|
|
526
|
+
|
|
527
|
+
# Write to file
|
|
528
|
+
with open(filepath, 'w', encoding='utf-8') as f:
|
|
529
|
+
json.dump(config, f, indent=2, default=str)
|
|
530
|
+
|
|
531
|
+
logging.info(f"Configuration exported to: {filepath}")
|
|
532
|
+
return str(filepath)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def print_configuration_summary(pool_id: str, pool_name: str, region: str, bucket_name: Optional[str] = None, upload_path: Optional[str] = None):
|
|
536
|
+
"""Print configuration summary and usage instructions."""
|
|
537
|
+
logging.info("\n" + "="*60)
|
|
538
|
+
logging.info("COGNITO IDENTITY POOL CONFIGURATION COMPLETE")
|
|
539
|
+
logging.info("="*60)
|
|
540
|
+
logging.info(f"Identity Pool Name: {pool_name}")
|
|
541
|
+
logging.info(f"Identity Pool ID: {pool_id}")
|
|
542
|
+
logging.info(f"Region: {region}")
|
|
543
|
+
|
|
544
|
+
if bucket_name:
|
|
545
|
+
logging.info(f"S3 Bucket: {bucket_name}")
|
|
546
|
+
logging.info(f"Upload Path: {upload_path}")
|
|
547
|
+
|
|
548
|
+
logging.info("\n" + "-"*60)
|
|
549
|
+
logging.info("CLIENT CONFIGURATION (JavaScript/Web)")
|
|
550
|
+
logging.info("-"*60)
|
|
551
|
+
|
|
552
|
+
print(f"""
|
|
553
|
+
// AWS SDK v3 (recommended)
|
|
554
|
+
import {{ CognitoIdentityClient }} from "@aws-sdk/client-cognito-identity";
|
|
555
|
+
import {{ fromCognitoIdentityPool }} from "@aws-sdk/credential-provider-cognito-identity";
|
|
556
|
+
import {{ S3Client, PutObjectCommand }} from "@aws-sdk/client-s3";
|
|
557
|
+
|
|
558
|
+
const s3Client = new S3Client({{
|
|
559
|
+
region: "{region}",
|
|
560
|
+
credentials: fromCognitoIdentityPool({{
|
|
561
|
+
client: new CognitoIdentityClient({{ region: "{region}" }}),
|
|
562
|
+
identityPoolId: "{pool_id}",
|
|
563
|
+
}})
|
|
564
|
+
}});
|
|
565
|
+
|
|
566
|
+
// Upload file
|
|
567
|
+
async function uploadFile(file) {{
|
|
568
|
+
const params = {{
|
|
569
|
+
Bucket: "{bucket_name or 'YOUR_BUCKET'}",
|
|
570
|
+
Key: "{upload_path or 'uploads/'}${{file.name}}",
|
|
571
|
+
Body: file,
|
|
572
|
+
ContentType: file.type
|
|
573
|
+
}};
|
|
574
|
+
|
|
575
|
+
const command = new PutObjectCommand(params);
|
|
576
|
+
const response = await s3Client.send(command);
|
|
577
|
+
return response;
|
|
578
|
+
}}
|
|
579
|
+
""")
|
|
580
|
+
|
|
581
|
+
logging.info("-"*60)
|
|
582
|
+
logging.info("AWS SDK v2 (older version)")
|
|
583
|
+
logging.info("-"*60)
|
|
584
|
+
|
|
585
|
+
print(f"""
|
|
586
|
+
AWS.config.region = '{region}';
|
|
587
|
+
AWS.config.credentials = new AWS.CognitoIdentityCredentials({{
|
|
588
|
+
IdentityPoolId: '{pool_id}'
|
|
589
|
+
}});
|
|
590
|
+
|
|
591
|
+
const s3 = new AWS.S3();
|
|
592
|
+
|
|
593
|
+
// Upload file
|
|
594
|
+
function uploadFile(file) {{
|
|
595
|
+
const params = {{
|
|
596
|
+
Bucket: '{bucket_name or "YOUR_BUCKET"}',
|
|
597
|
+
Key: '{upload_path or "uploads/"}' + file.name,
|
|
598
|
+
Body: file,
|
|
599
|
+
ContentType: file.type
|
|
600
|
+
}};
|
|
601
|
+
|
|
602
|
+
return s3.upload(params).promise();
|
|
603
|
+
}}
|
|
604
|
+
""")
|
|
605
|
+
|
|
606
|
+
logging.info("="*60)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def main():
|
|
610
|
+
"""Main function to orchestrate the create."""
|
|
611
|
+
args = parse_arguments()
|
|
612
|
+
|
|
613
|
+
try:
|
|
614
|
+
# Create AWS clients
|
|
615
|
+
clients = create_aws_clients(args.aws_profile, args.region)
|
|
616
|
+
cognito_client = clients['cognito_identity']
|
|
617
|
+
iam_client = clients['iam']
|
|
618
|
+
s3_client = clients['s3']
|
|
619
|
+
sts_client = clients['sts']
|
|
620
|
+
|
|
621
|
+
# Get AWS account ID
|
|
622
|
+
account_id = get_account_id(sts_client)
|
|
623
|
+
logging.info(f"AWS Account ID: {account_id}")
|
|
624
|
+
|
|
625
|
+
# Find or create identity pool
|
|
626
|
+
existing_pool = find_identity_pool(cognito_client, args.pool_name)
|
|
627
|
+
|
|
628
|
+
# Track created resources for export
|
|
629
|
+
role_arn = None
|
|
630
|
+
policy_arn = None
|
|
631
|
+
|
|
632
|
+
if existing_pool:
|
|
633
|
+
pool_id = existing_pool['IdentityPoolId']
|
|
634
|
+
|
|
635
|
+
if args.fetch_only:
|
|
636
|
+
logging.info(f"Fetch-only mode: Found identity pool {pool_id}")
|
|
637
|
+
pool_details = get_identity_pool_details(cognito_client, pool_id)
|
|
638
|
+
pool_roles = get_identity_pool_roles(cognito_client, pool_id)
|
|
639
|
+
print_configuration_summary(
|
|
640
|
+
pool_id=pool_id,
|
|
641
|
+
pool_name=args.pool_name,
|
|
642
|
+
region=args.region
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
# Export if requested
|
|
646
|
+
if args.export:
|
|
647
|
+
export_configuration(
|
|
648
|
+
export_dir=args.export_dir,
|
|
649
|
+
pool_id=pool_id,
|
|
650
|
+
pool_name=args.pool_name,
|
|
651
|
+
region=args.region,
|
|
652
|
+
account_id=account_id,
|
|
653
|
+
allow_unauthenticated=pool_details.get('AllowUnauthenticatedIdentities', True),
|
|
654
|
+
pool_details=pool_details,
|
|
655
|
+
pool_roles=pool_roles
|
|
656
|
+
)
|
|
657
|
+
return 0
|
|
658
|
+
|
|
659
|
+
logging.info(f"Using existing identity pool: {pool_id}")
|
|
660
|
+
else:
|
|
661
|
+
if args.fetch_only:
|
|
662
|
+
logging.error(f"Identity pool '{args.pool_name}' not found")
|
|
663
|
+
return 1
|
|
664
|
+
|
|
665
|
+
# Create new identity pool
|
|
666
|
+
allow_unauth = not args.no_unauthenticated
|
|
667
|
+
pool = create_identity_pool(cognito_client, args.pool_name, allow_unauth)
|
|
668
|
+
pool_id = pool['IdentityPoolId']
|
|
669
|
+
|
|
670
|
+
# If not fetch-only, configure IAM roles and S3
|
|
671
|
+
if not args.fetch_only:
|
|
672
|
+
normalized_upload_path = _normalize_upload_path(args.upload_path)
|
|
673
|
+
|
|
674
|
+
# Create IAM role
|
|
675
|
+
role_name = f"Cognito_{args.pool_name.replace(' ', '_')}_Unauth_Role"
|
|
676
|
+
role_arn = create_iam_role_for_cognito(
|
|
677
|
+
iam_client,
|
|
678
|
+
role_name,
|
|
679
|
+
pool_id,
|
|
680
|
+
args.region,
|
|
681
|
+
account_id
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
# Create S3 upload policy
|
|
685
|
+
policy_name = f"Cognito_{args.pool_name.replace(' ', '_')}_S3_Upload"
|
|
686
|
+
policy_arn = create_or_update_s3_upload_policy(
|
|
687
|
+
iam_client,
|
|
688
|
+
policy_name,
|
|
689
|
+
args.bucket_name,
|
|
690
|
+
normalized_upload_path,
|
|
691
|
+
account_id
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
# Attach policy to role
|
|
695
|
+
attach_policy_to_role(iam_client, role_name, policy_arn)
|
|
696
|
+
|
|
697
|
+
# Set identity pool roles
|
|
698
|
+
allow_unauth = not args.no_unauthenticated
|
|
699
|
+
set_identity_pool_roles(cognito_client, pool_id, role_arn, allow_unauth)
|
|
700
|
+
|
|
701
|
+
# Configure S3 CORS
|
|
702
|
+
configure_s3_cors(s3_client, args.bucket_name)
|
|
703
|
+
|
|
704
|
+
# Print summary
|
|
705
|
+
print_configuration_summary(
|
|
706
|
+
pool_id=pool_id,
|
|
707
|
+
pool_name=args.pool_name,
|
|
708
|
+
region=args.region,
|
|
709
|
+
bucket_name=args.bucket_name,
|
|
710
|
+
upload_path=normalized_upload_path
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
# Export configuration if requested
|
|
714
|
+
if args.export:
|
|
715
|
+
pool_details = get_identity_pool_details(cognito_client, pool_id)
|
|
716
|
+
pool_roles = get_identity_pool_roles(cognito_client, pool_id)
|
|
717
|
+
export_configuration(
|
|
718
|
+
export_dir=args.export_dir,
|
|
719
|
+
pool_id=pool_id,
|
|
720
|
+
pool_name=args.pool_name,
|
|
721
|
+
region=args.region,
|
|
722
|
+
account_id=account_id,
|
|
723
|
+
bucket_name=args.bucket_name,
|
|
724
|
+
upload_path=normalized_upload_path,
|
|
725
|
+
role_arn=role_arn,
|
|
726
|
+
policy_arn=policy_arn,
|
|
727
|
+
allow_unauthenticated=allow_unauth,
|
|
728
|
+
pool_details=pool_details,
|
|
729
|
+
pool_roles=pool_roles
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
return 0
|
|
733
|
+
|
|
734
|
+
except Exception as e:
|
|
735
|
+
logging.error(f"Setup failed: {e}")
|
|
736
|
+
return 1
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
if __name__ == "__main__":
|
|
740
|
+
sys.exit(main())
|