diff --git a/sqs-lambda-tenant-isolation-sam-py/sqs-processor/index.py b/sqs-lambda-tenant-isolation-sam-py/sqs-processor/index.py index fca8a902d..480e6049a 100644 --- a/sqs-lambda-tenant-isolation-sam-py/sqs-processor/index.py +++ b/sqs-lambda-tenant-isolation-sam-py/sqs-processor/index.py @@ -1,29 +1,53 @@ import json +import re +import sys import boto3 import os +from botocore.exceptions import ClientError lambda_client = boto3.client('lambda') TENANT_ISOLATED_FUNCTION = os.environ['TENANT_ISOLATED_FUNCTION_NAME'] +TENANT_ID_PATTERN = re.compile(r'^[a-zA-Z0-9-]{1,64}$') +MAX_BODY_SIZE_BYTES = 262144 # 256 KB + def handler(event, context): for record in event['Records']: - body = json.loads(record['body']) - - # Get message group ID from SQS attributes + try: + body = json.loads(record['body']) + except json.JSONDecodeError as e: + print(f"Failed to parse message body: {e}") + continue + + body_size = sys.getsizeof(record['body']) + if body_size > MAX_BODY_SIZE_BYTES: + print(f"Message body exceeds max size ({body_size} > {MAX_BODY_SIZE_BYTES}), skipping") + continue + + if not isinstance(body, dict): + print(f"Message body is not a JSON object, skipping") + continue + attributes = record.get('attributes') or {} message_group_id = attributes.get('MessageGroupId') - + if not message_group_id: - print(f"Missing MessageGroupId in SQS record: {record}") - message_group_id = "default" - - lambda_client.invoke( - FunctionName=TENANT_ISOLATED_FUNCTION, - InvocationType='Event', - Payload=json.dumps(body), - TenantId=message_group_id - ) - + raise ValueError("MessageGroupId is required for tenant isolation") + + if not TENANT_ID_PATTERN.match(message_group_id): + raise ValueError(f"Invalid tenant ID format: {message_group_id}") + + try: + lambda_client.invoke( + FunctionName=TENANT_ISOLATED_FUNCTION, + InvocationType='Event', + Payload=json.dumps(body), + TenantId=message_group_id + ) + except ClientError as e: + print(f"Lambda invocation failed for tenant {message_group_id}: {e}") + raise # Let SQS retry + print(f"Invoked tenant-isolated function for messagegroup: {message_group_id}") - + return {'statusCode': 200} diff --git a/sqs-lambda-tenant-isolation-sam-py/template.yml b/sqs-lambda-tenant-isolation-sam-py/template.yml index 080cfa065..3cfb3740c 100644 --- a/sqs-lambda-tenant-isolation-sam-py/template.yml +++ b/sqs-lambda-tenant-isolation-sam-py/template.yml @@ -3,11 +3,52 @@ Transform: AWS::Serverless-2016-10-31 Description: Lambda Tenant Isolation Demo Resources: + DeadLetterQueue: + Type: AWS::SQS::Queue + Properties: + QueueName: tenant-isolation-dlq + SqsManagedSseEnabled: true + MessageRetentionPeriod: 1209600 # 14 days + ProcessingQueue: Type: AWS::SQS::Queue Properties: QueueName: tenant-isolation-queue VisibilityTimeout: 300 + SqsManagedSseEnabled: true + RedrivePolicy: + deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn + maxReceiveCount: 3 + + QueuePolicy: + Type: AWS::SQS::QueuePolicy + Properties: + Queues: + - !Ref ProcessingQueue + PolicyDocument: + Statement: + - Sid: DenyInsecureTransport + Effect: Deny + Principal: '*' + Action: 'sqs:*' + Resource: !GetAtt ProcessingQueue.Arn + Condition: + Bool: + 'aws:SecureTransport': 'false' + - Sid: AllowSameAccountSendMessage + Effect: Allow + Principal: + AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root' + Action: 'sqs:SendMessage' + Resource: !GetAtt ProcessingQueue.Arn + - Sid: DenyExternalSendMessage + Effect: Deny + Principal: '*' + Action: 'sqs:SendMessage' + Resource: !GetAtt ProcessingQueue.Arn + Condition: + StringNotEquals: + 'aws:PrincipalAccount': !Ref AWS::AccountId TenantIsolatedFunctionRole: Type: AWS::IAM::Role @@ -34,6 +75,34 @@ Resources: TenancyConfig: TenantIsolationMode: PER_TENANT + SQSProcessorFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: SQSProcessorPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - sqs:ReceiveMessage + - sqs:DeleteMessage + - sqs:GetQueueAttributes + Resource: !GetAtt ProcessingQueue.Arn + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: !GetAtt TenantIsolatedFunction.Arn + SQSProcessorFunction: Type: AWS::Serverless::Function Properties: @@ -42,21 +111,17 @@ Resources: Handler: index.handler Runtime: python3.12 Timeout: 60 + Role: !GetAtt SQSProcessorFunctionRole.Arn Environment: Variables: TENANT_ISOLATED_FUNCTION_NAME: !Ref TenantIsolatedFunction - Policies: - - Statement: - - Effect: Allow - Action: - - lambda:InvokeFunction - Resource: !GetAtt TenantIsolatedFunction.Arn Events: SQSEvent: Type: SQS Properties: Queue: !GetAtt ProcessingQueue.Arn BatchSize: 10 + Outputs: QueueUrl: diff --git a/sqs-lambda-tenant-isolation-sam-py/tenant-isolated-processor/index.py b/sqs-lambda-tenant-isolation-sam-py/tenant-isolated-processor/index.py index d5432f88e..670e99350 100644 --- a/sqs-lambda-tenant-isolation-sam-py/tenant-isolated-processor/index.py +++ b/sqs-lambda-tenant-isolation-sam-py/tenant-isolated-processor/index.py @@ -1,16 +1,28 @@ import json +import re + +TENANT_ID_PATTERN = re.compile(r'^[a-zA-Z0-9-]{1,64}$') def handler(event, context): tenant_id = context.tenant_id - + + if not tenant_id: + raise ValueError("tenant_id is missing from context") + + if not TENANT_ID_PATTERN.match(tenant_id): + raise ValueError(f"Invalid tenant ID format: {tenant_id}") + + if not isinstance(event, dict): + raise ValueError(f"Expected event to be a dict, got {type(event).__name__}") + print(f"Processing request for tenant: {tenant_id}") print(f"Event data: {json.dumps(event)}") - + # Process tenant-specific logic here result = { 'tenant_id': tenant_id, 'message': 'Request processed successfully', 'data': event } - + return result