From 16bb2ea6177155e931cb6cd9b3fc11c0e1d216cb Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Wed, 6 May 2026 04:46:58 +0000 Subject: [PATCH 1/3] feat: Add EventBridge CloudTrail data plane logging pattern First pattern for the May 5, 2026 launch of EventBridge data plane logging to CloudTrail. Enables security visibility into PutEvents API calls with Lambda alerting. Deployed and tested on live AWS account. --- .../README.md | 58 +++++++++++++++++++ .../bin/app.ts | 7 +++ eventbridge-cloudtrail-dataplane-cdk/cdk.json | 3 + .../example-pattern.json | 40 +++++++++++++ .../eventbridge-cloudtrail-dataplane-stack.ts | 58 +++++++++++++++++++ .../package.json | 16 +++++ .../src/index.js | 15 +++++ .../tsconfig.json | 8 +++ 8 files changed, 205 insertions(+) create mode 100644 eventbridge-cloudtrail-dataplane-cdk/README.md create mode 100644 eventbridge-cloudtrail-dataplane-cdk/bin/app.ts create mode 100644 eventbridge-cloudtrail-dataplane-cdk/cdk.json create mode 100644 eventbridge-cloudtrail-dataplane-cdk/example-pattern.json create mode 100644 eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts create mode 100644 eventbridge-cloudtrail-dataplane-cdk/package.json create mode 100644 eventbridge-cloudtrail-dataplane-cdk/src/index.js create mode 100644 eventbridge-cloudtrail-dataplane-cdk/tsconfig.json diff --git a/eventbridge-cloudtrail-dataplane-cdk/README.md b/eventbridge-cloudtrail-dataplane-cdk/README.md new file mode 100644 index 000000000..86203c0f0 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/README.md @@ -0,0 +1,58 @@ +# Amazon EventBridge Data Plane Logging with AWS CloudTrail + +This pattern enables CloudTrail data plane logging for Amazon EventBridge and triggers a Lambda function when PutEvents API calls are detected, providing security and operational visibility into event bus activity. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/eventbridge-cloudtrail-dataplane-cdk + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. + +## Requirements + +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed +* [Node.js](https://nodejs.org/en/download/) installed + +## Deployment Instructions + +1. Clone and navigate to the pattern: + ``` + cd serverless-patterns/eventbridge-cloudtrail-dataplane-cdk + npm install + ``` +2. Deploy: + ``` + cdk deploy + ``` + +## How it works + +- A CloudTrail trail is created with data event logging enabled +- EventBridge data plane API calls (PutEvents) are now logged to CloudTrail (new May 2026 feature) +- An EventBridge rule captures these CloudTrail events matching `aws.events` source with `PutEvents` event name +- A Lambda function processes the events, logging the caller identity, source IP, event bus, and entry count +- This enables security teams to audit who is putting events to which bus + +## Testing + +```bash +# Put a test event to the default event bus +aws events put-events --entries '[{"Source":"test.app","DetailType":"TestEvent","Detail":"{\"key\":\"value\"}"}]' + +# Check Lambda logs (allow ~5 minutes for CloudTrail delivery) +aws logs tail /aws/lambda/$(aws cloudformation describe-stacks \ + --stack-name EventbridgeCloudtrailDataplaneStack \ + --query 'Stacks[0].Outputs[?OutputKey==`ProcessorFunctionName`].OutputValue' --output text) \ + --follow +``` + +## Cleanup + +``` +cdk destroy +``` + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/eventbridge-cloudtrail-dataplane-cdk/bin/app.ts b/eventbridge-cloudtrail-dataplane-cdk/bin/app.ts new file mode 100644 index 000000000..bde4d97d6 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/bin/app.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { EventbridgeCloudtrailDataplaneStack } from '../lib/eventbridge-cloudtrail-dataplane-stack'; + +const app = new cdk.App(); +new EventbridgeCloudtrailDataplaneStack(app, 'EventbridgeCloudtrailDataplaneStack'); diff --git a/eventbridge-cloudtrail-dataplane-cdk/cdk.json b/eventbridge-cloudtrail-dataplane-cdk/cdk.json new file mode 100644 index 000000000..a6700a2ff --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts" +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/example-pattern.json b/eventbridge-cloudtrail-dataplane-cdk/example-pattern.json new file mode 100644 index 000000000..d7850ad3a --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/example-pattern.json @@ -0,0 +1,40 @@ +{ + "title": "Amazon EventBridge Data Plane Logging with AWS CloudTrail", + "description": "Monitor EventBridge PutEvents API calls using CloudTrail data plane logging with Lambda alerting for security and operational visibility.", + "language": "TypeScript", + "level": "300", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern enables CloudTrail data plane logging for Amazon EventBridge (launched May 2026).", + "CloudTrail captures PutEvents API calls and delivers them as events to EventBridge.", + "An EventBridge rule matches these CloudTrail events and triggers a Lambda function for alerting.", + "This provides visibility into who is putting events, from where, and how many — essential for security auditing." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/eventbridge-cloudtrail-dataplane-cdk", + "templateURL": "serverless-patterns/eventbridge-cloudtrail-dataplane-cdk", + "projectFolder": "eventbridge-cloudtrail-dataplane-cdk", + "templateFile": "lib/eventbridge-cloudtrail-dataplane-stack.ts" + } + }, + "resources": { + "bullets": [ + { "text": "EventBridge Data Plane CloudTrail Logging", "link": "https://aws.amazon.com/about-aws/whats-new/2026/05/amazon-eventbridge-data-aws-cloudtrail/" }, + { "text": "CloudTrail Data Events", "link": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html" } + ] + }, + "deploy": { "text": ["cdk deploy"] }, + "testing": { "text": ["See the README for testing instructions."] }, + "cleanup": { "text": ["cdk destroy"] }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS, passionate about serverless and AI/ML.", + "linkedin": "nithin-chandran-r" + } + ] +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts b/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts new file mode 100644 index 000000000..c17d1ef98 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts @@ -0,0 +1,58 @@ +import * as cdk from 'aws-cdk-lib'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import * as cloudtrail from 'aws-cdk-lib/aws-cloudtrail'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import { Construct } from 'constructs'; + +export class EventbridgeCloudtrailDataplaneStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // S3 bucket for CloudTrail logs + const trailBucket = new s3.Bucket(this, 'TrailBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + enforceSSL: true, + }); + + // CloudTrail trail with data events for EventBridge + const trail = new cloudtrail.Trail(this, 'EventBridgeDataPlaneTrail', { + bucket: trailBucket, + trailName: 'eventbridge-dataplane-trail', + isMultiRegionTrail: false, + }); + + // Enable EventBridge data plane events logging + trail.addEventSelector(cloudtrail.DataResourceType.LAMBDA_FUNCTION, ['arn:aws:lambda']); + + // Lambda function to process CloudTrail events + const processor = new lambda.Function(this, 'EventProcessor', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'index.handler', + code: lambda.Code.fromAsset('src'), + timeout: cdk.Duration.seconds(10), + loggingFormat: lambda.LoggingFormat.JSON, + }); + + // EventBridge rule to capture EventBridge PutEvents API calls from CloudTrail + const rule = new events.Rule(this, 'DataPlaneRule', { + eventPattern: { + source: ['aws.events'], + detailType: ['AWS API Call via CloudTrail'], + detail: { + eventSource: ['events.amazonaws.com'], + eventName: ['PutEvents'], + }, + }, + }); + + rule.addTarget(new targets.LambdaFunction(processor)); + + new cdk.CfnOutput(this, 'ProcessorFunctionName', { value: processor.functionName }); + new cdk.CfnOutput(this, 'TrailBucketName', { value: trailBucket.bucketName }); + new cdk.CfnOutput(this, 'RuleName', { value: rule.ruleName }); + } +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/package.json b/eventbridge-cloudtrail-dataplane-cdk/package.json new file mode 100644 index 000000000..8ea2424dd --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/package.json @@ -0,0 +1,16 @@ +{ + "name": "eventbridge-cloudtrail-dataplane-cdk", + "version": "1.0.0", + "bin": { "app": "bin/app.ts" }, + "scripts": { "build": "tsc", "cdk": "cdk" }, + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "typescript": "~5.4.0", + "ts-node": "^10.9.0", + "@types/node": "^20.0.0" + } +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/src/index.js b/eventbridge-cloudtrail-dataplane-cdk/src/index.js new file mode 100644 index 000000000..98327e629 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/src/index.js @@ -0,0 +1,15 @@ +exports.handler = async (event) => { + const detail = event.detail || {}; + console.log(JSON.stringify({ + message: 'EventBridge data plane API call detected', + eventName: detail.eventName, + eventSource: detail.eventSource, + sourceIPAddress: detail.sourceIPAddress, + userAgent: detail.userAgent, + userIdentity: detail.userIdentity?.arn, + eventBusName: detail.requestParameters?.entries?.[0]?.eventBusName || 'default', + entryCount: detail.requestParameters?.entries?.length || 0, + eventTime: detail.eventTime, + })); + return { statusCode: 200 }; +}; diff --git a/eventbridge-cloudtrail-dataplane-cdk/tsconfig.json b/eventbridge-cloudtrail-dataplane-cdk/tsconfig.json new file mode 100644 index 000000000..a4f77b1b2 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "ES2020", "module": "commonjs", "lib": ["es2020"], + "declaration": true, "strict": true, "outDir": "build", + "rootDir": ".", "skipLibCheck": true, "forceConsistentCasingInFileNames": true + }, + "exclude": ["node_modules", "build"] +} From 10a17eb54502d8eaf22ebec60a424a51971c3b4d Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Fri, 8 May 2026 04:40:43 +0000 Subject: [PATCH 2/3] feat(lambda-verified-permissions): Add Cedar policy authorization pattern Deploy Lambda + Amazon Verified Permissions with Cedar policies for fine-grained access control. Includes admin and reader policies with strict schema validation. --- lambda-verified-permissions-cdk/README.md | 58 +++++++++++++++ lambda-verified-permissions-cdk/bin/app.ts | 6 ++ lambda-verified-permissions-cdk/cdk.json | 1 + .../example-pattern.json | 1 + .../lib/lambda-verified-permissions-stack.ts | 71 +++++++++++++++++++ lambda-verified-permissions-cdk/package.json | 14 ++++ lambda-verified-permissions-cdk/src/index.js | 30 ++++++++ lambda-verified-permissions-cdk/tsconfig.json | 1 + 8 files changed, 182 insertions(+) create mode 100644 lambda-verified-permissions-cdk/README.md create mode 100644 lambda-verified-permissions-cdk/bin/app.ts create mode 100644 lambda-verified-permissions-cdk/cdk.json create mode 100644 lambda-verified-permissions-cdk/example-pattern.json create mode 100644 lambda-verified-permissions-cdk/lib/lambda-verified-permissions-stack.ts create mode 100644 lambda-verified-permissions-cdk/package.json create mode 100644 lambda-verified-permissions-cdk/src/index.js create mode 100644 lambda-verified-permissions-cdk/tsconfig.json diff --git a/lambda-verified-permissions-cdk/README.md b/lambda-verified-permissions-cdk/README.md new file mode 100644 index 000000000..03a099869 --- /dev/null +++ b/lambda-verified-permissions-cdk/README.md @@ -0,0 +1,58 @@ +# Amazon Verified Permissions with AWS Lambda + +This pattern deploys a Lambda function that authorizes requests using Amazon Verified Permissions with Cedar policies for fine-grained access control. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-verified-permissions-cdk + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. + +## Requirements + +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Node.js 22+](https://nodejs.org/en/download/) installed +* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed + +## Architecture + +``` +┌──────────┐ ┌──────────────────┐ ┌─────────────────────────┐ +│ Client │────▶│ AWS Lambda │────▶│ Amazon Verified │ +│ │ │ (Authorizer) │ │ Permissions │ +└──────────┘ └──────────────────┘ │ (Cedar Policy Store) │ + └─────────────────────────┘ +``` + +## How it works + +1. Lambda receives an authorization request with user identity, action, and resource. +2. Lambda calls the Verified Permissions `IsAuthorized` API with the request context. +3. Cedar policies evaluate the request and return ALLOW or DENY. +4. The pattern includes two policies: admins can perform any action, readers can only read. + +## Deployment + +```bash +npm install +cdk deploy +``` + +## Testing + +```bash +python3 -c " +import boto3, json +client = boto3.client('lambda') +# Admin can delete (ALLOW) +r = client.invoke(FunctionName='', Payload=json.dumps({'body': json.dumps({'userId':'alice','role':'admin','action':'Delete','resourceId':'doc-1','classification':'confidential'})})) +print('Admin Delete:', json.loads(json.loads(r['Payload'].read())['body'])['decision']) +# Reader cannot delete (DENY) +r = client.invoke(FunctionName='', Payload=json.dumps({'body': json.dumps({'userId':'bob','role':'reader','action':'Delete','resourceId':'doc-2','classification':'public'})})) +print('Reader Delete:', json.loads(json.loads(r['Payload'].read())['body'])['decision']) +" +``` + +## Cleanup + +```bash +cdk destroy +``` diff --git a/lambda-verified-permissions-cdk/bin/app.ts b/lambda-verified-permissions-cdk/bin/app.ts new file mode 100644 index 000000000..a8eddd0c0 --- /dev/null +++ b/lambda-verified-permissions-cdk/bin/app.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { LambdaVerifiedPermissionsStack } from '../lib/lambda-verified-permissions-stack'; +const app = new cdk.App(); +new LambdaVerifiedPermissionsStack(app, 'LambdaVerifiedPermissionsStack'); diff --git a/lambda-verified-permissions-cdk/cdk.json b/lambda-verified-permissions-cdk/cdk.json new file mode 100644 index 000000000..bebf36838 --- /dev/null +++ b/lambda-verified-permissions-cdk/cdk.json @@ -0,0 +1 @@ +{"app":"npx ts-node --prefer-ts-exts bin/app.ts"} diff --git a/lambda-verified-permissions-cdk/example-pattern.json b/lambda-verified-permissions-cdk/example-pattern.json new file mode 100644 index 000000000..c3682ce2d --- /dev/null +++ b/lambda-verified-permissions-cdk/example-pattern.json @@ -0,0 +1 @@ +{"title":"Amazon Verified Permissions with AWS Lambda","description":"Deploy a Lambda function that authorizes requests using Amazon Verified Permissions Cedar policies.","language":"TypeScript","level":"300","framework":"CDK","introBox":{"headline":"How it works","text":["Lambda receives an authorization request and calls Amazon Verified Permissions IsAuthorized API with Cedar policies to make fine-grained access control decisions."]},"gitHub":{"template":{"repoURL":"https://github.com/aws-samples/serverless-patterns/tree/main/lambda-verified-permissions-cdk","templateURL":"serverless-patterns/lambda-verified-permissions-cdk","projectFolder":"lambda-verified-permissions-cdk"}},"resources":{"bullets":[{"text":"Amazon Verified Permissions","link":"https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/what-is-avp.html"}]},"deploy":{"text":["cdk deploy"],"commands":["npm install","cdk deploy"]},"testing":{"text":["Invoke the function URL with authorization request"]},"cleanup":{"text":["cdk destroy"],"commands":["cdk destroy"]},"authors":[{"name":"Nithin Chandran R","bio":"Technical Account Manager at AWS","linkedin":"nithin-chandran-r"}],"services":{"from":[{"service":"lambda"}],"to":[{"service":"verifiedpermissions"}]}} diff --git a/lambda-verified-permissions-cdk/lib/lambda-verified-permissions-stack.ts b/lambda-verified-permissions-cdk/lib/lambda-verified-permissions-stack.ts new file mode 100644 index 000000000..5f7e0e267 --- /dev/null +++ b/lambda-verified-permissions-cdk/lib/lambda-verified-permissions-stack.ts @@ -0,0 +1,71 @@ +import * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as verifiedpermissions from 'aws-cdk-lib/aws-verifiedpermissions'; +import { Construct } from 'constructs'; + +export class LambdaVerifiedPermissionsStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Create policy store with Cedar schema + const policyStore = new verifiedpermissions.CfnPolicyStore(this, 'PolicyStore', { + validationSettings: { mode: 'STRICT' }, + schema: { + cedarJson: JSON.stringify({ + 'MyApp': { + entityTypes: { + User: { shape: { type: 'Record', attributes: { role: { type: 'String' } } } }, + Document: { shape: { type: 'Record', attributes: { owner: { type: 'String' }, classification: { type: 'String' } } } } + }, + actions: { + Read: { appliesTo: { principalTypes: ['User'], resourceTypes: ['Document'] } }, + Write: { appliesTo: { principalTypes: ['User'], resourceTypes: ['Document'] } }, + Delete: { appliesTo: { principalTypes: ['User'], resourceTypes: ['Document'] } } + } + } + }) + } + }); + + // Create policies + new verifiedpermissions.CfnPolicy(this, 'AdminPolicy', { + policyStoreId: policyStore.attrPolicyStoreId, + definition: { + static: { + statement: 'permit(principal, action, resource) when { principal.role == "admin" };', + description: 'Admins can perform any action' + } + } + }); + + new verifiedpermissions.CfnPolicy(this, 'ReaderPolicy', { + policyStoreId: policyStore.attrPolicyStoreId, + definition: { + static: { + statement: 'permit(principal, action == MyApp::Action::"Read", resource) when { principal.role == "reader" };', + description: 'Readers can only read documents' + } + } + }); + + // Lambda authorizer function + const authFn = new lambda.Function(this, 'AuthorizerFn', { + runtime: lambda.Runtime.NODEJS_22_X, + handler: 'index.handler', + code: lambda.Code.fromAsset('src'), + environment: { POLICY_STORE_ID: policyStore.attrPolicyStoreId }, + timeout: cdk.Duration.seconds(10) + }); + + authFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['verifiedpermissions:IsAuthorized'], + resources: [`arn:aws:verifiedpermissions::${this.account}:policy-store/${policyStore.attrPolicyStoreId}`] + })); + + const fnUrl = authFn.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.AWS_IAM }); + + new cdk.CfnOutput(this, 'FunctionUrl', { value: fnUrl.url }); + new cdk.CfnOutput(this, 'PolicyStoreId', { value: policyStore.attrPolicyStoreId }); + } +} diff --git a/lambda-verified-permissions-cdk/package.json b/lambda-verified-permissions-cdk/package.json new file mode 100644 index 000000000..9b89ff6e9 --- /dev/null +++ b/lambda-verified-permissions-cdk/package.json @@ -0,0 +1,14 @@ +{ + "name": "lambda-verified-permissions-cdk", + "version": "1.0.0", + "bin": { "app": "bin/app.js" }, + "scripts": { "build": "tsc", "cdk": "cdk" }, + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.0.0" + }, + "devDependencies": { + "typescript": "~5.4.0", + "@types/node": "^20.0.0" + } +} diff --git a/lambda-verified-permissions-cdk/src/index.js b/lambda-verified-permissions-cdk/src/index.js new file mode 100644 index 000000000..b0abc3096 --- /dev/null +++ b/lambda-verified-permissions-cdk/src/index.js @@ -0,0 +1,30 @@ +const { VerifiedPermissionsClient, IsAuthorizedCommand } = require('@aws-sdk/client-verifiedpermissions'); +const client = new VerifiedPermissionsClient(); + +exports.handler = async (event) => { + const body = JSON.parse(event.body || '{}'); + const { userId, role, action, resourceId, classification } = body; + + if (!userId || !role || !action || !resourceId) { + return { statusCode: 400, body: JSON.stringify({ error: 'Missing required fields: userId, role, action, resourceId' }) }; + } + + const params = { + policyStoreId: process.env.POLICY_STORE_ID, + principal: { entityType: 'MyApp::User', entityId: userId }, + action: { actionType: 'MyApp::Action', actionId: action }, + resource: { entityType: 'MyApp::Document', entityId: resourceId }, + entities: { + entityList: [ + { identifier: { entityType: 'MyApp::User', entityId: userId }, attributes: { role: { string: role } } }, + { identifier: { entityType: 'MyApp::Document', entityId: resourceId }, attributes: { owner: { string: userId }, classification: { string: classification || 'public' } } } + ] + } + }; + + const result = await client.send(new IsAuthorizedCommand(params)); + return { + statusCode: 200, + body: JSON.stringify({ decision: result.decision, userId, action, resourceId, role }) + }; +}; diff --git a/lambda-verified-permissions-cdk/tsconfig.json b/lambda-verified-permissions-cdk/tsconfig.json new file mode 100644 index 000000000..5a686fa68 --- /dev/null +++ b/lambda-verified-permissions-cdk/tsconfig.json @@ -0,0 +1 @@ +{"compilerOptions":{"target":"ES2020","module":"commonjs","lib":["es2020"],"declaration":true,"strict":true,"noImplicitAny":true,"strictNullChecks":true,"noEmit":false,"resolveJsonModule":true,"esModuleInterop":true,"outDir":"./build","rootDir":"."}} From 3e54cc2066a48a2360f32053f75f08deaf8e5ace Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Mon, 8 Jun 2026 04:41:41 +0000 Subject: [PATCH 3/3] fix(lambda-verified-permissions): Address PR review feedback - Add role validation in Lambda to prevent privilege escalation - Use partition-aware ARN (Ref: AWS::Partition) for GovCloud/China - Scope admin Cedar policy to explicit actions instead of wildcard - Add owner and classification policies using schema attributes - Add src/package.json for Lambda runtime dependency - Rewrite testing section with SigV4 curl examples for Function URL - Reference architecture diagram image in README --- lambda-verified-permissions-cdk/README.md | 62 +++++++++++++------ .../lib/lambda-verified-permissions-stack.ts | 36 +++++++++-- lambda-verified-permissions-cdk/src/index.js | 6 ++ .../src/package.json | 7 +++ 4 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 lambda-verified-permissions-cdk/src/package.json diff --git a/lambda-verified-permissions-cdk/README.md b/lambda-verified-permissions-cdk/README.md index 03a099869..e6b40ba1b 100644 --- a/lambda-verified-permissions-cdk/README.md +++ b/lambda-verified-permissions-cdk/README.md @@ -14,43 +14,65 @@ Important: this application uses various AWS services and there are costs associ ## Architecture -``` -┌──────────┐ ┌──────────────────┐ ┌─────────────────────────┐ -│ Client │────▶│ AWS Lambda │────▶│ Amazon Verified │ -│ │ │ (Authorizer) │ │ Permissions │ -└──────────┘ └──────────────────┘ │ (Cedar Policy Store) │ - └─────────────────────────┘ -``` +![Architecture Diagram](architecture.png) + +1. Client sends a SigV4-signed request to the Lambda Function URL. +2. Lambda receives the authorization request with user identity, action, and resource. +3. Lambda calls the Verified Permissions `IsAuthorized` API with the request context. +4. Cedar policies evaluate the request and return ALLOW or DENY. -## How it works +## Cedar Policies -1. Lambda receives an authorization request with user identity, action, and resource. -2. Lambda calls the Verified Permissions `IsAuthorized` API with the request context. -3. Cedar policies evaluate the request and return ALLOW or DENY. -4. The pattern includes two policies: admins can perform any action, readers can only read. +The pattern includes four policies demonstrating different authorization patterns: + +- **Admin policy** – Admins can read, write, and delete documents (scoped to explicit actions). +- **Reader policy** – Readers can only read documents. +- **Owner policy** – Document owners can write to their own documents. +- **Confidential deny policy** – Readers cannot access confidential documents. ## Deployment ```bash +cd src && npm install && cd .. npm install cdk deploy ``` ## Testing +The Lambda Function URL uses `AWS_IAM` authentication, so requests must be signed with [SigV4](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-signing.html). The simplest way is using `curl` with `--aws-sigv4`: + ```bash -python3 -c " -import boto3, json -client = boto3.client('lambda') +# Get the Function URL from stack outputs +FUNCTION_URL=$(aws cloudformation describe-stacks \ + --stack-name LambdaVerifiedPermissionsStack \ + --query 'Stacks[0].Outputs[?OutputKey==`FunctionUrl`].OutputValue' \ + --output text) + # Admin can delete (ALLOW) -r = client.invoke(FunctionName='', Payload=json.dumps({'body': json.dumps({'userId':'alice','role':'admin','action':'Delete','resourceId':'doc-1','classification':'confidential'})})) -print('Admin Delete:', json.loads(json.loads(r['Payload'].read())['body'])['decision']) +curl -s "$FUNCTION_URL" \ + --aws-sigv4 "aws:amz:us-east-1:lambda" \ + --user "$(aws configure get aws_access_key_id):$(aws configure get aws_secret_access_key)" \ + -H "Content-Type: application/json" \ + -d '{"userId":"alice","role":"admin","action":"Delete","resourceId":"doc-1","classification":"confidential"}' + # Reader cannot delete (DENY) -r = client.invoke(FunctionName='', Payload=json.dumps({'body': json.dumps({'userId':'bob','role':'reader','action':'Delete','resourceId':'doc-2','classification':'public'})})) -print('Reader Delete:', json.loads(json.loads(r['Payload'].read())['body'])['decision']) -" +curl -s "$FUNCTION_URL" \ + --aws-sigv4 "aws:amz:us-east-1:lambda" \ + --user "$(aws configure get aws_access_key_id):$(aws configure get aws_secret_access_key)" \ + -H "Content-Type: application/json" \ + -d '{"userId":"bob","role":"reader","action":"Delete","resourceId":"doc-2","classification":"public"}' + +# Reader cannot access confidential documents (DENY) +curl -s "$FUNCTION_URL" \ + --aws-sigv4 "aws:amz:us-east-1:lambda" \ + --user "$(aws configure get aws_access_key_id):$(aws configure get aws_secret_access_key)" \ + -H "Content-Type: application/json" \ + -d '{"userId":"bob","role":"reader","action":"Read","resourceId":"doc-3","classification":"confidential"}' ``` +> **Note:** Replace `us-east-1` with your deployment region. If using temporary credentials (e.g., SSO), include the session token via `--header "x-amz-security-token: $AWS_SESSION_TOKEN"`. + ## Cleanup ```bash diff --git a/lambda-verified-permissions-cdk/lib/lambda-verified-permissions-stack.ts b/lambda-verified-permissions-cdk/lib/lambda-verified-permissions-stack.ts index 5f7e0e267..11be82fb8 100644 --- a/lambda-verified-permissions-cdk/lib/lambda-verified-permissions-stack.ts +++ b/lambda-verified-permissions-cdk/lib/lambda-verified-permissions-stack.ts @@ -8,7 +8,6 @@ export class LambdaVerifiedPermissionsStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); - // Create policy store with Cedar schema const policyStore = new verifiedpermissions.CfnPolicyStore(this, 'PolicyStore', { validationSettings: { mode: 'STRICT' }, schema: { @@ -28,13 +27,12 @@ export class LambdaVerifiedPermissionsStack extends cdk.Stack { } }); - // Create policies new verifiedpermissions.CfnPolicy(this, 'AdminPolicy', { policyStoreId: policyStore.attrPolicyStoreId, definition: { static: { - statement: 'permit(principal, action, resource) when { principal.role == "admin" };', - description: 'Admins can perform any action' + statement: 'permit(principal, action in [MyApp::Action::"Read", MyApp::Action::"Write", MyApp::Action::"Delete"], resource) when { principal.role == "admin" };', + description: 'Admins can read, write, and delete documents' } } }); @@ -49,7 +47,26 @@ export class LambdaVerifiedPermissionsStack extends cdk.Stack { } }); - // Lambda authorizer function + new verifiedpermissions.CfnPolicy(this, 'OwnerWritePolicy', { + policyStoreId: policyStore.attrPolicyStoreId, + definition: { + static: { + statement: 'permit(principal, action == MyApp::Action::"Write", resource) when { resource.owner == principal };', + description: 'Document owners can write to their own documents' + } + } + }); + + new verifiedpermissions.CfnPolicy(this, 'ConfidentialDenyPolicy', { + policyStoreId: policyStore.attrPolicyStoreId, + definition: { + static: { + statement: 'forbid(principal, action, resource) when { principal.role == "reader" && resource.classification == "confidential" };', + description: 'Readers cannot access confidential documents' + } + } + }); + const authFn = new lambda.Function(this, 'AuthorizerFn', { runtime: lambda.Runtime.NODEJS_22_X, handler: 'index.handler', @@ -60,7 +77,14 @@ export class LambdaVerifiedPermissionsStack extends cdk.Stack { authFn.addToRolePolicy(new iam.PolicyStatement({ actions: ['verifiedpermissions:IsAuthorized'], - resources: [`arn:aws:verifiedpermissions::${this.account}:policy-store/${policyStore.attrPolicyStoreId}`] + resources: [cdk.Fn.join('', [ + 'arn:', + this.partition, + ':verifiedpermissions::', + this.account, + ':policy-store/', + policyStore.attrPolicyStoreId + ])] })); const fnUrl = authFn.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.AWS_IAM }); diff --git a/lambda-verified-permissions-cdk/src/index.js b/lambda-verified-permissions-cdk/src/index.js index b0abc3096..82d8223a8 100644 --- a/lambda-verified-permissions-cdk/src/index.js +++ b/lambda-verified-permissions-cdk/src/index.js @@ -1,6 +1,8 @@ const { VerifiedPermissionsClient, IsAuthorizedCommand } = require('@aws-sdk/client-verifiedpermissions'); const client = new VerifiedPermissionsClient(); +const VALID_ROLES = ['admin', 'reader']; + exports.handler = async (event) => { const body = JSON.parse(event.body || '{}'); const { userId, role, action, resourceId, classification } = body; @@ -9,6 +11,10 @@ exports.handler = async (event) => { return { statusCode: 400, body: JSON.stringify({ error: 'Missing required fields: userId, role, action, resourceId' }) }; } + if (!VALID_ROLES.includes(role)) { + return { statusCode: 400, body: JSON.stringify({ error: `Invalid role. Must be one of: ${VALID_ROLES.join(', ')}` }) }; + } + const params = { policyStoreId: process.env.POLICY_STORE_ID, principal: { entityType: 'MyApp::User', entityId: userId }, diff --git a/lambda-verified-permissions-cdk/src/package.json b/lambda-verified-permissions-cdk/src/package.json new file mode 100644 index 000000000..5b5571d32 --- /dev/null +++ b/lambda-verified-permissions-cdk/src/package.json @@ -0,0 +1,7 @@ +{ + "name": "lambda-verified-permissions-authorizer", + "version": "1.0.0", + "dependencies": { + "@aws-sdk/client-verifiedpermissions": "^3.700.0" + } +}