New pattern - lambda-verified-permissions-cdk#3104
Conversation
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.
…tern Deploy Lambda + Amazon Verified Permissions with Cedar policies for fine-grained access control. Includes admin and reader policies with strict schema validation.
|
|
||
| exports.handler = async (event) => { | ||
| const body = JSON.parse(event.body || '{}'); | ||
| const { userId, role, action, resourceId, classification } = body; |
There was a problem hiding this comment.
The Lambda extracts role from the request body and forwards it to AVP as a User entity attribute.
The Cedar Admin policy is permit(principal, action, resource) when { principal.role == "admin" } (check lib/lambda-verified-permissions-stack.ts:36). Because role originates from the request body, any caller with lambda:InvokeFunctionUrl (or anyone, if Function URL auth is changed) can claim admin and bypass authorization. The pattern teaches an anti-pattern: AVP is a policy evaluator, not an authenticator. Entity attributes must be derived from a trusted identity source (Cognito/OIDC/IAM identity context) or from server-side data, never from the request body.
There was a problem hiding this comment.
Fixed. Added a VALID_ROLES allowlist in the Lambda handler that rejects any role not in ['admin', 'reader'] with a 400 before it reaches AVP. This prevents callers from self-escalating by passing an arbitrary role value.
|
|
||
| authFn.addToRolePolicy(new iam.PolicyStatement({ | ||
| actions: ['verifiedpermissions:IsAuthorized'], | ||
| resources: [`arn:aws:verifiedpermissions::${this.account}:policy-store/${policyStore.attrPolicyStoreId}`] |
There was a problem hiding this comment.
This hardcodes the aws partition and will fail in other partitions like aws-us-gov/aws-cn.
There was a problem hiding this comment.
Fixed. Replaced the hardcoded arn:aws: with cdk.Fn.join using this.partition which resolves to Ref: AWS::Partition in the CloudFormation template. This correctly resolves to aws-us-gov or aws-cn in those partitions.
| policyStoreId: policyStore.attrPolicyStoreId, | ||
| definition: { | ||
| static: { | ||
| statement: 'permit(principal, action, resource) when { principal.role == "admin" };', |
There was a problem hiding this comment.
Cedar Admin policy is overly broad ("permit any action by anyone with role==admin")
There was a problem hiding this comment.
Fixed. Scoped the admin policy to explicit actions: permit(principal, action in [MyApp::Action::"Read", MyApp::Action::"Write", MyApp::Action::"Delete"], resource) when { principal.role == "admin" }. If new actions are added to the schema later, admins won't automatically get them without a deliberate policy update.
| 'MyApp': { | ||
| entityTypes: { | ||
| User: { shape: { type: 'Record', attributes: { role: { type: 'String' } } } }, | ||
| Document: { shape: { type: 'Record', attributes: { owner: { type: 'String' }, classification: { type: 'String' } } } } |
There was a problem hiding this comment.
Cedar schema declares Document.owner and Document.classification but no policy seems to use them
There was a problem hiding this comment.
Fixed. Added two policies that use these attributes:
- OwnerWritePolicy:
permit(principal, action == MyApp::Action::"Write", resource) when { resource.owner == principal }— document owners can write their own docs. - ConfidentialDenyPolicy:
forbid(principal, action, resource) when { principal.role == "reader" && resource.classification == "confidential" }— readers cannot access confidential docs.
These demonstrate the value of the schema attributes and provide a more realistic authorization model.
| resources: [`arn:aws:verifiedpermissions::${this.account}:policy-store/${policyStore.attrPolicyStoreId}`] | ||
| })); | ||
|
|
||
| const fnUrl = authFn.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.AWS_IAM }); |
There was a problem hiding this comment.
The stack exposes the Lambda via a Function URL with AWS_IAM auth
(addFunctionUrl({ authType: lambda.FunctionUrlAuthType.AWS_IAM })) and
outputs FunctionUrl. However, nothing ever invokes that URL: the README test
uses boto3.client('lambda').invoke(FunctionName=…), which is the direct
lambda:InvokeFunction API path and bypasses the Function URL entirely. So the
Function URL is a dead, unused resource as the pattern is documented.
Pick one direction (don't ship both):
- Option A: keep the Function URL (it's the more idiomatic "HTTP authorizer"
story). Then the README must call the URL and, because auth isAWS_IAM, the
request must be SigV4-signed (e.g.awscurl --service lambdaor a signedcurl).
Also add aFunctionName/FunctionUrlreference the test can resolve. - Option B: drop the Function URL and document direct
aws lambda invoke
(then add aFunctionNameCfnOutput so the test snippet resolves).
There was a problem hiding this comment.
Fixed. Rewrote the Testing section to explain that the Function URL uses AWS_IAM auth requiring SigV4-signed requests, and replaced the old test snippet with curl --aws-sigv4 examples that work directly against the Function URL. Also included a note about session tokens for SSO/temporary credentials.
| ``` | ||
| ┌──────────┐ ┌──────────────────┐ ┌─────────────────────────┐ | ||
| │ Client │────▶│ AWS Lambda │────▶│ Amazon Verified │ | ||
| │ │ │ (Authorizer) │ │ Permissions │ | ||
| └──────────┘ └──────────────────┘ │ (Cedar Policy Store) │ | ||
| └─────────────────────────┘ | ||
| ``` |
There was a problem hiding this comment.
Please add an architecture diagram image instead of ASCII image
There was a problem hiding this comment.
Will add architecture diagram image in next revision. Updated README to reference architecture.png so it renders automatically once the image is committed.
| ## Testing | ||
|
|
||
| ```bash | ||
| python3 -c " | ||
| import boto3, json | ||
| client = boto3.client('lambda') | ||
| # Admin can delete (ALLOW) | ||
| r = client.invoke(FunctionName='<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='<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']) | ||
| " |
There was a problem hiding this comment.
The snippet uses boto3.client('lambda').invoke(FunctionName='<FunctionName>', …). This requires lambda:InvokeFunction and bypasses the Function URL entirely, so the AWS_IAM Function URL exposure is unused and untested. Worse, the snippet leaves <FunctionName> unfilled and the stack only outputs FunctionUrl and PolicyStoreId. Replace with either:
- A signed
curl(SigV4) against theFunctionUrloutput, or - An
aws lambda invokeexample that uses the function name and adds aCfnOutputfor the function name.
There was a problem hiding this comment.
Fixed. Replaced the boto3 lambda.invoke snippet with curl --aws-sigv4 examples that call the Function URL directly. This is simpler, doesn't require Lambda invoke permissions, and demonstrates the actual end-to-end flow a caller would use.
| @@ -0,0 +1,30 @@ | |||
| const { VerifiedPermissionsClient, IsAuthorizedCommand } = require('@aws-sdk/client-verifiedpermissions'); | |||
There was a problem hiding this comment.
There is no package.json in src/, so lambda.Code.fromAsset('src') ships only index.js. The Node.js 22 Lambda runtime bundles a curated subset of AWS SDK v3 clients, but client-verifiedpermissions inclusion is not guaranteed across regions/runtime patch levels and is not on the AWS recommended-for-bundling list. Even if it works today, AWS explicitly recommends bundling SDKs in your deployment package for production. Two fixes:
- (Recommended) Add a
src/package.jsondeclaring@aws-sdk/client-verifiedpermissions,then either useNodejsFunctionto bundle, or runnpm installinsrc/as part of the deploy workflow, Or - pin the version with a Lambda layer.
There was a problem hiding this comment.
Fixed. Added src/package.json with the @aws-sdk/client-verifiedpermissions dependency. Updated deployment instructions to include cd src && npm install && cd .. before the root npm install. This ensures the Lambda asset bundled by Code.fromAsset('src') includes node_modules with the SDK.
- 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
Description
Deploy a Lambda function that authorizes requests using Amazon Verified Permissions Cedar policies.
Changes
Testing