raindancers-cloudfront 0.0.0 → 0.0.1

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 (36) hide show
  1. package/lib/bicep/azure-functions/custom-claims-provider/function_app.py +164 -0
  2. package/lib/bicep/azure-functions/custom-claims-provider/host.json +10 -0
  3. package/lib/bicep/azure-functions/custom-claims-provider/requirements.txt +8 -0
  4. package/lib/bicep/deploy/lambda/Dockerfile +10 -0
  5. package/lib/bicep/deploy/lambda/index.py +341 -0
  6. package/lib/bicep/patterns/scripts/LOCALDEBUGREADME.md +86 -0
  7. package/lib/bicep/patterns/scripts/wire-custom-claims-extension.sh +122 -0
  8. package/lib/bicep/patterns/scripts/wire-custom-claims-extension.sh.bak +118 -0
  9. package/lib/cloudfront/cloudfront-functions/auth-check.js +220 -0
  10. package/lib/cloudfront/cloudfront-functions/modules/auth-check.js +263 -0
  11. package/lib/cloudfront/cloudfront-functions/modules/cognito-auth-check.js +223 -0
  12. package/lib/cloudfront/cloudfront-functions/modules/shared-utils.js +47 -0
  13. package/lib/cloudfront/cloudfront-functions/modules/tls-check.js +39 -0
  14. package/lib/cloudfront/cloudfront-functions/modules/url-rewrite.js +6 -0
  15. package/lib/cloudfront/cloudfront-functions/role-enforcer.js +93 -0
  16. package/lib/cloudfront/cloudfront-functions/tls-enforcer.js +55 -0
  17. package/lib/cloudfront/cloudfront-functions/userinfo-endpoint.js +208 -0
  18. package/lib/cloudfront/lambda/certificate/index.py +219 -0
  19. package/lib/cloudfront/lambda/cognito-auth/oauth-callback.py +215 -0
  20. package/lib/cloudfront/lambda/cognito-auth/requirements.txt +3 -0
  21. package/lib/cloudfront/lambda/edge-auth/config.py +6 -0
  22. package/lib/cloudfront/lambda/edge-auth/config_generated.py +12 -0
  23. package/lib/cloudfront/lambda/edge-auth/oauth-callback.py +538 -0
  24. package/lib/cloudfront/lambda/edge-auth/requirements.txt +3 -0
  25. package/lib/cloudfront/lambda/hmacSecret/index.py +129 -0
  26. package/lib/cloudfront/lambda/hmacSecret/requirements.txt +1 -0
  27. package/lib/cloudfront/lambda/jwt-decoder/index.py +88 -0
  28. package/lib/cloudfront/lambda/pre-token/index.py +11 -0
  29. package/lib/cloudfront/lambda/rotateSecret/index.py +86 -0
  30. package/lib/cloudfront/lambda/session-revocation/index.py +80 -0
  31. package/lib/cloudfront/lambda/ssm-writer/index.py +44 -0
  32. package/lib/cloudfront/lambda/stream-processor/index.py +53 -0
  33. package/lib/cloudfront/lambda/webacl/index.py +356 -0
  34. package/lib/cloudfront/logging/README.md +144 -0
  35. package/lib/cloudfront/patterns/SPLIT_STACK_USAGE.md +138 -0
  36. package/package.json +1 -1
