From c72988492430ae9accaacd151a0e48ad1f3ae287 Mon Sep 17 00:00:00 2001 From: Satya Tulasi Jalandhar C H Date: Tue, 30 Sep 2025 08:48:42 +0000 Subject: [PATCH 01/10] feat: add amazon-verified-permissions-rest-api example --- .../.gitignore | 10 ++ .../README.md | 58 ++++++++ .../app.py | 16 +++ .../cdk.json | 87 ++++++++++++ .../requirements.txt | 3 + .../source.bat | 13 ++ .../stack/__init__.py | 0 .../stack/apigw/__init__.py | 0 .../stack/apigw/authorizer.py | 21 +++ .../stack/apigw/integration.py | 9 ++ .../stack/apigw/main.py | 43 ++++++ .../stack/cognito/main.py | 45 ++++++ .../stack/lambdas/admin/main.py | 8 ++ .../stack/lambdas/authorizer/main.js | 132 ++++++++++++++++++ .../stack/lambdas/user/main.py | 8 ++ .../stack/main.py | 66 +++++++++ .../stack/verified_permissions/__init__.py | 3 + .../stack/verified_permissions/main.py | 71 ++++++++++ .../verified_permissions/policy/__init__.py | 4 + .../verified_permissions/policy/admin.py | 29 ++++ .../stack/verified_permissions/policy/user.py | 30 ++++ .../stack/verified_permissions/schema.py | 33 +++++ 22 files changed, 689 insertions(+) create mode 100644 python/amazon-verified-permissions-rest-api/.gitignore create mode 100644 python/amazon-verified-permissions-rest-api/README.md create mode 100644 python/amazon-verified-permissions-rest-api/app.py create mode 100644 python/amazon-verified-permissions-rest-api/cdk.json create mode 100644 python/amazon-verified-permissions-rest-api/requirements.txt create mode 100644 python/amazon-verified-permissions-rest-api/source.bat create mode 100644 python/amazon-verified-permissions-rest-api/stack/__init__.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/apigw/__init__.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/apigw/authorizer.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/apigw/integration.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/apigw/main.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/cognito/main.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/lambdas/admin/main.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/lambdas/authorizer/main.js create mode 100644 python/amazon-verified-permissions-rest-api/stack/lambdas/user/main.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/main.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/verified_permissions/__init__.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/verified_permissions/main.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/__init__.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/admin.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/user.py create mode 100644 python/amazon-verified-permissions-rest-api/stack/verified_permissions/schema.py diff --git a/python/amazon-verified-permissions-rest-api/.gitignore b/python/amazon-verified-permissions-rest-api/.gitignore new file mode 100644 index 0000000000..37833f8beb --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/.gitignore @@ -0,0 +1,10 @@ +*.swp +package-lock.json +__pycache__ +.pytest_cache +.venv +*.egg-info + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/python/amazon-verified-permissions-rest-api/README.md b/python/amazon-verified-permissions-rest-api/README.md new file mode 100644 index 0000000000..c53f0b50cc --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/README.md @@ -0,0 +1,58 @@ + +# Welcome to your CDK Python project! + +This is a blank project for CDK development with Python. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +This project is set up like a standard Python project. The initialization +process also creates a virtualenv within this project, stored under the `.venv` +directory. To create the virtualenv it assumes that there is a `python3` +(or `python` for Windows) executable in your path with access to the `venv` +package. If for any reason the automatic creation of the virtualenv fails, +you can create the virtualenv manually. + +To manually create a virtualenv on MacOS and Linux: + +``` +$ python3 -m venv .venv +``` + +After the init process completes and the virtualenv is created, you can use the following +step to activate your virtualenv. + +``` +$ source .venv/bin/activate +``` + +If you are a Windows platform, you would activate the virtualenv like this: + +``` +% .venv\Scripts\activate.bat +``` + +Once the virtualenv is activated, you can install the required dependencies. + +``` +$ pip install -r requirements.txt +``` + +At this point you can now synthesize the CloudFormation template for this code. + +``` +$ cdk synth +``` + +To add additional dependencies, for example other CDK libraries, just add +them to your `setup.py` file and rerun the `pip install -r requirements.txt` +command. + +## Useful commands + + * `cdk ls` list all stacks in the app + * `cdk synth` emits the synthesized CloudFormation template + * `cdk deploy` deploy this stack to your default AWS account/region + * `cdk diff` compare deployed stack with current state + * `cdk docs` open CDK documentation + +Enjoy! diff --git a/python/amazon-verified-permissions-rest-api/app.py b/python/amazon-verified-permissions-rest-api/app.py new file mode 100644 index 0000000000..1304d5124a --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/app.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import os +import aws_cdk as cdk +from stack.main import Backend + +app = cdk.App() +Backend( + app, + "AmazonVerifiedPermissionsRestAPI", + env=cdk.Environment( + account=os.getenv("CDK_DEFAULT_ACCOUNT"), + region=os.getenv("CDK_DEFAULT_REGION"), + ), +) + +app.synth() diff --git a/python/amazon-verified-permissions-rest-api/cdk.json b/python/amazon-verified-permissions-rest-api/cdk.json new file mode 100644 index 0000000000..bd863e9244 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/cdk.json @@ -0,0 +1,87 @@ +{ + "app": "python3 app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "**/__pycache__", + "tests" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": true, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true + } +} diff --git a/python/amazon-verified-permissions-rest-api/requirements.txt b/python/amazon-verified-permissions-rest-api/requirements.txt new file mode 100644 index 0000000000..3ab01b1592 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/requirements.txt @@ -0,0 +1,3 @@ +aws-cdk-lib==2.198.0 +constructs>=10.0.0,<11.0.0 +cdklabs.cdk-verified-permissions==0.3.0 \ No newline at end of file diff --git a/python/amazon-verified-permissions-rest-api/source.bat b/python/amazon-verified-permissions-rest-api/source.bat new file mode 100644 index 0000000000..9e1a83442a --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/source.bat @@ -0,0 +1,13 @@ +@echo off + +rem The sole purpose of this script is to make the command +rem +rem source .venv/bin/activate +rem +rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. +rem On Windows, this command just runs this batch file (the argument is ignored). +rem +rem Now we don't need to document a Windows command for activating a virtualenv. + +echo Executing .venv\Scripts\activate.bat for you +.venv\Scripts\activate.bat diff --git a/python/amazon-verified-permissions-rest-api/stack/__init__.py b/python/amazon-verified-permissions-rest-api/stack/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/amazon-verified-permissions-rest-api/stack/apigw/__init__.py b/python/amazon-verified-permissions-rest-api/stack/apigw/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/amazon-verified-permissions-rest-api/stack/apigw/authorizer.py b/python/amazon-verified-permissions-rest-api/stack/apigw/authorizer.py new file mode 100644 index 0000000000..c63ebb4e8b --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/apigw/authorizer.py @@ -0,0 +1,21 @@ +from aws_cdk import ( + Duration, + aws_apigateway as apigateway, +) + + +def create_authorizer(construct, handler): + authorizer = apigateway.RequestAuthorizer( + construct, + "ApiGatewayAuthorizer", + handler=handler, + authorizer_name="AmazonVerifiedPermissions", + identity_sources=[ + apigateway.IdentitySource.header("Authorization"), + apigateway.IdentitySource.context("httpMethod"), + apigateway.IdentitySource.context("path"), + ], + results_cache_ttl=Duration.seconds(120), + ) + + return authorizer diff --git a/python/amazon-verified-permissions-rest-api/stack/apigw/integration.py b/python/amazon-verified-permissions-rest-api/stack/apigw/integration.py new file mode 100644 index 0000000000..f4216e8513 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/apigw/integration.py @@ -0,0 +1,9 @@ +from aws_cdk import aws_apigateway as apigw + + +def lambda_integration(function): + integration = apigw.LambdaIntegration( + handler=function, + ) + + return integration diff --git a/python/amazon-verified-permissions-rest-api/stack/apigw/main.py b/python/amazon-verified-permissions-rest-api/stack/apigw/main.py new file mode 100644 index 0000000000..4ccc9f0475 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/apigw/main.py @@ -0,0 +1,43 @@ +from constructs import Construct +from aws_cdk import ( + NestedStack, + aws_apigateway as apigw, +) + +from .authorizer import create_authorizer +from .integration import lambda_integration + + +class API(NestedStack): + + def __init__( + self, scope: Construct, id: str, authorizer, admin_lambda, user_lambda + ) -> None: + super().__init__(scope, id) + + # Create the REST API + self.api = apigw.RestApi( + self, + "REST", + endpoint_types=[apigw.EndpointType.REGIONAL], + ) + + # Create authorizer + authorizer = create_authorizer(self, authorizer) + + # Create API resources and methods + admin_resource = self.api.root.add_resource("admin") + admin_resource.add_method( + "GET", + lambda_integration(admin_lambda), + authorizer=authorizer, + authorization_type=apigw.AuthorizationType.CUSTOM, + ) + + user_resource = self.api.root.add_resource("user") + user_resource.add_method( + "GET", + lambda_integration(user_lambda), + authorizer=authorizer, + authorization_type=apigw.AuthorizationType.CUSTOM, + ) diff --git a/python/amazon-verified-permissions-rest-api/stack/cognito/main.py b/python/amazon-verified-permissions-rest-api/stack/cognito/main.py new file mode 100644 index 0000000000..5ed9e50db0 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/cognito/main.py @@ -0,0 +1,45 @@ +from aws_cdk import ( + NestedStack, + aws_cognito as cognito, +) +from constructs import Construct + + +class Cognito(NestedStack): + def __init__( + self, + scope: Construct, + id: str, + ) -> None: + super().__init__(scope, id) + + self.user_pool = cognito.UserPool( + self, + "UserPool", + feature_plan=cognito.FeaturePlan.LITE, + ) + + cognito.UserPoolGroup( + self, + "UserPoolGroupAdmin", + user_pool=self.user_pool, + description="Admin Group", + group_name="admin", + precedence=1, + ) + + cognito.UserPoolGroup( + self, + "UserPoolGroupUser", + user_pool=self.user_pool, + description="User Group", + group_name="user", + precedence=2, + ) + + self.user_pool_client = cognito.UserPoolClient( + self, + "UserPoolClient", + user_pool=self.user_pool, + auth_flows=cognito.AuthFlow(user_srp=True), + ) diff --git a/python/amazon-verified-permissions-rest-api/stack/lambdas/admin/main.py b/python/amazon-verified-permissions-rest-api/stack/lambdas/admin/main.py new file mode 100644 index 0000000000..0421685897 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/lambdas/admin/main.py @@ -0,0 +1,8 @@ +import json + + +def handler(event, context): + return { + "statusCode": 200, + "body": json.dumps({"message": "Hello from Admin!"}), + } diff --git a/python/amazon-verified-permissions-rest-api/stack/lambdas/authorizer/main.js b/python/amazon-verified-permissions-rest-api/stack/lambdas/authorizer/main.js new file mode 100644 index 0000000000..90e7c4d4bd --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/lambdas/authorizer/main.js @@ -0,0 +1,132 @@ +const { VerifiedPermissions } = require('@aws-sdk/client-verifiedpermissions'); +const policyStoreId = process.env.POLICY_STORE_ID; +const namespace = process.env.NAMESPACE; +const tokenType = process.env.TOKEN_TYPE; +const resourceType = `${namespace}::Application`; +const resourceId = namespace; +const actionType = `${namespace}::Action`; + +const verifiedpermissions = !!process.env.ENDPOINT + ? new VerifiedPermissions({ + endpoint: `https://${process.env.ENDPOINT}ford.${process.env.AWS_REGION}.amazonaws.com`, + }) + : new VerifiedPermissions(); + +function getContextMap(event) { + const hasPathParameters = Object.keys(event.pathParameters).length > 0; + const hasQueryString = Object.keys(event.queryStringParameters).length > 0; + if (!hasPathParameters && !hasQueryString) { + return undefined; + } + const pathParametersObj = !hasPathParameters ? {} : { + pathParameters: { + // transform regular map into smithy format + record: Object.keys(event.pathParameters).reduce((acc, pathParamKey) => { + return { + ...acc, + [pathParamKey]: { + string: event.pathParameters[pathParamKey] + } + } + }, {}), + } + }; + const queryStringObj = !hasQueryString ? {} : { + queryStringParameters: { + // transform regular map into smithy format + record: Object.keys(event.queryStringParameters).reduce((acc, queryParamKey) => { + return { + ...acc, + [queryParamKey]: { + string: event.queryStringParameters[queryParamKey] + } + } + }, {}), + } + }; + return { + contextMap: { + ...queryStringObj, + ...pathParametersObj, + } + }; +} + +async function handler(event, context) { + // https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html + // > Header names and query parameters are processed in a case-sensitive way. + // https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2 + // > header field names MUST be converted to lowercase prior to their encoding in HTTP/2 + // curl defaults to HTTP/2 + let bearerToken = + event.headers?.Authorization || event.headers?.authorization; + if (bearerToken?.toLowerCase().startsWith('bearer ')) { + // per https://www.rfc-editor.org/rfc/rfc6750#section-2.1 "Authorization" header should contain: + // "Bearer" 1*SP b64token + // however, match behavior of COGNITO_USER_POOLS authorizer allowing "Bearer" to be optional + bearerToken = bearerToken.split(' ')[1]; + } + try { + const parsedToken = JSON.parse(Buffer.from(bearerToken.split('.')[1], 'base64').toString()); + const actionId = `${event.requestContext.httpMethod.toLowerCase()} ${event.requestContext.resourcePath}`; + + const input = { + [tokenType]: bearerToken, + policyStoreId: policyStoreId, + action: { + actionType: actionType, + actionId: actionId, + }, + resource: { + entityType: resourceType, + entityId: resourceId + }, + context: getContextMap(event), + }; + + const authResponse = await verifiedpermissions.isAuthorizedWithToken(input); + console.log('Decision from AVP:', authResponse.decision); + let principalId = `${parsedToken.iss.split('/')[3]}|${parsedToken.sub}`; + if (authResponse.principal) { + const principalEidObj = authResponse.principal; + principalId = `${principalEidObj.entityType}::"${principalEidObj.entityId}"`; + } + + return { + principalId, + policyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'execute-api:Invoke', + Effect: authResponse.decision.toUpperCase() === 'ALLOW' ? 'Allow' : 'Deny', + Resource: event.methodArn + } + ] + }, + context: { + actionId, + } + } + } catch (e) { + console.log('Error: ', e); + return { + principalId: '', + policyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'execute-api:Invoke', + Effect: 'Deny', + Resource: event.methodArn + } + ] + }, + context: {} + } + } +} + +module.exports = { + handler, +}; diff --git a/python/amazon-verified-permissions-rest-api/stack/lambdas/user/main.py b/python/amazon-verified-permissions-rest-api/stack/lambdas/user/main.py new file mode 100644 index 0000000000..0126d8c5d4 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/lambdas/user/main.py @@ -0,0 +1,8 @@ +import json + + +def handler(event, context): + return { + "statusCode": 200, + "body": json.dumps({"message": "Hello from User!"}), + } diff --git a/python/amazon-verified-permissions-rest-api/stack/main.py b/python/amazon-verified-permissions-rest-api/stack/main.py new file mode 100644 index 0000000000..3c11b0098c --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/main.py @@ -0,0 +1,66 @@ +from aws_cdk import ( + Stack, + aws_lambda as _lambda, +) +from constructs import Construct + +from .apigw.main import API +from .cognito.main import Cognito +from .verified_permissions.main import VerifiedPermissionsNested + + +class Backend(Stack): + + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + # Create Cognito User Pool + cognito = Cognito(self, "Cognito") + + # Create Verified Permissions Policy Store + verified_permissions = VerifiedPermissionsNested( + self, + "VerifiedPermissions", + user_pool=cognito.user_pool, + ) + + # Create Authorizer Lambda function + authorizer = _lambda.Function( + self, + "AuthorizerFunction", + runtime=_lambda.Runtime.NODEJS_20_X, + code=_lambda.Code.from_asset("stack/lambdas/authorizer"), + handler="main.handler", + environment={ + "POLICY_STORE_ID": verified_permissions.policy_store_id, + "NAMESPACE": "amazonverified", + "TOKEN_TYPE": "accessToken", + }, + ) + # Create Lambda functions + admin_lambda = _lambda.Function( + self, + "AdminFunction", + runtime=_lambda.Runtime.PYTHON_3_13, + code=_lambda.Code.from_asset("stack/lambdas/admin"), + handler="main.handler", + ) + + user_lambda = _lambda.Function( + self, + "UserFunction", + runtime=_lambda.Runtime.PYTHON_3_13, + code=_lambda.Code.from_asset("stack/lambdas/user"), + handler="main.handler", + ) + + + # Create REST API + API( + self, + "API", + authorizer=authorizer, + admin_lambda=admin_lambda, + user_lambda=user_lambda, + ) + diff --git a/python/amazon-verified-permissions-rest-api/stack/verified_permissions/__init__.py b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/__init__.py new file mode 100644 index 0000000000..108b6d5a30 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/__init__.py @@ -0,0 +1,3 @@ +from .main import VerifiedPermissionsNested + +__all__ = ["VerifiedPermissionsNested"] diff --git a/python/amazon-verified-permissions-rest-api/stack/verified_permissions/main.py b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/main.py new file mode 100644 index 0000000000..f566573eb4 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/main.py @@ -0,0 +1,71 @@ +from cdklabs.cdk_verified_permissions import ( + PolicyStore, + ValidationSettingsMode, + DeletionProtectionMode, + IdentitySourceConfiguration, + IdentitySource, + CognitoUserPoolConfiguration, + CognitoGroupConfiguration, +) +from aws_cdk import NestedStack +from constructs import Construct +from .schema import cedar_schema +from .policy import ( + create_admin_policy, + create_user_policy, +) + + +class VerifiedPermissionsNested(NestedStack): + """A Verified Permissions nested stack. + A nested stack that sets up an Amazon Verified Permissions policy store, + configures it to use a Cognito user pool as an identity source, and adds + the necessary policies. + + Attributes: + scope (Construct): The scope in which this construct is defined. + id (str): The construct ID. + user_pool: The Cognito user pool to be used as an identity source. + kwargs: Additional keyword arguments for the NestedStack. + policy_store (PolicyStore): The Verified Permissions policy store. + """ + + def __init__(self, scope: Construct, id: str, user_pool, **kwargs) -> None: + super().__init__(scope, id, **kwargs) + + user_pool_id = user_pool.user_pool_id + validation_settings_strict = {"mode": ValidationSettingsMode.STRICT} + + self.policy_store = PolicyStore( + self, + "PolicyStore", + schema=cedar_schema, + validation_settings=validation_settings_strict, + description="Policy store", + deletion_protection=DeletionProtectionMode.DISABLED, + ) + + # Configure identity provider + IdentitySource( + self, + "IdentitySource", + configuration=IdentitySourceConfiguration( + cognito_user_pool_configuration=CognitoUserPoolConfiguration( + user_pool=user_pool, + group_configuration=CognitoGroupConfiguration( + group_entity_type="amazonverified::UserGroup" + ), + ) + ), + policy_store=self.policy_store, + principal_entity_type="amazonverified::User", + ) + + # Add policies + create_user_policy(self, self.policy_store, user_pool_id) + create_admin_policy(self, self.policy_store, user_pool_id) + + @property + def policy_store_id(self): + """Return the policy store ID.""" + return self.policy_store.policy_store_id diff --git a/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/__init__.py b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/__init__.py new file mode 100644 index 0000000000..071488943e --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/__init__.py @@ -0,0 +1,4 @@ +from .admin import create_admin_policy +from .user import create_user_policy + +__all__ = ["create_admin_policy", "create_user_policy"] diff --git a/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/admin.py b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/admin.py new file mode 100644 index 0000000000..17603833f4 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/admin.py @@ -0,0 +1,29 @@ +from cdklabs.cdk_verified_permissions import ( + Policy, + PolicyDefinitionProperty, + StaticPolicyDefinitionProperty, +) + + +def create_admin_policy(construct, policy_store, user_pool_id): + """Create and attach a admin policy to the given policy store. + Args: + construct: The CDK construct to attach the policy to. + policy_store: The Verified Permissions Policy Store to attach the policy to. + user_pool_id: The Cognito User Pool to use in the policy. + """ + Policy( + construct, + "AdminPolicy", + definition=PolicyDefinitionProperty( + static=StaticPolicyDefinitionProperty( + statement=f"""permit ( + principal in amazonverified::UserGroup::"{user_pool_id}|admin", + action in [amazonverified::Action::"get /admin", amazonverified::Action::"get /user"], + resource + );""", + description="Policy defining permissions for admin group", + ) + ), + policy_store=policy_store, + ) diff --git a/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/user.py b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/user.py new file mode 100644 index 0000000000..9fdcb30c97 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/user.py @@ -0,0 +1,30 @@ +from cdklabs.cdk_verified_permissions import ( + Policy, + PolicyDefinitionProperty, + StaticPolicyDefinitionProperty, +) + + +def create_user_policy(construct, policy_store, user_pool_id): + """Create and attach a user policy to the given policy store. + Args: + construct: The CDK construct to attach the policy to. + policy_store: The Verified Permissions Policy Store to attach the policy to. + user_pool_id: The Cognito User Pool to use in the policy. + """ + Policy( + construct, + "UserPolicy", + definition=PolicyDefinitionProperty( + static=StaticPolicyDefinitionProperty( + statement=f"""permit ( + principal in amazonverified::UserGroup::"{user_pool_id}|user", + action in + [amazonverified::Action::"get /user"], + resource + );""", + description="Policy defining permissions for user group", + ) + ), + policy_store=policy_store, + ) diff --git a/python/amazon-verified-permissions-rest-api/stack/verified_permissions/schema.py b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/schema.py new file mode 100644 index 0000000000..aa25d48d22 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/schema.py @@ -0,0 +1,33 @@ +import json + +cedar_json_schema = { + "amazonverified": { + "entityTypes": { + "User": { + "shape": {"type": "Record", "attributes": {}}, + "memberOfTypes": ["UserGroup"], + }, + "UserGroup": {"shape": {"attributes": {}, "type": "Record"}}, + "Application": {"shape": {"attributes": {}, "type": "Record"}}, + }, + "actions": { + "get /admin": { + "appliesTo": { + "context": {"type": "Record", "attributes": {}}, + "principalTypes": ["User"], + "resourceTypes": ["Application"], + } + }, + "get /user": { + "appliesTo": { + "context": {"type": "Record", "attributes": {}}, + "principalTypes": ["User"], + "resourceTypes": ["Application"], + } + }, + }, + } +} + + +cedar_schema = {"cedar_json": json.dumps(cedar_json_schema)} From f2a0677b65e6d98ecfb5efae41e025cd50881579 Mon Sep 17 00:00:00 2001 From: Satya Tulasi Jalandhar C H Date: Tue, 30 Sep 2025 08:50:40 +0000 Subject: [PATCH 02/10] docs: add amazon-verified-permissions-rest-api example to README --- python/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/python/README.md b/python/README.md index 4b70780d47..834a236345 100644 --- a/python/README.md +++ b/python/README.md @@ -51,6 +51,7 @@ $ cdk destroy | Example | Description | |---------|-------------| +| [amazon-verified-permissions-rest-api](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/amazon-verified-permissions-rest-api/) | Creating a REST API Gateway with Amazon Verified Permissions for fine-grained authorization | | [api-cors-lambda](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/api-cors-lambda/) | Shows creation of Rest API (GW) with an /example GET endpoint, with CORS enabled | | [application-load-balancer](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/application-load-balancer/) | Using an AutoScalingGroup with an Application Load Balancer | | [appsync-graphql-dynamodb](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/appsync-graphql-dynamodb/) | Creating a single GraphQL API with an API Key, and four Resolvers doing CRUD operations over a single DynamoDB | From 1269417a155529d066aabbe4c7f444dd8c0eb135 Mon Sep 17 00:00:00 2001 From: Satya Tulasi Jalandhar C H Date: Tue, 30 Sep 2025 10:15:16 +0000 Subject: [PATCH 03/10] feat: add user pool configuration options for sign-in aliases and self sign-up --- .../stack/cognito/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/amazon-verified-permissions-rest-api/stack/cognito/main.py b/python/amazon-verified-permissions-rest-api/stack/cognito/main.py index 5ed9e50db0..f819330562 100644 --- a/python/amazon-verified-permissions-rest-api/stack/cognito/main.py +++ b/python/amazon-verified-permissions-rest-api/stack/cognito/main.py @@ -1,5 +1,6 @@ from aws_cdk import ( NestedStack, + RemovalPolicy, aws_cognito as cognito, ) from constructs import Construct @@ -17,6 +18,9 @@ def __init__( self, "UserPool", feature_plan=cognito.FeaturePlan.LITE, + sign_in_aliases=cognito.SignInAliases(email=True, username=False), + self_sign_up_enabled=True, + removal_policy=RemovalPolicy.DESTROY, # Remove this line for production use ) cognito.UserPoolGroup( From aebe5249e64a1bdfbc68f400dae694ae4c0c2f72 Mon Sep 17 00:00:00 2001 From: Satya Tulasi Jalandhar C H Date: Tue, 30 Sep 2025 10:15:49 +0000 Subject: [PATCH 04/10] feat: add API endpoint output and permissions for authorizer Lambda function --- .../stack/main.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/python/amazon-verified-permissions-rest-api/stack/main.py b/python/amazon-verified-permissions-rest-api/stack/main.py index 3c11b0098c..dba18acee2 100644 --- a/python/amazon-verified-permissions-rest-api/stack/main.py +++ b/python/amazon-verified-permissions-rest-api/stack/main.py @@ -1,6 +1,8 @@ from aws_cdk import ( Stack, + aws_iam, aws_lambda as _lambda, + CfnOutput, ) from constructs import Construct @@ -37,6 +39,18 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: "TOKEN_TYPE": "accessToken", }, ) + policy_statement = aws_iam.PolicyStatement( + actions=[ + "verifiedpermissions:isAuthorizedWithToken", + "logs:PutLogEvents", + ], + resources=["*"], + effect=aws_iam.Effect.ALLOW, + ) + + # Grant the Lambda function permissions to call Verified Permissions and write logs + authorizer.role.add_to_policy(policy_statement) + # Create Lambda functions admin_lambda = _lambda.Function( self, @@ -54,9 +68,8 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: handler="main.handler", ) - # Create REST API - API( + apigw = API( self, "API", authorizer=authorizer, @@ -64,3 +77,10 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: user_lambda=user_lambda, ) + # Output the API endpoint + CfnOutput( + self, + "ApiEndpoint", + value=apigw.api.url, + description="API Gateway endpoint URL", + ) From c9f8a82ea681c39d4ebca0e28d07f9a0f030ff06 Mon Sep 17 00:00:00 2001 From: Satya Tulasi Jalandhar C H Date: Tue, 30 Sep 2025 10:17:40 +0000 Subject: [PATCH 05/10] feat: add API methods for root and user resources with custom authorization --- .../stack/apigw/main.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/python/amazon-verified-permissions-rest-api/stack/apigw/main.py b/python/amazon-verified-permissions-rest-api/stack/apigw/main.py index 4ccc9f0475..b872025324 100644 --- a/python/amazon-verified-permissions-rest-api/stack/apigw/main.py +++ b/python/amazon-verified-permissions-rest-api/stack/apigw/main.py @@ -26,6 +26,16 @@ def __init__( authorizer = create_authorizer(self, authorizer) # Create API resources and methods + + # ("/") resource + self.api.root.add_method( + "GET", + lambda_integration(user_lambda), + authorizer=authorizer, + authorization_type=apigw.AuthorizationType.CUSTOM, + ) + + # ("/admin") resource admin_resource = self.api.root.add_resource("admin") admin_resource.add_method( "GET", @@ -34,6 +44,7 @@ def __init__( authorization_type=apigw.AuthorizationType.CUSTOM, ) + # ("/user") resource user_resource = self.api.root.add_resource("user") user_resource.add_method( "GET", From bde8f2387bc302ac4b5056a6ba97decc960611ba Mon Sep 17 00:00:00 2001 From: Satya Tulasi Jalandhar C H Date: Tue, 30 Sep 2025 10:17:50 +0000 Subject: [PATCH 06/10] feat: extend permissions for admin and user policies to include root access --- .../stack/verified_permissions/policy/admin.py | 7 ++++++- .../stack/verified_permissions/policy/user.py | 5 ++++- .../stack/verified_permissions/schema.py | 7 +++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/admin.py b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/admin.py index 17603833f4..d5320e4868 100644 --- a/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/admin.py +++ b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/admin.py @@ -19,7 +19,12 @@ def create_admin_policy(construct, policy_store, user_pool_id): static=StaticPolicyDefinitionProperty( statement=f"""permit ( principal in amazonverified::UserGroup::"{user_pool_id}|admin", - action in [amazonverified::Action::"get /admin", amazonverified::Action::"get /user"], + action in + [ + amazonverified::Action::"get /admin", + amazonverified::Action::"get /user", + amazonverified::Action::"get /" + ], resource );""", description="Policy defining permissions for admin group", diff --git a/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/user.py b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/user.py index 9fdcb30c97..4a88309fce 100644 --- a/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/user.py +++ b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/user.py @@ -20,7 +20,10 @@ def create_user_policy(construct, policy_store, user_pool_id): statement=f"""permit ( principal in amazonverified::UserGroup::"{user_pool_id}|user", action in - [amazonverified::Action::"get /user"], + [ + amazonverified::Action::"get /user", + amazonverified::Action::"get /" + ], resource );""", description="Policy defining permissions for user group", diff --git a/python/amazon-verified-permissions-rest-api/stack/verified_permissions/schema.py b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/schema.py index aa25d48d22..c26edde828 100644 --- a/python/amazon-verified-permissions-rest-api/stack/verified_permissions/schema.py +++ b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/schema.py @@ -25,6 +25,13 @@ "resourceTypes": ["Application"], } }, + "get /": { + "appliesTo": { + "context": {"type": "Record", "attributes": {}}, + "principalTypes": ["User"], + "resourceTypes": ["Application"], + } + }, }, } } From b6fb91323369b16bc40be725916ced2fbd32c194 Mon Sep 17 00:00:00 2001 From: Satya Tulasi Jalandhar C H Date: Tue, 30 Sep 2025 11:52:03 +0000 Subject: [PATCH 07/10] fix: update auth flows for user pool client --- .../stack/cognito/main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/amazon-verified-permissions-rest-api/stack/cognito/main.py b/python/amazon-verified-permissions-rest-api/stack/cognito/main.py index f819330562..3e75849f7b 100644 --- a/python/amazon-verified-permissions-rest-api/stack/cognito/main.py +++ b/python/amazon-verified-permissions-rest-api/stack/cognito/main.py @@ -20,7 +20,7 @@ def __init__( feature_plan=cognito.FeaturePlan.LITE, sign_in_aliases=cognito.SignInAliases(email=True, username=False), self_sign_up_enabled=True, - removal_policy=RemovalPolicy.DESTROY, # Remove this line for production use + removal_policy=RemovalPolicy.DESTROY, # Remove this line for production use ) cognito.UserPoolGroup( @@ -45,5 +45,7 @@ def __init__( self, "UserPoolClient", user_pool=self.user_pool, - auth_flows=cognito.AuthFlow(user_srp=True), + auth_flows=cognito.AuthFlow( + user_password=True, + ), ) From e6bf16884d3253d66fa80e37faadb3646e2df55b Mon Sep 17 00:00:00 2001 From: Satya Tulasi Jalandhar C H Date: Tue, 30 Sep 2025 12:12:43 +0000 Subject: [PATCH 08/10] docs: add documentation with an architecture diagram --- .../README.md | 117 +++++++++++++----- .../docs/architecture.md | 61 +++++++++ .../docs/deployment.md | 47 +++++++ .../docs/diagram.svg | 37 ++++++ .../docs/operations.md | 90 ++++++++++++++ 5 files changed, 318 insertions(+), 34 deletions(-) create mode 100644 python/amazon-verified-permissions-rest-api/docs/architecture.md create mode 100644 python/amazon-verified-permissions-rest-api/docs/deployment.md create mode 100644 python/amazon-verified-permissions-rest-api/docs/diagram.svg create mode 100644 python/amazon-verified-permissions-rest-api/docs/operations.md diff --git a/python/amazon-verified-permissions-rest-api/README.md b/python/amazon-verified-permissions-rest-api/README.md index c53f0b50cc..d1f9a925b4 100644 --- a/python/amazon-verified-permissions-rest-api/README.md +++ b/python/amazon-verified-permissions-rest-api/README.md @@ -1,58 +1,107 @@ -# Welcome to your CDK Python project! +# Amazon Verified Permissions REST API (AWS CDK Python) -This is a blank project for CDK development with Python. +This example deploys a REST API secured by Amazon Verified Permissions (AVP) and Amazon Cognito. The stack demonstrates how to combine a Cedar policy store with an API Gateway Request Authorizer so that only members of the appropriate Cognito group can invoke protected routes. -The `cdk.json` file tells the CDK Toolkit how to execute your app. +> :information_source: Detailed guides live in the `docs/` directory: +> - [`docs/architecture.md`](docs/architecture.md) – system design and authorization model +> - [`docs/deployment.md`](docs/deployment.md) – environment setup and deployment steps +> - [`docs/operations.md`](docs/operations.md) – testing, troubleshooting, and cleanup -This project is set up like a standard Python project. The initialization -process also creates a virtualenv within this project, stored under the `.venv` -directory. To create the virtualenv it assumes that there is a `python3` -(or `python` for Windows) executable in your path with access to the `venv` -package. If for any reason the automatic creation of the virtualenv fails, -you can create the virtualenv manually. +## Solution Highlights -To manually create a virtualenv on MacOS and Linux: +- **End-to-end RBAC** – Cognito users receive access tokens that are evaluated by AVP before API Gateway invokes your Lambda handlers. +- **Cedar-first design** – The Cedar schema and policies are provisioned with the `cdklabs.cdk_verified_permissions` library for repeatable deployments. +- **Composable infrastructure** – Cognito, Verified Permissions, and API Gateway live in dedicated nested stacks to make future customization straightforward. +- **Language mix** – The authorizer runs on Node.js (to use the AWS SDK for AVP), while the demo business logic stays in simple Python Lambda handlers. -``` -$ python3 -m venv .venv -``` +| Resource | Description | +|----------|-------------| +| Cognito User Pool & Client | Provides user management and issues JWT access tokens. Adds `admin` and `user` groups. | +| Verified Permissions Policy Store | Hosts the Cedar schema and static policies mapping Cognito groups to REST actions. | +| API Gateway REST API | Exposes `/`, `/user`, and `/admin` endpoints secured by a custom Request Authorizer. | +| Lambda Functions | Node.js authorizer for AVP calls plus Python handlers that simulate protected resources. | -After the init process completes and the virtualenv is created, you can use the following -step to activate your virtualenv. +## Quick Start -``` -$ source .venv/bin/activate +### 1. Set Up the Environment + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt ``` -If you are a Windows platform, you would activate the virtualenv like this: +Install or upgrade the AWS CDK Toolkit if needed: -``` -% .venv\Scripts\activate.bat +```bash +npm install -g aws-cdk@latest ``` -Once the virtualenv is activated, you can install the required dependencies. +Bootstrap the target account and region the first time you deploy CDK there: +```bash +cdk bootstrap aws:/// ``` -$ pip install -r requirements.txt + +### 2. Deploy the Stack + +```bash +cdk deploy ``` -At this point you can now synthesize the CloudFormation template for this code. +Note the `ApiEndpoint` output once the deployment completes. + +For additional deployment options, refer to [`docs/deployment.md`](docs/deployment.md). + +## Trying the API + +1. Create or confirm a Cognito user and add them to the `user` and/or `admin` groups. The implementation guide provides ready-to-run CLI snippets. +2. Authenticate against the user pool (for example, with `aws cognito-idp initiate-auth`) and capture the access token. +3. Call the API with the bearer token: + + ```bash + curl -i \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + "$API_URL/user" + ``` + + - Members of the `user` group receive `Hello from User!`. + - Only `admin` members may call `/admin`. + +## Project Layout ``` -$ cdk synth +├── app.py # CDK entry point +├── stack/ +│ ├── main.py # Root stack wiring together nested stacks +│ ├── apigw/ # REST API, Lambda integrations, authorizer +│ ├── cognito/ # Cognito user pool, client, and groups +│ ├── verified_permissions/ # Cedar schema and policies +│ └── lambdas/ # Authorizer (Node.js) and demo handlers (Python) +└── docs/implementation-guide.md # In-depth deployment and operations guide ``` -To add additional dependencies, for example other CDK libraries, just add -them to your `setup.py` file and rerun the `pip install -r requirements.txt` -command. +## Customizing Verified Permissions + +1. Update `stack/verified_permissions/schema.py` with new entity types or actions. +2. Modify or add policy definitions under `stack/verified_permissions/policy/`. +3. Redeploy with `cdk deploy` to publish the schema and policies. -## Useful commands +Consider migrating to policy templates if you need per-tenant or per-resource authorization decisions. + +## Cleanup + +Destroy the stack when you are finished to avoid unexpected charges: + +```bash +cdk destroy +``` - * `cdk ls` list all stacks in the app - * `cdk synth` emits the synthesized CloudFormation template - * `cdk deploy` deploy this stack to your default AWS account/region - * `cdk diff` compare deployed stack with current state - * `cdk docs` open CDK documentation +## Additional Reading -Enjoy! +- [`docs/architecture.md`](docs/architecture.md) – architecture diagrams, component breakdown, and Cedar policy model. +- [`docs/deployment.md`](docs/deployment.md) – prerequisites, bootstrapping, and deployment workflows. +- [`docs/operations.md`](docs/operations.md) – post-deployment tasks, troubleshooting tips, and sample CLI commands. +- [Amazon Verified Permissions User Guide](https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/) +- [Cedar policy language](https://www.cedarpolicy.com/) diff --git a/python/amazon-verified-permissions-rest-api/docs/architecture.md b/python/amazon-verified-permissions-rest-api/docs/architecture.md new file mode 100644 index 0000000000..aaec7f133d --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/docs/architecture.md @@ -0,0 +1,61 @@ +# Architecture Overview + +## Solution Summary + +This project deploys a reference REST API that protects endpoints with [Amazon Verified Permissions (AVP)](https://aws.amazon.com/verified-permissions/) and Amazon Cognito. It demonstrates how to combine a Cedar policy store with an API Gateway Request Authorizer so that only members of the appropriate Cognito group can invoke protected routes. + +## High-Level Diagram + +![High-level architecture diagram](diagram.svg) + +## Core Components + +| Component | Purpose | Source Reference | +|-----------|---------|------------------| +| Amazon Cognito User Pool | Manages users and issues JWT access tokens. Provides `admin` and `user` groups for RBAC. | `stack/cognito/main.py` | +| Amazon Verified Permissions | Stores the Cedar schema and group-based policies. | `stack/verified_permissions/` | +| Amazon API Gateway REST API | Provides `/`, `/user`, and `/admin` resources secured by the custom authorizer. | `stack/apigw/main.py` | +| Request Authorizer Lambda (Node.js) | Exchanges the bearer token and request context for an AVP decision via `isAuthorizedWithToken`. | `stack/lambdas/authorizer/main.js` | +| Demo Business Lambdas (Python) | Return simple payloads to illustrate RBAC outcomes. | `stack/lambdas/{user,admin}/main.py` | + +## Request Authorization Flow + +1. A client authenticates against the Cognito User Pool to obtain an access token. +2. The client calls the API Gateway endpoint, sending the JWT in the `Authorization` header. +3. API Gateway forwards the request to the custom Request Authorizer Lambda. +4. The authorizer extracts the bearer token, request path, and method, then calls `VerifiedPermissions.isAuthorizedWithToken`. +5. Verified Permissions evaluates the Cedar policies against the supplied action and principal: + - Members of the `admin` group can invoke all routes. + - Members of the `user` group can invoke `/` and `/user` only. +6. API Gateway allows or denies the original request based on the evaluation result. + +## Authorization Model + +### Namespace and Entities + +The Cedar namespace is `amazonverified`. It defines: + +- `User`: Represents a Cognito user. +- `UserGroup`: Represents Cognito groups (`admin`, `user`). +- `Application`: Represents the protected API surface. + +Actions map one-to-one with the REST routes: `get /`, `get /user`, and `get /admin`. + +### Schema Reference + +The Cedar schema is defined in `stack/verified_permissions/schema.py` and supplied to the `PolicyStore` during stack synthesis. The schema is serialized to JSON through `cedar_schema = {"cedar_json": json.dumps(cedar_json_schema)}`. + +### Policies + +Policies are provided via `StaticPolicyDefinitionProperty` constructs: + +- `stack/verified_permissions/policy/admin.py` grants the `admin` group access to every action. +- `stack/verified_permissions/policy/user.py` grants the `user` group access to `get /` and `get /user`. + +Because policies refer to Cognito groups using the pattern `"|"`, redeploying to a new environment automatically scopes the policy to the correct pool. + +### Extending the Model + +1. Update `cedar_json_schema` with new actions or entity types. +2. Add or modify policy definitions. You can compose static policies or migrate to policy templates for dynamic authorizations. +3. Re-deploy the stack to publish the new schema and policies. diff --git a/python/amazon-verified-permissions-rest-api/docs/deployment.md b/python/amazon-verified-permissions-rest-api/docs/deployment.md new file mode 100644 index 0000000000..77a531a7ba --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/docs/deployment.md @@ -0,0 +1,47 @@ +# Deployment Guide + +> Verified Permissions is available in specific AWS regions. Choose a supported region (for example, `ap-south-1`) before deploying. + +## Prerequisites + +- AWS account with permissions to create IAM, Cognito, Lambda, API Gateway, and Verified Permissions resources. +- AWS CLI configured with credentials for the target account and region. +- Node.js v22.20.0 or lts (required for the AWS CDK Toolkit and Lambda bundling). +- Python 3.13 or later. +- AWS CDK Toolkit (`npm install -g aws-cdk@latest`). + +## Set Up the Project Environment + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +If the virtual environment fails to create automatically, create it manually with the same commands. + +## Bootstrap the Environment (First Deployment Only) + +```bash +cdk bootstrap aws:/// +``` + +Replace `` and `` with the target deployment values. + +## Deploy the Stack + +```bash +cdk deploy +``` + +The deployment outputs the REST API endpoint (for example, `https://abc123.execute-api.us-east-1.amazonaws.com/prod/`). Record this value for testing. + +## Updating the Stack + +After modifying infrastructure or policies, redeploy with the same command: + +```bash +cdk deploy +``` + +CDK will perform a change set comparison and apply only the required updates. diff --git a/python/amazon-verified-permissions-rest-api/docs/diagram.svg b/python/amazon-verified-permissions-rest-api/docs/diagram.svg new file mode 100644 index 0000000000..1d3f164d29 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/docs/diagram.svg @@ -0,0 +1,37 @@ + + + + + Icon-Architecture/32/Arch_Amazon-Verified-Permissions_32 + + + + + + + + eraser.io + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/python/amazon-verified-permissions-rest-api/docs/operations.md b/python/amazon-verified-permissions-rest-api/docs/operations.md new file mode 100644 index 0000000000..9ce0f35916 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/docs/operations.md @@ -0,0 +1,90 @@ +# Operations & Testing + +## Post-Deployment Tasks + +### Define the Client and User Pool IDs + +1. In the AWS Management Console, open **Amazon Cognito** and select your user pool. +2. Copy the **App client ID** for the deployed client and the **User pool ID**. + +Set them as shell variables for the commands below: + +```bash +CLIENT_ID= + +USER_POOL_ID= +``` + +### Create a Test User + +```bash +aws cognito-idp sign-up --client-id "$CLIENT_ID" --username demo@example.com --password 'P@ssw0rd!' --user-attributes Name=email,Value=demo@example.com + +aws cognito-idp admin-confirm-sign-up --user-pool-id "$USER_POOL_ID" --username demo@example.com +``` + +### Assign Groups + +```bash +aws cognito-idp admin-add-user-to-group --user-pool-id "$USER_POOL_ID" --username demo@example.com --group-name user + +aws cognito-idp admin-add-user-to-group --user-pool-id "$USER_POOL_ID" --username demo@example.com --group-name admin # optional elevated access +``` + +## Request Tokens & Call the API + +1. Initiate authentication: + + ```bash + aws cognito-idp initiate-auth \ + --auth-flow USER_PASSWORD_AUTH \ + --client-id "$CLIENT_ID" \ + --auth-parameters USERNAME=demo@example.com,PASSWORD='P@ssw0rd!' + ``` + +2. Extract the `AccessToken` from the response. +3. Invoke the API: + + ```bash + API_URL= + ACCESS_TOKEN= + + curl -i \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + "$API_URL/user" + ``` + +Users in the `user` group receive `Hello from User!`. Only `admin` group members receive `Hello from Admin!` from the `/admin` route. + +## Troubleshooting + +- **403 errors on every route:** Ensure the token is an *access* token (not an ID token) and that the Cognito app client allows the SRP auth flow. +- **`AccessDeniedException` from AVP:** Confirm the policy store was created in a region that supports Verified Permissions and that the token issuer matches the configured user pool. +- **Caching delays:** The Request Authorizer caches decisions for 120 seconds. Use different tokens or redeploy the stack when testing new policies. + +## Observability + +The authorizer Lambda logs every AVP decision. Review the CloudWatch log stream for entries similar to `Decision from AVP: Allow` or `Decision from AVP: Deny`. + +## Security & Operational Considerations + +- Remove `RemovalPolicy.DESTROY` from the user pool before production to avoid accidental data loss. +- Replace the demo Lambda functions with real business logic or integrate with existing services. +- Integrate infrastructure-as-code validation and automated tests before promoting changes. +- Rotate Cognito secrets and enforce password policies that meet organizational requirements. + +## Cleanup + +To avoid ongoing charges when finished experimenting: + +```bash +cdk destroy +``` + +Verify that the Cognito users, Verified Permissions policy store, and CloudWatch logs are removed. + +## Additional Resources + +- [Amazon Verified Permissions documentation](https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/) +- [Cedar policy language](https://docs.cedarpolicy.com/) +- [AWS Cloud Development Kit (AWS CDK) v2](https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-python.html) From b9a6a89ae7dd017207e18827f4f9c80f551f0982 Mon Sep 17 00:00:00 2001 From: Satya Tulasi Jalandhar C H Date: Fri, 10 Oct 2025 04:04:58 +0000 Subject: [PATCH 09/10] refactor: remove unnecessary windows batch script --- .../amazon-verified-permissions-rest-api/source.bat | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 python/amazon-verified-permissions-rest-api/source.bat diff --git a/python/amazon-verified-permissions-rest-api/source.bat b/python/amazon-verified-permissions-rest-api/source.bat deleted file mode 100644 index 9e1a83442a..0000000000 --- a/python/amazon-verified-permissions-rest-api/source.bat +++ /dev/null @@ -1,13 +0,0 @@ -@echo off - -rem The sole purpose of this script is to make the command -rem -rem source .venv/bin/activate -rem -rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. -rem On Windows, this command just runs this batch file (the argument is ignored). -rem -rem Now we don't need to document a Windows command for activating a virtualenv. - -echo Executing .venv\Scripts\activate.bat for you -.venv\Scripts\activate.bat From be031b233b3a6cc19b124b1b788ff6df47bef85b Mon Sep 17 00:00:00 2001 From: Satya Tulasi Jalandhar C H Date: Fri, 10 Oct 2025 04:05:09 +0000 Subject: [PATCH 10/10] fix: update aws-cdk-lib version to 2.219.0 in requirements --- python/amazon-verified-permissions-rest-api/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/amazon-verified-permissions-rest-api/requirements.txt b/python/amazon-verified-permissions-rest-api/requirements.txt index 3ab01b1592..95025dc218 100644 --- a/python/amazon-verified-permissions-rest-api/requirements.txt +++ b/python/amazon-verified-permissions-rest-api/requirements.txt @@ -1,3 +1,3 @@ -aws-cdk-lib==2.198.0 +aws-cdk-lib==2.219.0 constructs>=10.0.0,<11.0.0 cdklabs.cdk-verified-permissions==0.3.0 \ No newline at end of file