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.
- package/lib/bicep/azure-functions/custom-claims-provider/function_app.py +164 -0
- package/lib/bicep/azure-functions/custom-claims-provider/host.json +10 -0
- package/lib/bicep/azure-functions/custom-claims-provider/requirements.txt +8 -0
- package/lib/bicep/deploy/lambda/Dockerfile +10 -0
- package/lib/bicep/deploy/lambda/index.py +341 -0
- package/lib/bicep/patterns/scripts/LOCALDEBUGREADME.md +86 -0
- package/lib/bicep/patterns/scripts/wire-custom-claims-extension.sh +122 -0
- package/lib/bicep/patterns/scripts/wire-custom-claims-extension.sh.bak +118 -0
- package/lib/cloudfront/cloudfront-functions/auth-check.js +220 -0
- package/lib/cloudfront/cloudfront-functions/modules/auth-check.js +263 -0
- package/lib/cloudfront/cloudfront-functions/modules/cognito-auth-check.js +223 -0
- package/lib/cloudfront/cloudfront-functions/modules/shared-utils.js +47 -0
- package/lib/cloudfront/cloudfront-functions/modules/tls-check.js +39 -0
- package/lib/cloudfront/cloudfront-functions/modules/url-rewrite.js +6 -0
- package/lib/cloudfront/cloudfront-functions/role-enforcer.js +93 -0
- package/lib/cloudfront/cloudfront-functions/tls-enforcer.js +55 -0
- package/lib/cloudfront/cloudfront-functions/userinfo-endpoint.js +208 -0
- package/lib/cloudfront/lambda/certificate/index.py +219 -0
- package/lib/cloudfront/lambda/cognito-auth/oauth-callback.py +215 -0
- package/lib/cloudfront/lambda/cognito-auth/requirements.txt +3 -0
- package/lib/cloudfront/lambda/edge-auth/config.py +6 -0
- package/lib/cloudfront/lambda/edge-auth/config_generated.py +12 -0
- package/lib/cloudfront/lambda/edge-auth/oauth-callback.py +538 -0
- package/lib/cloudfront/lambda/edge-auth/requirements.txt +3 -0
- package/lib/cloudfront/lambda/hmacSecret/index.py +129 -0
- package/lib/cloudfront/lambda/hmacSecret/requirements.txt +1 -0
- package/lib/cloudfront/lambda/jwt-decoder/index.py +88 -0
- package/lib/cloudfront/lambda/pre-token/index.py +11 -0
- package/lib/cloudfront/lambda/rotateSecret/index.py +86 -0
- package/lib/cloudfront/lambda/session-revocation/index.py +80 -0
- package/lib/cloudfront/lambda/ssm-writer/index.py +44 -0
- package/lib/cloudfront/lambda/stream-processor/index.py +53 -0
- package/lib/cloudfront/lambda/webacl/index.py +356 -0
- package/lib/cloudfront/logging/README.md +144 -0
- package/lib/cloudfront/patterns/SPLIT_STACK_USAGE.md +138 -0
- package/package.json +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import urllib3
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger()
|
|
7
|
+
logger.setLevel(logging.INFO)
|
|
8
|
+
http = urllib3.PoolManager()
|
|
9
|
+
|
|
10
|
+
def send_response(event, context, status, data=None):
|
|
11
|
+
"""Send response to CloudFormation"""
|
|
12
|
+
response_body = {
|
|
13
|
+
'Status': status,
|
|
14
|
+
'Reason': f'See CloudWatch Log Stream: {context.log_stream_name}',
|
|
15
|
+
'PhysicalResourceId': context.log_stream_name,
|
|
16
|
+
'StackId': event['StackId'],
|
|
17
|
+
'RequestId': event['RequestId'],
|
|
18
|
+
'LogicalResourceId': event['LogicalResourceId'],
|
|
19
|
+
'Data': data or {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
json_response = json.dumps(response_body)
|
|
23
|
+
headers = {'content-type': '', 'content-length': str(len(json_response))}
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
http.request('PUT', event['ResponseURL'], body=json_response, headers=headers)
|
|
27
|
+
except Exception as e:
|
|
28
|
+
logger.error(f'Failed to send response: {e}')
|
|
29
|
+
|
|
30
|
+
def copy_secret_to_kvs(secret_arn, kvs_arn):
|
|
31
|
+
"""
|
|
32
|
+
Core logic to copy HMAC secret from Secrets Manager to CloudFront KVS.
|
|
33
|
+
Implements dual-secret rotation: stores both current and old secret for zero-downtime rotation.
|
|
34
|
+
"""
|
|
35
|
+
logger.info(f'Copying secret from {secret_arn} to KVS {kvs_arn}')
|
|
36
|
+
|
|
37
|
+
secrets_client = boto3.client('secretsmanager')
|
|
38
|
+
secret_response = secrets_client.get_secret_value(SecretId=secret_arn)
|
|
39
|
+
secret_data = json.loads(secret_response['SecretString'])
|
|
40
|
+
new_secret = secret_data['hmac_key']
|
|
41
|
+
|
|
42
|
+
logger.info('New secret retrieved from Secrets Manager')
|
|
43
|
+
|
|
44
|
+
cf_client = boto3.client('cloudfront-keyvaluestore')
|
|
45
|
+
|
|
46
|
+
kvs_response = cf_client.describe_key_value_store(KvsARN=kvs_arn)
|
|
47
|
+
etag = kvs_response['ETag']
|
|
48
|
+
logger.info(f'KVS ETag: {etag}')
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
current_response = cf_client.get_key(
|
|
52
|
+
KvsARN=kvs_arn,
|
|
53
|
+
# amazonq-ignore-next-line
|
|
54
|
+
Key='jwt.secret'
|
|
55
|
+
)
|
|
56
|
+
old_secret = current_response['Value']
|
|
57
|
+
|
|
58
|
+
cf_client.put_key(
|
|
59
|
+
KvsARN=kvs_arn,
|
|
60
|
+
# amazonq-ignore-next-line
|
|
61
|
+
Key='jwt.secret.old',
|
|
62
|
+
Value=old_secret,
|
|
63
|
+
IfMatch=etag
|
|
64
|
+
)
|
|
65
|
+
logger.info('Current secret preserved as old secret')
|
|
66
|
+
|
|
67
|
+
kvs_response = cf_client.describe_key_value_store(KvsARN=kvs_arn)
|
|
68
|
+
etag = kvs_response['ETag']
|
|
69
|
+
except cf_client.exceptions.ResourceNotFoundException:
|
|
70
|
+
logger.info('No existing secret found (first deployment)')
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.warning(f'Could not preserve old secret: {e}')
|
|
73
|
+
|
|
74
|
+
cf_client.put_key(
|
|
75
|
+
KvsARN=kvs_arn,
|
|
76
|
+
# amazonq-ignore-next-line
|
|
77
|
+
Key='jwt.secret',
|
|
78
|
+
Value=new_secret,
|
|
79
|
+
IfMatch=etag
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
logger.info('New secret copied to CloudFront KVS successfully')
|
|
83
|
+
logger.info('Dual-secret rotation complete: jwt.secret (new) + jwt.secret.old (previous)')
|
|
84
|
+
|
|
85
|
+
def handler(event, context):
|
|
86
|
+
"""
|
|
87
|
+
Handler supporting both CloudFormation Custom Resource and direct Lambda invocations.
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
is_cfn_event = 'RequestType' in event and 'ResponseURL' in event
|
|
91
|
+
|
|
92
|
+
if is_cfn_event:
|
|
93
|
+
logger.info(f'CloudFormation Request Type: {event["RequestType"]}')
|
|
94
|
+
request_type = event['RequestType']
|
|
95
|
+
|
|
96
|
+
if request_type in ['Create', 'Update']:
|
|
97
|
+
secret_arn = event['ResourceProperties']['SecretArn']
|
|
98
|
+
kvs_arn = event['ResourceProperties']['KvsArn']
|
|
99
|
+
|
|
100
|
+
copy_secret_to_kvs(secret_arn, kvs_arn)
|
|
101
|
+
send_response(event, context, 'SUCCESS', {
|
|
102
|
+
'Message': 'Secret rotation complete with dual-secret support'
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
elif request_type == 'Delete':
|
|
106
|
+
logger.info('Delete request - no cleanup needed')
|
|
107
|
+
send_response(event, context, 'SUCCESS')
|
|
108
|
+
|
|
109
|
+
else:
|
|
110
|
+
logger.warning(f'Unknown request type: {request_type}')
|
|
111
|
+
send_response(event, context, 'SUCCESS')
|
|
112
|
+
else:
|
|
113
|
+
logger.info('Direct invocation (non-CloudFormation)')
|
|
114
|
+
secret_arn = event['ResourceProperties']['SecretArn']
|
|
115
|
+
kvs_arn = event['ResourceProperties']['KvsArn']
|
|
116
|
+
|
|
117
|
+
copy_secret_to_kvs(secret_arn, kvs_arn)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
'statusCode': 200,
|
|
121
|
+
'body': json.dumps({'message': 'Secret copied successfully'})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.error(f'Error: {str(e)}', exc_info=True)
|
|
126
|
+
if 'ResponseURL' in event:
|
|
127
|
+
send_response(event, context, 'FAILED')
|
|
128
|
+
else:
|
|
129
|
+
raise
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
botocore[crt]>=1.31.0
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import base64
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
def handler(event, context):
|
|
6
|
+
cookies = event.get('cookies', [])
|
|
7
|
+
|
|
8
|
+
auth_token = None
|
|
9
|
+
for cookie in cookies:
|
|
10
|
+
if cookie.startswith('__Host-auth_session='):
|
|
11
|
+
auth_token = cookie.split('=', 1)[1]
|
|
12
|
+
break
|
|
13
|
+
|
|
14
|
+
if not auth_token:
|
|
15
|
+
return {
|
|
16
|
+
'statusCode': 200,
|
|
17
|
+
'headers': {
|
|
18
|
+
'Content-Type': 'text/html',
|
|
19
|
+
'Content-Security-Policy': "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'",
|
|
20
|
+
},
|
|
21
|
+
'body': '<html><body><h1>No JWT Found</h1><p>No auth_session cookie present.</p></body></html>'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
parts = auth_token.split('.')
|
|
26
|
+
if len(parts) != 3:
|
|
27
|
+
raise ValueError('Invalid JWT format')
|
|
28
|
+
|
|
29
|
+
header = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
|
|
30
|
+
payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
|
|
31
|
+
|
|
32
|
+
# Format timestamps
|
|
33
|
+
nbf_time = datetime.fromtimestamp(payload.get('nbf', 0)).strftime('%Y-%m-%d %H:%M:%S') if 'nbf' in payload else 'N/A'
|
|
34
|
+
exp_time = datetime.fromtimestamp(payload.get('exp', 0)).strftime('%Y-%m-%d %H:%M:%S') if 'exp' in payload else 'N/A'
|
|
35
|
+
|
|
36
|
+
html = f'''
|
|
37
|
+
<html>
|
|
38
|
+
<head><title>JWT Decoder</title></head>
|
|
39
|
+
<body style="font-family: monospace; padding: 20px;">
|
|
40
|
+
<h1>JWT Token Details</h1>
|
|
41
|
+
<p><strong>Not Before:</strong> <span id="nbf">{nbf_time} UTC</span></p>
|
|
42
|
+
<p><strong>Expires:</strong> <span id="exp">{exp_time} UTC</span></p>
|
|
43
|
+
<h2>Header</h2>
|
|
44
|
+
<pre>{json.dumps(header, indent=2)}</pre>
|
|
45
|
+
<h2>Payload</h2>
|
|
46
|
+
<pre>{json.dumps(payload, indent=2)}</pre>
|
|
47
|
+
<h2>Signature</h2>
|
|
48
|
+
<pre>{parts[2]}</pre>
|
|
49
|
+
<script>
|
|
50
|
+
const nbf = {payload.get('nbf', 0)};
|
|
51
|
+
const exp = {payload.get('exp', 0)};
|
|
52
|
+
|
|
53
|
+
const offset = -new Date().getTimezoneOffset();
|
|
54
|
+
const offsetHours = Math.floor(Math.abs(offset) / 60);
|
|
55
|
+
const offsetMinutes = Math.abs(offset) % 60;
|
|
56
|
+
const offsetSign = offset >= 0 ? '+' : '-';
|
|
57
|
+
const offsetStr = `${{offsetSign}}${{String(offsetHours).padStart(2, '0')}}:${{String(offsetMinutes).padStart(2, '0')}}`;
|
|
58
|
+
|
|
59
|
+
const options = {{ year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }};
|
|
60
|
+
|
|
61
|
+
if (nbf) {{
|
|
62
|
+
document.getElementById('nbf').textContent = `${{new Date(nbf * 1000).toLocaleString('en-NZ', options)}} (UTC${{offsetStr}})`;
|
|
63
|
+
}}
|
|
64
|
+
if (exp) {{
|
|
65
|
+
document.getElementById('exp').textContent = `${{new Date(exp * 1000).toLocaleString('en-NZ', options)}} (UTC${{offsetStr}})`;
|
|
66
|
+
}}
|
|
67
|
+
</script>
|
|
68
|
+
</body>
|
|
69
|
+
</html>
|
|
70
|
+
'''
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
'statusCode': 200,
|
|
74
|
+
'headers': {
|
|
75
|
+
'Content-Type': 'text/html',
|
|
76
|
+
'Content-Security-Policy': "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'",
|
|
77
|
+
},
|
|
78
|
+
'body': html
|
|
79
|
+
}
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return {
|
|
82
|
+
'statusCode': 200,
|
|
83
|
+
'headers': {
|
|
84
|
+
'Content-Type': 'text/html',
|
|
85
|
+
'Content-Security-Policy': "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'",
|
|
86
|
+
},
|
|
87
|
+
'body': f'<html><body><h1>Error Decoding JWT</h1><p>{str(e)}</p></body></html>'
|
|
88
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
def handler(event, context):
|
|
2
|
+
groups = event['request']['groupConfiguration']['groupsToOverride']
|
|
3
|
+
event['response']['claimsAndScopeOverrideDetails'] = {
|
|
4
|
+
'idTokenGeneration': {
|
|
5
|
+
'claimsToAddOrOverride': {
|
|
6
|
+
'roles': groups,
|
|
7
|
+
'https://aws.amazon.com/tags/principal_tags/Claims': ':' + ':'.join(groups) + ':',
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return event
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import boto3
|
|
3
|
+
import secrets
|
|
4
|
+
import string
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
secretsmanager = boto3.client('secretsmanager')
|
|
9
|
+
lambda_client = boto3.client('lambda')
|
|
10
|
+
|
|
11
|
+
SECRET_ARN = os.environ['SECRET_ARN']
|
|
12
|
+
COPY_LAMBDA_ARN = os.environ['COPY_LAMBDA_ARN']
|
|
13
|
+
KVS_ARN = os.environ['KVS_ARN']
|
|
14
|
+
|
|
15
|
+
MAX_RETRIES = 3
|
|
16
|
+
RETRY_DELAY = 2
|
|
17
|
+
|
|
18
|
+
def generate_hmac_key(length=64):
|
|
19
|
+
alphabet = string.ascii_letters + string.digits
|
|
20
|
+
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
|
21
|
+
|
|
22
|
+
def invoke_copy_lambda_with_retry(payload):
|
|
23
|
+
"""Invoke copy lambda synchronously with retry logic"""
|
|
24
|
+
for attempt in range(MAX_RETRIES):
|
|
25
|
+
try:
|
|
26
|
+
print(f'Invoking copy lambda (attempt {attempt + 1}/{MAX_RETRIES})')
|
|
27
|
+
response = lambda_client.invoke(
|
|
28
|
+
FunctionName=COPY_LAMBDA_ARN,
|
|
29
|
+
InvocationType='RequestResponse',
|
|
30
|
+
Payload=json.dumps(payload)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
response_payload = json.loads(response['Payload'].read())
|
|
34
|
+
|
|
35
|
+
if response['StatusCode'] == 200:
|
|
36
|
+
if 'statusCode' in response_payload and response_payload['statusCode'] == 200:
|
|
37
|
+
print('Copy lambda invoked successfully')
|
|
38
|
+
return response_payload
|
|
39
|
+
elif 'FunctionError' in response:
|
|
40
|
+
raise Exception(f"Copy lambda error: {response_payload}")
|
|
41
|
+
else:
|
|
42
|
+
print('Copy lambda completed successfully')
|
|
43
|
+
return response_payload
|
|
44
|
+
else:
|
|
45
|
+
raise Exception(f"Lambda invocation failed with status {response['StatusCode']}")
|
|
46
|
+
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(f'Attempt {attempt + 1} failed: {str(e)}')
|
|
49
|
+
if attempt < MAX_RETRIES - 1:
|
|
50
|
+
print(f'Retrying in {RETRY_DELAY} seconds...')
|
|
51
|
+
time.sleep(RETRY_DELAY)
|
|
52
|
+
else:
|
|
53
|
+
raise Exception(f'Failed to invoke copy lambda after {MAX_RETRIES} attempts: {str(e)}')
|
|
54
|
+
|
|
55
|
+
def handler(event, context):
|
|
56
|
+
print(f"Rotating secret: {SECRET_ARN}")
|
|
57
|
+
|
|
58
|
+
response = secretsmanager.get_secret_value(SecretId=SECRET_ARN)
|
|
59
|
+
current_secret = json.loads(response['SecretString'])
|
|
60
|
+
|
|
61
|
+
new_hmac_key = generate_hmac_key()
|
|
62
|
+
|
|
63
|
+
current_secret['hmac_key'] = new_hmac_key
|
|
64
|
+
|
|
65
|
+
secretsmanager.put_secret_value(
|
|
66
|
+
SecretId=SECRET_ARN,
|
|
67
|
+
SecretString=json.dumps(current_secret)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
print(f"Secret rotated in Secrets Manager")
|
|
71
|
+
|
|
72
|
+
payload = {
|
|
73
|
+
'ResourceProperties': {
|
|
74
|
+
'SecretArn': SECRET_ARN,
|
|
75
|
+
'KvsArn': KVS_ARN
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
invoke_copy_lambda_with_retry(payload)
|
|
80
|
+
|
|
81
|
+
print(f"KVS updated successfully")
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
'statusCode': 200,
|
|
85
|
+
'body': json.dumps('Secret rotated and synchronized successfully')
|
|
86
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import boto3
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger()
|
|
8
|
+
logger.setLevel(logging.INFO)
|
|
9
|
+
|
|
10
|
+
dynamodb = boto3.client('dynamodb')
|
|
11
|
+
kvs_client = boto3.client('cloudfront-keyvaluestore')
|
|
12
|
+
|
|
13
|
+
TABLE_NAME = os.environ['TABLE_NAME']
|
|
14
|
+
KVS_ARN = os.environ['KVS_ARN']
|
|
15
|
+
|
|
16
|
+
def lambda_handler(event, context):
|
|
17
|
+
"""Triggered by SNS message to revoke user sessions
|
|
18
|
+
|
|
19
|
+
Message format: {"action": "revoke", "userId": "user@example.com"}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
for record in event['Records']:
|
|
23
|
+
try:
|
|
24
|
+
message = json.loads(record['Sns']['Message'])
|
|
25
|
+
|
|
26
|
+
if message.get('action') != 'revoke':
|
|
27
|
+
logger.warning(f'Unknown action: {message.get("action")}')
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
user_id = message.get('userId')
|
|
31
|
+
if not user_id:
|
|
32
|
+
logger.error('Missing userId in revocation message')
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
logger.info(f'Processing revocation for user: {user_id}')
|
|
36
|
+
|
|
37
|
+
response = dynamodb.query(
|
|
38
|
+
TableName=TABLE_NAME,
|
|
39
|
+
IndexName='GSI1',
|
|
40
|
+
KeyConditionExpression='gsi1pk = :pk',
|
|
41
|
+
FilterExpression='revoked = :false',
|
|
42
|
+
ExpressionAttributeValues={
|
|
43
|
+
':pk': {'S': f'USER#{user_id}'},
|
|
44
|
+
':false': {'BOOL': False}
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
revoked_count = 0
|
|
49
|
+
for item in response.get('Items', []):
|
|
50
|
+
jti = item['jti']['S']
|
|
51
|
+
|
|
52
|
+
dynamodb.update_item(
|
|
53
|
+
TableName=TABLE_NAME,
|
|
54
|
+
Key={
|
|
55
|
+
'pk': {'S': f'SESSION#{user_id}'},
|
|
56
|
+
'sk': {'S': f'SESSION#{jti}'}
|
|
57
|
+
},
|
|
58
|
+
UpdateExpression='SET revoked = :true, revokedAt = :now',
|
|
59
|
+
ExpressionAttributeValues={
|
|
60
|
+
':true': {'BOOL': True},
|
|
61
|
+
':now': {'N': str(int(time.time()))}
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
kvs_client.put_key(
|
|
66
|
+
KvsARN=KVS_ARN,
|
|
67
|
+
Key=f'revoked:{jti}',
|
|
68
|
+
Value=str(int(time.time())),
|
|
69
|
+
IfMatch='*'
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
revoked_count += 1
|
|
73
|
+
|
|
74
|
+
logger.info(f'Revoked {revoked_count} sessions for user: {user_id}')
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f'Error processing revocation: {str(e)}')
|
|
78
|
+
raise
|
|
79
|
+
|
|
80
|
+
return {'statusCode': 200, 'body': 'Revocation processed'}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import json
|
|
3
|
+
import urllib3
|
|
4
|
+
|
|
5
|
+
http = urllib3.PoolManager()
|
|
6
|
+
|
|
7
|
+
def send_response(event, context, status, reason, physical_id):
|
|
8
|
+
body = json.dumps({
|
|
9
|
+
'Status': status,
|
|
10
|
+
'Reason': reason,
|
|
11
|
+
'PhysicalResourceId': physical_id,
|
|
12
|
+
'StackId': event['StackId'],
|
|
13
|
+
'RequestId': event['RequestId'],
|
|
14
|
+
'LogicalResourceId': event['LogicalResourceId'],
|
|
15
|
+
'Data': {},
|
|
16
|
+
})
|
|
17
|
+
try:
|
|
18
|
+
http.request('PUT', event['ResponseURL'], body=body.encode('utf-8'), headers={'Content-Type': ''})
|
|
19
|
+
except Exception as e:
|
|
20
|
+
print(f'Failed to send response to CloudFormation: {str(e)}')
|
|
21
|
+
|
|
22
|
+
def handler(event, context):
|
|
23
|
+
physical_id = event.get('PhysicalResourceId', context.log_stream_name)
|
|
24
|
+
try:
|
|
25
|
+
props = event['ResourceProperties']
|
|
26
|
+
prefix = props['Prefix']
|
|
27
|
+
params = json.loads(props['Params'])
|
|
28
|
+
region = props.get('Region', 'us-east-1')
|
|
29
|
+
ssm = boto3.client('ssm', region_name=region)
|
|
30
|
+
|
|
31
|
+
if event['RequestType'] in ('Create', 'Update'):
|
|
32
|
+
for key, value in params.items():
|
|
33
|
+
ssm.put_parameter(Name=f'{prefix}/{key}', Value=value, Type='String', Overwrite=True)
|
|
34
|
+
physical_id = prefix
|
|
35
|
+
|
|
36
|
+
elif event['RequestType'] == 'Delete':
|
|
37
|
+
names = [f'{prefix}/{key}' for key in params]
|
|
38
|
+
for i in range(0, len(names), 10):
|
|
39
|
+
ssm.delete_parameters(Names=names[i:i+10])
|
|
40
|
+
|
|
41
|
+
send_response(event, context, 'SUCCESS', 'OK', physical_id)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print(f'Error: {str(e)}')
|
|
44
|
+
send_response(event, context, 'FAILED', str(e), physical_id)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger()
|
|
6
|
+
logger.setLevel(logging.INFO)
|
|
7
|
+
|
|
8
|
+
kvs_client = boto3.client('cloudfront-keyvaluestore')
|
|
9
|
+
KVS_ARN = os.environ['KVS_ARN']
|
|
10
|
+
|
|
11
|
+
def lambda_handler(event, context):
|
|
12
|
+
"""Triggered by DynamoDB Stream when TTL deletes expired sessions
|
|
13
|
+
|
|
14
|
+
Automatically removes corresponding revocations from KVS.
|
|
15
|
+
Only processes revoked sessions (not all TTL deletions).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
deleted_count = 0
|
|
19
|
+
|
|
20
|
+
for record in event['Records']:
|
|
21
|
+
if record['eventName'] != 'REMOVE':
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
if 'userIdentity' not in record or record['userIdentity'].get('type') != 'Service':
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
old_image = record['dynamodb'].get('OldImage', {})
|
|
28
|
+
|
|
29
|
+
pk = old_image.get('pk', {}).get('S', '')
|
|
30
|
+
if not pk.startswith('SESSION#'):
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
revoked = old_image.get('revoked', {}).get('BOOL', False)
|
|
34
|
+
if not revoked:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
jti = old_image.get('jti', {}).get('S')
|
|
38
|
+
if not jti:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
kvs_client.delete_key(
|
|
43
|
+
KvsARN=KVS_ARN,
|
|
44
|
+
Key=f'revoked:{jti}',
|
|
45
|
+
IfMatch='*'
|
|
46
|
+
)
|
|
47
|
+
deleted_count += 1
|
|
48
|
+
logger.info(f'Cleaned up expired revocation from KVS: {jti}')
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.warning(f'Failed to delete revocation from KVS: {jti}, error: {e}')
|
|
51
|
+
|
|
52
|
+
logger.info(f'Processed {len(event["Records"])} stream records, cleaned {deleted_count} revocations')
|
|
53
|
+
return {'statusCode': 200, 'deletedCount': deleted_count}
|