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.
Files changed (68) hide show
  1. granny/__init__.py +19 -0
  2. granny/analyze/__init__.py +6 -0
  3. granny/analyze/lambdas.py +59 -0
  4. granny/analyze/vpcs.py +57 -0
  5. granny/cdn/__init__.py +9 -0
  6. granny/cdn/bunny.py +231 -0
  7. granny/cli/__init__.py +0 -0
  8. granny/cli/analyze.py +66 -0
  9. granny/cli/cdn.py +210 -0
  10. granny/cli/create.py +94 -0
  11. granny/cli/credentials.py +99 -0
  12. granny/cli/dns.py +290 -0
  13. granny/cli/docker.py +165 -0
  14. granny/cli/edge.py +106 -0
  15. granny/cli/email.py +224 -0
  16. granny/cli/main.py +98 -0
  17. granny/cli/serverless.py +278 -0
  18. granny/cli/storage.py +249 -0
  19. granny/create/__init__.py +4 -0
  20. granny/create/auto_certificate.py +1899 -0
  21. granny/create/cloudfront-security-headers.js +53 -0
  22. granny/create/manage-dns.sh +321 -0
  23. granny/create/manage_mailjet_contacts.py +619 -0
  24. granny/create/registrars.py +363 -0
  25. granny/create/setup_aws_cloudfront.py +2808 -0
  26. granny/create/setup_bunny_edge_script.py +923 -0
  27. granny/create/setup_bunny_storage.py +1719 -0
  28. granny/create/setup_cognito_identity_pool.py +740 -0
  29. granny/create/setup_hetzner_bunny.py +1482 -0
  30. granny/create/setup_mailjet_dns.py +1103 -0
  31. granny/create/setup_private_cdn.py +547 -0
  32. granny/create/setup_s3_website.py +1512 -0
  33. granny/create/setup_scaleway_faas.py +1165 -0
  34. granny/create/setup_workmail.py +1217 -0
  35. granny/create/www-redirect-function.js +17 -0
  36. granny/credentials/__init__.py +15 -0
  37. granny/credentials/secrets.py +403 -0
  38. granny/dns/__init__.py +22 -0
  39. granny/dns/base.py +113 -0
  40. granny/dns/bunny.py +150 -0
  41. granny/dns/cloudflare.py +192 -0
  42. granny/dns/cloudns.py +162 -0
  43. granny/dns/desec.py +152 -0
  44. granny/dns/factory.py +72 -0
  45. granny/dns/hetzner.py +165 -0
  46. granny/dns/manual.py +64 -0
  47. granny/dns/records.py +29 -0
  48. granny/docker/__init__.py +5 -0
  49. granny/docker/build_base.py +204 -0
  50. granny/edge/__init__.py +5 -0
  51. granny/edge/bunny.py +147 -0
  52. granny/email/__init__.py +7 -0
  53. granny/email/mailjet.py +119 -0
  54. granny/email/mailjet_contacts.py +115 -0
  55. granny/email/ses_forwarding.py +281 -0
  56. granny/email/workmail.py +145 -0
  57. granny/report.py +128 -0
  58. granny/serverless/__init__.py +5 -0
  59. granny/serverless/scaleway.py +264 -0
  60. granny/storage/__init__.py +7 -0
  61. granny/storage/aws.py +113 -0
  62. granny/storage/bunny.py +98 -0
  63. granny/storage/hetzner.py +118 -0
  64. granny_devops-0.4.0.dist-info/METADATA +445 -0
  65. granny_devops-0.4.0.dist-info/RECORD +68 -0
  66. granny_devops-0.4.0.dist-info/WHEEL +4 -0
  67. granny_devops-0.4.0.dist-info/entry_points.txt +2 -0
  68. 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())