diff --git a/python/README.md b/python/README.md index 4b70780d4..834a23634 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 | diff --git a/python/amazon-verified-permissions-rest-api/.gitignore b/python/amazon-verified-permissions-rest-api/.gitignore new file mode 100644 index 000000000..37833f8be --- /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 000000000..d1f9a925b --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/README.md @@ -0,0 +1,107 @@ + +# Amazon Verified Permissions REST API (AWS CDK 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. + +> :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 + +## Solution Highlights + +- **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. + +| 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. | + +## Quick Start + +### 1. Set Up the Environment + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +Install or upgrade the AWS CDK Toolkit if needed: + +```bash +npm install -g aws-cdk@latest +``` + +Bootstrap the target account and region the first time you deploy CDK there: + +```bash +cdk bootstrap aws:/// +``` + +### 2. Deploy the Stack + +```bash +cdk deploy +``` + +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 + +``` +├── 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 +``` + +## 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. + +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 +``` + +## Additional Reading + +- [`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/app.py b/python/amazon-verified-permissions-rest-api/app.py new file mode 100644 index 000000000..1304d5124 --- /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 000000000..bd863e924 --- /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/docs/architecture.md b/python/amazon-verified-permissions-rest-api/docs/architecture.md new file mode 100644 index 000000000..aaec7f133 --- /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 000000000..77a531a7b --- /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 000000000..1d3f164d2 --- /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 000000000..9ce0f3591 --- /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) 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 000000000..95025dc21 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/requirements.txt @@ -0,0 +1,3 @@ +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 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 000000000..e69de29bb 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 000000000..e69de29bb 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 000000000..c63ebb4e8 --- /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 000000000..f4216e851 --- /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 000000000..b87202532 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/apigw/main.py @@ -0,0 +1,54 @@ +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 + + # ("/") 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", + lambda_integration(admin_lambda), + authorizer=authorizer, + authorization_type=apigw.AuthorizationType.CUSTOM, + ) + + # ("/user") resource + 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 000000000..3e75849f7 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/cognito/main.py @@ -0,0 +1,51 @@ +from aws_cdk import ( + NestedStack, + RemovalPolicy, + 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, + 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( + 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_password=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 000000000..042168589 --- /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 000000000..90e7c4d4b --- /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 000000000..0126d8c5d --- /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 000000000..dba18acee --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/main.py @@ -0,0 +1,86 @@ +from aws_cdk import ( + Stack, + aws_iam, + aws_lambda as _lambda, + CfnOutput, +) +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", + }, + ) + 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, + "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 + apigw = API( + self, + "API", + authorizer=authorizer, + admin_lambda=admin_lambda, + user_lambda=user_lambda, + ) + + # Output the API endpoint + CfnOutput( + self, + "ApiEndpoint", + value=apigw.api.url, + description="API Gateway endpoint URL", + ) 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 000000000..108b6d5a3 --- /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 000000000..f566573eb --- /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 000000000..071488943 --- /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 000000000..d5320e486 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/admin.py @@ -0,0 +1,34 @@ +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", + amazonverified::Action::"get /" + ], + 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 000000000..4a88309fc --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/policy/user.py @@ -0,0 +1,33 @@ +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", + amazonverified::Action::"get /" + ], + 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 000000000..c26edde82 --- /dev/null +++ b/python/amazon-verified-permissions-rest-api/stack/verified_permissions/schema.py @@ -0,0 +1,40 @@ +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"], + } + }, + "get /": { + "appliesTo": { + "context": {"type": "Record", "attributes": {}}, + "principalTypes": ["User"], + "resourceTypes": ["Application"], + } + }, + }, + } +} + + +cedar_schema = {"cedar_json": json.dumps(cedar_json_schema)}