@@ -0,0 +1,356 @@
1
+ import json
2
+ import boto3
3
+ from typing import Any, Dict, List
4
+
5
+ wafv2 = boto3.client('wafv2', region_name='us-east-1')
6
+
7
+ def handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
8
+ """
9
+ Custom resource handler for creating WAF WebACLs in us-east-1.
10
+ """
11
+ print(f"Event: {json.dumps(event)}")
12
+
13
+ try:
14
+ request_type = event['RequestType']
15
+ props = event['ResourceProperties']
16
+
17
+ name = props['Name']
18
+ enable_managed_rules = props.get('EnableManagedRules', 'true')
19
+ # Convert string to boolean
20
+ if isinstance(enable_managed_rules, str):
21
+ enable_managed_rules = enable_managed_rules.lower() == 'true'
22
+ rate_limit = int(props.get('RateLimit', 2000))
23
+ path_rate_limits_str = props.get('PathRateLimits', '[]')
24
+ path_rate_limits = json.loads(path_rate_limits_str) if isinstance(path_rate_limits_str, str) else path_rate_limits_str
25
+ allowed_countries = props.get('AllowedCountries', [])
26
+ blocked_countries = props.get('BlockedCountries', [])
27
+
28
+ print(f"Processing {request_type} for WebACL: {name}")
29
+
30
+ if request_type == 'Create':
31
+ return create_webacl(name, enable_managed_rules, rate_limit, path_rate_limits, allowed_countries, blocked_countries)
32
+ elif request_type == 'Update':
33
+ webacl_id = event['PhysicalResourceId']
34
+ return update_webacl(webacl_id, name, enable_managed_rules, rate_limit, path_rate_limits, allowed_countries, blocked_countries)
35
+ elif request_type == 'Delete':
36
+ webacl_id = event['PhysicalResourceId']
37
+ return delete_webacl(webacl_id, name)
38
+
39
+ return {
40
+ 'PhysicalResourceId': event.get('PhysicalResourceId', 'unknown'),
41
+ 'Data': {}
42
+ }
43
+ except Exception as e:
44
+ print(f"ERROR: {str(e)}")
45
+ import traceback
46
+ traceback.print_exc()
47
+ raise
48
+
49
+ def create_webacl(name: str, enable_managed_rules: bool, rate_limit: int, path_rate_limits: List[Dict[str, Any]], allowed_countries: List[str], blocked_countries: List[str]) -> Dict[str, Any]:
50
+ """Create a WAF WebACL."""
51
+
52
+ rules = build_rules(enable_managed_rules, rate_limit, path_rate_limits, allowed_countries, blocked_countries)
53
+
54
+ print(f"Creating WebACL with {len(rules)} rules")
55
+
56
+ response = wafv2.create_web_acl(
57
+ Name=name,
58
+ Scope='CLOUDFRONT',
59
+ DefaultAction={'Allow': {}},
60
+ Rules=rules,
61
+ VisibilityConfig={
62
+ 'SampledRequestsEnabled': True,
63
+ 'CloudWatchMetricsEnabled': True,
64
+ 'MetricName': name
65
+ }
66
+ )
67
+
68
+ webacl_arn = response['Summary']['ARN']
69
+ webacl_id = response['Summary']['Id']
70
+
71
+ print(f"WebACL created: {webacl_arn}")
72
+
73
+ return {
74
+ 'PhysicalResourceId': webacl_id,
75
+ 'Data': {
76
+ 'WebAclArn': webacl_arn,
77
+ 'WebAclId': webacl_id
78
+ }
79
+ }
80
+
81
+ def update_webacl(webacl_id: str, name: str, enable_managed_rules: bool, rate_limit: int, path_rate_limits: List[Dict[str, Any]], allowed_countries: List[str], blocked_countries: List[str]) -> Dict[str, Any]:
82
+ """Update a WAF WebACL."""
83
+
84
+ # Get current WebACL to get lock token
85
+ response = wafv2.get_web_acl(
86
+ Name=name,
87
+ Scope='CLOUDFRONT',
88
+ Id=webacl_id
89
+ )
90
+
91
+ lock_token = response['LockToken']
92
+ webacl_arn = response['WebACL']['ARN']
93
+
94
+ rules = build_rules(enable_managed_rules, rate_limit, path_rate_limits, allowed_countries, blocked_countries)
95
+
96
+ print(f"Updating WebACL with {len(rules)} rules")
97
+
98
+ wafv2.update_web_acl(
99
+ Name=name,
100
+ Scope='CLOUDFRONT',
101
+ Id=webacl_id,
102
+ DefaultAction={'Allow': {}},
103
+ Rules=rules,
104
+ VisibilityConfig={
105
+ 'SampledRequestsEnabled': True,
106
+ 'CloudWatchMetricsEnabled': True,
107
+ 'MetricName': name
108
+ },
109
+ LockToken=lock_token
110
+ )
111
+
112
+ print(f"WebACL updated: {webacl_arn}")
113
+
114
+ return {
115
+ 'PhysicalResourceId': webacl_id,
116
+ 'Data': {
117
+ 'WebAclArn': webacl_arn,
118
+ 'WebAclId': webacl_id
119
+ }
120
+ }
121
+
122
+ def delete_webacl(webacl_id: str, name: str) -> Dict[str, Any]:
123
+ """Delete a WAF WebACL."""
124
+
125
+ try:
126
+ # Get current WebACL to get lock token
127
+ response = wafv2.get_web_acl(
128
+ Name=name,
129
+ Scope='CLOUDFRONT',
130
+ Id=webacl_id
131
+ )
132
+
133
+ lock_token = response['LockToken']
134
+
135
+ wafv2.delete_web_acl(
136
+ Name=name,
137
+ Scope='CLOUDFRONT',
138
+ Id=webacl_id,
139
+ LockToken=lock_token
140
+ )
141
+
142
+ print(f"WebACL deleted: {webacl_id}")
143
+ except wafv2.exceptions.WAFNonexistentItemException:
144
+ print(f"WebACL not found: {webacl_id}")
145
+ except Exception as e:
146
+ print(f"Error deleting WebACL: {e}")
147
+ import traceback
148
+ traceback.print_exc()
149
+ # Don't raise on delete - best effort cleanup
150
+
151
+ return {
152
+ 'PhysicalResourceId': webacl_id,
153
+ 'Data': {}
154
+ }
155
+
156
+ def build_rules(enable_managed_rules: bool, rate_limit: int, path_rate_limits: List[Dict[str, Any]], allowed_countries: List[str], blocked_countries: List[str]) -> List[Dict[str, Any]]:
157
+ """Build the rules list for the WebACL."""
158
+
159
+ rules = []
160
+ priority = 0
161
+
162
+ # Geo-blocking rules (highest priority)
163
+ if allowed_countries:
164
+ print(f"Adding geo-allow rule for countries: {allowed_countries}")
165
+ rules.append({
166
+ 'Name': 'GeoAllowRule',
167
+ 'Priority': priority,
168
+ 'Statement': {
169
+ 'NotStatement': {
170
+ 'Statement': {
171
+ 'GeoMatchStatement': {
172
+ 'CountryCodes': allowed_countries
173
+ }
174
+ }
175
+ }
176
+ },
177
+ 'Action': {
178
+ 'Block': {}
179
+ },
180
+ 'VisibilityConfig': {
181
+ 'SampledRequestsEnabled': True,
182
+ 'CloudWatchMetricsEnabled': True,
183
+ 'MetricName': 'GeoAllowRule'
184
+ }
185
+ })
186
+ priority += 1
187
+
188
+ if blocked_countries:
189
+ print(f"Adding geo-block rule for countries: {blocked_countries}")
190
+ rules.append({
191
+ 'Name': 'GeoBlockRule',
192
+ 'Priority': priority,
193
+ 'Statement': {
194
+ 'GeoMatchStatement': {
195
+ 'CountryCodes': blocked_countries
196
+ }
197
+ },
198
+ 'Action': {
199
+ 'Block': {}
200
+ },
201
+ 'VisibilityConfig': {
202
+ 'SampledRequestsEnabled': True,
203
+ 'CloudWatchMetricsEnabled': True,
204
+ 'MetricName': 'GeoBlockRule'
205
+ }
206
+ })
207
+ priority += 1
208
+
209
+ # Path-specific rate limiting rules (before general rate limit)
210
+ for idx, path_limit in enumerate(path_rate_limits):
211
+ path = path_limit['path']
212
+ limit = int(path_limit['rateLimit'])
213
+ rule_name = path_limit.get('name', f"PathRateLimit{idx}")
214
+
215
+ print(f"Adding path-specific rate limit: {path} = {limit} req/5min")
216
+
217
+ rules.append({
218
+ 'Name': rule_name,
219
+ 'Priority': priority,
220
+ 'Statement': {
221
+ 'RateBasedStatement': {
222
+ 'Limit': limit,
223
+ 'AggregateKeyType': 'IP',
224
+ 'ScopeDownStatement': {
225
+ 'ByteMatchStatement': {
226
+ 'SearchString': path,
227
+ 'FieldToMatch': {
228
+ 'UriPath': {}
229
+ },
230
+ 'TextTransformations': [{
231
+ 'Priority': 0,
232
+ 'Type': 'NONE'
233
+ }],
234
+ 'PositionalConstraint': 'STARTS_WITH' if not '*' in path else 'CONTAINS'
235
+ }
236
+ }
237
+ }
238
+ },
239
+ 'Action': {
240
+ 'Block': {}
241
+ },
242
+ 'VisibilityConfig': {
243
+ 'SampledRequestsEnabled': True,
244
+ 'CloudWatchMetricsEnabled': True,
245
+ 'MetricName': rule_name
246
+ }
247
+ })
248
+ priority += 1
249
+
250
+ # General rate limiting rule
251
+ rules.append({
252
+ 'Name': 'RateLimitRule',
253
+ 'Priority': priority,
254
+ 'Statement': {
255
+ 'RateBasedStatement': {
256
+ 'Limit': rate_limit,
257
+ 'AggregateKeyType': 'IP'
258
+ }
259
+ },
260
+ 'Action': {
261
+ 'Block': {}
262
+ },
263
+ 'VisibilityConfig': {
264
+ 'SampledRequestsEnabled': True,
265
+ 'CloudWatchMetricsEnabled': True,
266
+ 'MetricName': 'RateLimitRule'
267
+ }
268
+ })
269
+ priority += 1
270
+
271
+ if enable_managed_rules:
272
+ # Core Rule Set
273
+ rules.append({
274
+ 'Name': 'AWSManagedRulesCommonRuleSet',
275
+ 'Priority': priority,
276
+ 'Statement': {
277
+ 'ManagedRuleGroupStatement': {
278
+ 'VendorName': 'AWS',
279
+ 'Name': 'AWSManagedRulesCommonRuleSet'
280
+ }
281
+ },
282
+ 'OverrideAction': {
283
+ 'None': {}
284
+ },
285
+ 'VisibilityConfig': {
286
+ 'SampledRequestsEnabled': True,
287
+ 'CloudWatchMetricsEnabled': True,
288
+ 'MetricName': 'AWSManagedRulesCommonRuleSet'
289
+ }
290
+ })
291
+ priority += 1
292
+
293
+ # Known Bad Inputs
294
+ rules.append({
295
+ 'Name': 'AWSManagedRulesKnownBadInputsRuleSet',
296
+ 'Priority': priority,
297
+ 'Statement': {
298
+ 'ManagedRuleGroupStatement': {
299
+ 'VendorName': 'AWS',
300
+ 'Name': 'AWSManagedRulesKnownBadInputsRuleSet'
301
+ }
302
+ },
303
+ 'OverrideAction': {
304
+ 'None': {}
305
+ },
306
+ 'VisibilityConfig': {
307
+ 'SampledRequestsEnabled': True,
308
+ 'CloudWatchMetricsEnabled': True,
309
+ 'MetricName': 'AWSManagedRulesKnownBadInputsRuleSet'
310
+ }
311
+ })
312
+ priority += 1
313
+
314
+ # Amazon IP Reputation List
315
+ rules.append({
316
+ 'Name': 'AWSManagedRulesAmazonIpReputationList',
317
+ 'Priority': priority,
318
+ 'Statement': {
319
+ 'ManagedRuleGroupStatement': {
320
+ 'VendorName': 'AWS',
321
+ 'Name': 'AWSManagedRulesAmazonIpReputationList'
322
+ }
323
+ },
324
+ 'OverrideAction': {
325
+ 'None': {}
326
+ },
327
+ 'VisibilityConfig': {
328
+ 'SampledRequestsEnabled': True,
329
+ 'CloudWatchMetricsEnabled': True,
330
+ 'MetricName': 'AWSManagedRulesAmazonIpReputationList'
331
+ }
332
+ })
333
+ priority += 1
334
+
335
+ # Anonymous IP List
336
+ rules.append({
337
+ 'Name': 'AWSManagedRulesAnonymousIpList',
338
+ 'Priority': priority,
339
+ 'Statement': {
340
+ 'ManagedRuleGroupStatement': {
341
+ 'VendorName': 'AWS',
342
+ 'Name': 'AWSManagedRulesAnonymousIpList'
343
+ }
344
+ },
345
+ 'OverrideAction': {
346
+ 'None': {}
347
+ },
348
+ 'VisibilityConfig': {
349
+ 'SampledRequestsEnabled': True,
350
+ 'CloudWatchMetricsEnabled': True,
351
+ 'MetricName': 'AWSManagedRulesAnonymousIpList'
352
+ }
353
+ })
354
+ priority += 1
355
+
356
+ return rules
@@ -0,0 +1,144 @@
1
+ # Audit Log Archive
2
+
3
+ A reusable CDK construct for archiving CloudWatch Logs to S3 in Parquet format for long-term storage and analytics with Athena.
4
+
5
+ ## Features
6
+
7
+ - **Parquet Format**: Columnar storage with Snappy compression (80-90% smaller than JSON)
8
+ - **Intelligent-Tiering**: Automatic cost optimization for S3 storage
9
+ - **Athena-Ready**: Pre-configured Glue database and table for immediate querying
10
+ - **Partitioned**: Date-based partitioning (year/month/day) for efficient queries
11
+ - **Encrypted**: KMS encryption for data at rest
12
+ - **Configurable Retention**: Separate retention for CloudWatch Logs and S3 archive
13
+
14
+ ## Architecture
15
+
16
+ ```
17
+ CloudWatch Logs → Subscription Filter → Kinesis Firehose → S3 (Parquet)
18
+
19
+ Glue Catalog
20
+
21
+ Athena
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```typescript
27
+ import { AuditLogArchive } from './constructs/cloudfront/logging';
28
+
29
+ const auditLogs = new AuditLogArchive(this, 'AuditLogs', {
30
+ logGroupNames: [
31
+ '/aws/lambda/my-function-1',
32
+ '/aws/lambda/my-function-2',
33
+ ],
34
+ kmsKey: myKmsKey,
35
+ retentionDays: 30, // CloudWatch Logs retention (default: 30)
36
+ archiveRetentionDays: 365, // S3 archive retention (default: 365)
37
+ bucketName: 'my-audit-logs', // Optional
38
+ databaseName: 'audit_logs', // Optional
39
+ });
40
+ ```
41
+
42
+ ## Properties
43
+
44
+ | Property | Type | Required | Default | Description |
45
+ |----------|------|----------|---------|-------------|
46
+ | `logGroupNames` | `string[]` | Yes | - | CloudWatch Log Groups to archive |
47
+ | `kmsKey` | `IKey` | Yes | - | KMS key for encryption |
48
+ | `retentionDays` | `number` | No | 30 | CloudWatch Logs retention in days |
49
+ | `archiveRetentionDays` | `number` | No | 365 | S3 archive retention in days |
50
+ | `bucketName` | `string` | No | Auto-generated | S3 bucket name |
51
+ | `databaseName` | `string` | No | `audit_logs` | Glue database name |
52
+
53
+ ## Outputs
54
+
55
+ - `bucket`: S3 Bucket for archive storage
56
+ - `database`: Glue Database for Athena
57
+ - `table`: Glue Table with Parquet schema
58
+ - `deliveryStream`: Kinesis Firehose delivery stream
59
+
60
+ ## Querying with Athena
61
+
62
+ ### Example Queries
63
+
64
+ ```sql
65
+ -- View all logs from the last 7 days
66
+ SELECT * FROM audit_logs.logs
67
+ WHERE year = '2024' AND month = '12'
68
+ ORDER BY timestamp DESC
69
+ LIMIT 100;
70
+
71
+ -- Count logs by log group
72
+ SELECT log_group, COUNT(*) as count
73
+ FROM audit_logs.logs
74
+ WHERE year = '2024' AND month = '12'
75
+ GROUP BY log_group;
76
+
77
+ -- Search for specific events
78
+ SELECT timestamp, message, log_group
79
+ FROM audit_logs.logs
80
+ WHERE message LIKE '%authentication%'
81
+ AND year = '2024' AND month = '12';
82
+
83
+ -- Find logs for a specific user
84
+ SELECT * FROM audit_logs.logs
85
+ WHERE user_id = 'user@example.com'
86
+ AND year = '2024' AND month = '12'
87
+ ORDER BY timestamp DESC;
88
+ ```
89
+
90
+ ## Schema
91
+
92
+ | Column | Type | Description |
93
+ |--------|------|-------------|
94
+ | `timestamp` | bigint | Log timestamp in milliseconds |
95
+ | `message` | string | Log message |
96
+ | `log_group` | string | CloudWatch log group name |
97
+ | `log_stream` | string | CloudWatch log stream name |
98
+ | `event_type` | string | Event type (extracted from message) |
99
+ | `user_id` | string | User identifier (if available) |
100
+ | `ip_address` | string | Client IP address (if available) |
101
+
102
+ **Partition Keys**: `year`, `month`, `day`
103
+
104
+ ## Cost Optimization
105
+
106
+ ### Storage Costs (per GB/month)
107
+ - CloudWatch Logs: $0.50
108
+ - S3 Standard: $0.023
109
+ - S3 Intelligent-Tiering (90+ days): $0.0125
110
+ - S3 Intelligent-Tiering (180+ days): $0.004
111
+
112
+ ### Query Costs
113
+ - Athena: $5 per TB scanned
114
+ - Parquet reduces scan size by 80-90% vs JSON
115
+
116
+ ### Example Cost (1TB logs/year)
117
+ - CloudWatch (30 days): ~$15/month
118
+ - S3 Archive (365 days): ~$5-10/month
119
+ - Athena queries: ~$0.50 per TB scanned (vs $5 for JSON)
120
+
121
+ **Total**: ~$20-25/month for 1TB of logs with full year retention
122
+
123
+ ## Best Practices
124
+
125
+ 1. **Use Partitions**: Always filter by year/month/day in queries
126
+ 2. **Limit Columns**: Select only needed columns to reduce scan size
127
+ 3. **Batch Queries**: Combine multiple queries to reduce overhead
128
+ 4. **Monitor Costs**: Use AWS Cost Explorer to track Athena usage
129
+ 5. **Lifecycle Policies**: Adjust retention based on compliance requirements
130
+
131
+ ## Integration with CloudFront Auth
132
+
133
+ The `CloudFrontWithAzureAuth` construct automatically creates an `AuditLogArchive` instance for all authentication-related Lambda functions:
134
+
135
+ - OAuth callback logs
136
+ - Secret rotation logs
137
+ - Session revocation logs
138
+ - Stream processor logs
139
+
140
+ Access via:
141
+ ```typescript
142
+ const authConstruct = new CloudFrontWithAzureAuth(this, 'Auth', { ... });
143
+ const auditBucket = authConstruct.auditLogArchive?.bucket;
144
+ ```
@@ -0,0 +1,138 @@
1
+ # Split-Stack CloudFront Auth Architecture
2
+
3
+ ## Overview
4
+
5
+ This architecture splits CloudFront authentication resources across two regions:
6
+ - **Regional Stack (ap-southeast-2)**: DynamoDB, Secrets Manager, KMS, Lambda functions, audit logging
7
+ - **CloudFront Stack (us-east-1)**: CloudFront Distribution, Lambda@Edge, CloudFront Functions, ACM Certificate
8
+
9
+ ## Benefits
10
+
11
+ - Lower latency for ANZ traffic (DynamoDB in ap-southeast-2)
12
+ - Reduced us-east-1 footprint
13
+ - Better data locality
14
+ - Regional Lambda functions closer to data
15
+
16
+ ## Usage Example
17
+
18
+ ```typescript
19
+ import * as core from 'aws-cdk-lib';
20
+ import { aws_s3 as s3, aws_cloudfront_origins as origins } from 'aws-cdk-lib';
21
+ import * as local from '../constructs';
22
+
23
+ // Regional Stack (ap-southeast-2)
24
+ export class AuthRegionalStack extends core.Stack {
25
+ public readonly authInfra: local.cloudfront.patterns.AuthInfrastructure;
26
+
27
+ constructor(scope: core.App, id: string, props: core.StackProps) {
28
+ super(scope, id, props);
29
+
30
+ this.authInfra = new local.cloudfront.patterns.AuthInfrastructure(this, 'AuthInfra', {
31
+ domainNames: ['bicep-cdk.raindancers.cloud'],
32
+ redirectUri: 'https://bicep-cdk.raindancers.cloud/oauth2/callback',
33
+ hmacSecretRotationSchedule: core.Duration.hours(12),
34
+ autoRevokeOnReuse: true,
35
+ auditLogRetentionDays: 30,
36
+ auditArchiveRetentionDays: 365,
37
+ });
38
+ }
39
+ }
40
+
41
+ // CloudFront Stack (us-east-1)
42
+ export class CloudFrontStack extends core.Stack {
43
+ constructor(
44
+ scope: core.App,
45
+ id: string,
46
+ props: core.StackProps & {
47
+ regionalInfra: local.cloudfront.patterns.AuthInfrastructure;
48
+ jwtDecoderUrl: string;
49
+ }
50
+ ) {
51
+ super(scope, id, { ...props, crossRegionReferences: true });
52
+
53
+ const zone = new local.route53.PublicHostedZone(this, 'Zone', {
54
+ zoneName: 'bicep-cdk.raindancers.cloud',
55
+ });
56
+
57
+ const cert = new local.cloudfront.CloudFrontCertificate(this, 'Certificate', {
58
+ domainName: 'bicep-cdk.raindancers.cloud',
59
+ hostedZone: zone,
60
+ });
61
+
62
+ const webAcl = new local.cloudfront.CloudFrontWebAcl(this, 'WebAcl', {
63
+ rateLimit: 10000,
64
+ enableManagedRules: true,
65
+ allowedCountries: ['NewZealand', 'Australia'],
66
+ });
67
+
68
+ const bucket = new s3.Bucket(this, 'ContentBucket', {
69
+ encryption: s3.BucketEncryption.S3_MANAGED,
70
+ blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
71
+ });
72
+
73
+ const authDistribution = new local.cloudfront.patterns.CloudFrontWithAzureAuthSplit(
74
+ this,
75
+ 'AuthDistribution',
76
+ {
77
+ origin: origins.S3BucketOrigin.withOriginAccessControl(bucket),
78
+ domainNames: ['bicep-cdk.raindancers.cloud'],
79
+ certificate: cert.certificate,
80
+ webAclId: webAcl.webAclArn,
81
+ jwtDecoderUrl: props.jwtDecoderUrl,
82
+ regionalInfrastructure: props.regionalInfra,
83
+ }
84
+ );
85
+
86
+ new local.route53.CloudFrontAliasRecords(this, 'AliasRecords', {
87
+ zone: zone,
88
+ distribution: authDistribution.distribution,
89
+ });
90
+ }
91
+ }
92
+
93
+ // App
94
+ const app = new core.App();
95
+
96
+ const regionalStack = new AuthRegionalStack(app, 'AuthRegional', {
97
+ env: { region: 'ap-southeast-2', account: process.env.CDK_DEFAULT_ACCOUNT },
98
+ });
99
+
100
+ const cloudFrontStack = new CloudFrontStack(app, 'CloudFront', {
101
+ env: { region: 'us-east-1', account: process.env.CDK_DEFAULT_ACCOUNT },
102
+ crossRegionReferences: true,
103
+ regionalInfra: regionalStack.authInfra,
104
+ jwtDecoderUrl: 'https://example.com/jwt',
105
+ });
106
+
107
+ cloudFrontStack.addDependency(regionalStack);
108
+ ```
109
+
110
+ ## Migration from Single Stack
111
+
112
+ If you're currently using `CloudFrontWithAzureAuth`:
113
+
114
+ 1. Create new regional stack with `AuthInfrastructure`
115
+ 2. Create new CloudFront stack with `CloudFrontWithAzureAuthSplit`
116
+ 3. Deploy regional stack first
117
+ 4. Deploy CloudFront stack
118
+ 5. Update DNS
119
+ 6. Delete old single-stack resources
120
+
121
+ ## Resource Locations
122
+
123
+ ### ap-southeast-2
124
+ - DynamoDB Table
125
+ - Secrets Manager Secret
126
+ - KMS Key
127
+ - Lambda Functions (copy, rotate, stream processor, revocation)
128
+ - CloudWatch Log Groups
129
+ - S3 Audit Archive Bucket
130
+ - IAM Role (created here, used in us-east-1)
131
+
132
+ ### us-east-1
133
+ - CloudFront Distribution
134
+ - CloudFront Function
135
+ - Lambda@Edge Function
136
+ - CloudFront KeyValueStore
137
+ - ACM Certificate
138
+ - WAF WebACL
package/package.json CHANGED
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "main": "lib/index.js",
56
56
  "license": "Apache-2.0",
57
- "version": "0.0.0",
57
+ "version": "0.0.1",
58
58
  "jest": {
59
59
  "coverageProvider": "v8",
60
60
  "testMatch": [