├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── reference-architectures ├── diagrams │ ├── dynamodb-pooled-isolation.png │ ├── relational-database-pooled-isolation.png │ └── relational-database-sharding.png ├── dynamodb-pooled-isolation.md ├── relational-database-pooled-isolation.md └── relational-database-sharding.md └── samples ├── aurora-serverless-global-db-cdk ├── .gitignore ├── .npmignore ├── README.md ├── bin │ └── aurora-global-db-multistack.ts ├── cdk.context.json ├── cdk.json ├── fargate │ ├── Dockerfile │ ├── package.json │ └── src │ │ └── index.js ├── jest.config.js ├── lib │ ├── aurora-global-cluster-stack.ts │ ├── aurora-regional-cluster-stack.ts │ └── fargate-test-app-stack.ts ├── package.json ├── test │ └── global-aurora-serverless-cdk.test.ts └── tsconfig.json ├── multi-tenant-vector-database ├── amazon-aurora │ ├── aws-managed │ │ ├── 1_build_vector_db_on_aurora.sql │ │ ├── 2_bedrock_knowledgebase_managed_rag.ipynb │ │ └── policy-templates │ │ │ ├── bedrock_aurora_cluster_permissions_policy.json │ │ │ ├── bedrock_data_source_permissions_policy.json │ │ │ ├── bedrock_model_permissions_policy.json │ │ │ ├── bedrock_secrets_permissions_policy.json │ │ │ └── bedrock_trust_relationship_policy.json │ ├── cdk │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── bin │ │ │ └── cdk.ts │ │ ├── cdk.json │ │ ├── jest.config.js │ │ ├── lib │ │ │ └── aurora-cdk-stack.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── test │ │ │ └── cdk.test.ts │ │ └── tsconfig.json │ ├── metadata_tags │ │ ├── Home_Survey_Tenant1.pdf.metadata.json │ │ ├── Home_Survey_Tenant2.pdf.metadata.json │ │ ├── Home_Survey_Tenant3.pdf.metadata.json │ │ ├── Home_Survey_Tenant4.pdf.metadata.json │ │ └── Home_Survey_Tenant5.pdf.metadata.json │ ├── multi_tenant_survey_reports │ │ ├── Home_Survey_Tenant1.pdf │ │ ├── Home_Survey_Tenant2.pdf │ │ ├── Home_Survey_Tenant3.pdf │ │ ├── Home_Survey_Tenant4.pdf │ │ └── Home_Survey_Tenant5.pdf │ └── self-managed │ │ ├── 1_build_vector_db_on_aurora.sql │ │ └── 2_sql_based_self_managed_rag.ipynb └── amazon-opensearch │ ├── README.md │ ├── cfn │ └── opensearch-template.yaml │ ├── fully-managed │ ├── opensearch_fully_managed_notebook.ipynb │ └── policy-templates │ │ ├── bedrock_data_source_permissions_policy.json │ │ ├── bedrock_model_permissions_policy.json │ │ ├── bedrock_opensearch_collection_permissions_policy.json │ │ └── bedrock_trust_relationship_policy.json │ ├── images │ ├── os_fully_managed_arch.jpg │ └── os_self-managed_arch.jpg │ ├── metadata_tags │ ├── Home_Survey_Tenant1.pdf.metadata.json │ ├── Home_Survey_Tenant2.pdf.metadata.json │ ├── Home_Survey_Tenant3.pdf.metadata.json │ ├── Home_Survey_Tenant4.pdf.metadata.json │ └── Home_Survey_Tenant5.pdf.metadata.json │ ├── multi_tenant_survey_reports │ ├── Home_Survey_Tenant1.pdf │ ├── Home_Survey_Tenant2.pdf │ ├── Home_Survey_Tenant3.pdf │ ├── Home_Survey_Tenant4.pdf │ └── Home_Survey_Tenant5.pdf │ └── self-managed │ ├── opensearch_self_managed_notebook.ipynb │ └── policy-templates │ ├── bedrock_model_permissions_policy.json │ ├── bedrock_trust_relationship_policy.json │ ├── secretsmanager_permissions_policy.json │ └── secretsmanager_trust_relationship_policy.json ├── rds-data-api-rls ├── rds-data-api-rls-function.ipynb ├── rds-data-api-rls-function.py ├── rds-data-api-rls-transaction.ipynb └── rds-data-api-rls-transaction.py ├── scheduled-aurora-serverless-scaling ├── README.md ├── bin │ └── scheduled-autoscaling-amazon-aurora-serverless.ts ├── cdk.context.json ├── cdk.json ├── images │ ├── activity.png │ ├── architecture.png │ ├── peak.png │ ├── scaleup.png │ └── scaling.png ├── jest.config.js ├── lib │ └── scheduled-autoscaling-amazon-aurora-serverless-stack.ts ├── package-lock.json ├── package.json ├── test │ └── cdk.test.ts └── tsconfig.json └── tenant-isolation-patterns ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .npmignore ├── .projenrc.js ├── LICENSE ├── README.md ├── cdk.json ├── package.json ├── src ├── api │ ├── api.ts │ └── authorizer.function.ts ├── dynamodb │ ├── dynamodb.function.ts │ └── dynamodb.ts ├── identity │ ├── identity.ts │ └── pre-token-generation.function.ts └── main.ts ├── test ├── __snapshots__ │ └── main.test.ts.snap └── main.test.ts ├── test_interface.sh ├── tsconfig.dev.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | cdk.out 3 | node_modules 4 | yarn.lock 5 | .gitattributes 6 | .projen/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Data for SaaS 2 | 3 | This repository contains a collection of samples, best practices and reference architectures for implementing SaaS applications on AWS for databases and data services. 4 | 5 | ## Contents 6 | 7 | * [Reference Architectures](./reference-architectures/) 8 | * [Samples](./samples/) 9 | * * [RDS Data API Row-level Security](README.md#rds-data-api-row-level-security) 10 | * * [Multi-tenant vector databases](README.md#multi-tenant-vector-databases) 11 | * * [Scheduled Autoscaling Aurora Serverless V2](README.md#scheduled-autoscaling-aurora-serverless-v2) 12 | * * [Aurora Global Database Serverless V2](README.md#aurora-global-database-serverless-v2) 13 | * * [Tenant isolation patterns](README.md#tenant-isolation-patterns) 14 | * [Data for SaaS blogs](README.md#data-for-saas-blogs-books) 15 | * [Videos](README.md#videos-movie_camera) 16 | 17 | ## RDS Data API Row-level Security 18 | 19 | Row-level security is commonly employed in multi-tenant databases to provide isolation between tenant's data. Row level security policies are created in the database to enforce this isolation on tenant-owned tables. 20 | 21 | This sample contains 2 examples for implementing row-level security using the [RDS data API for Amazon Aurora](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html). The examples provided are in Python, but they can be easily transferred to other languages using the same patterns. Additionally, these examples will work for Amazon Aurora PostgreSQL provisioned or serverless V2. 22 | 23 | [Example 1 - RLS with PostgreSQL function](./samples/rds-data-api-rls/rds-data-api-rls-function.ipynb) 24 | 25 | [Example 2 - RLS with database transactions](./samples/rds-data-api-rls/rds-data-api-rls-transaction.ipynb) 26 | 27 | ## Multi-tenant vector databases 28 | 29 | ### Amazon Aurora 30 | 31 | Vector databases are commonly employed to store embeddings generated as part of generative-AI applications. A popular option is to use the pgvector extension for PostgreSQL. Amazon Aurora PostgreSQL supports the pgvector extension to store embeddings from machine learning (ML) models in your database and to perform efficient similarity searches. 32 | 33 | You can use an existing Aurora PostgreSQL cluster or use the CDK code to provision an Aurora PostgreSQL cluster that is a prerequisite for running the sample. 34 | 35 | [Deploy Aurora PostgreSQL using CDK](./samples/multi-tenant-vector-database/amazon-aurora/cdk/README.md) 36 | 37 | This sample shows how to use pgvector in a multi-tenant database, enforcing tenant isolation. One example is using a self-managed Retrieval-augmented generation (RAG) implementation, the other is using [Amazon Bedrock knowledge bases](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html). 38 | 39 | [Example 1 - Self-managed](./samples/multi-tenant-vector-database/amazon-aurora/self-managed/) 40 | 41 | [Example 2 - AWS-managed](./samples/multi-tenant-vector-database/amazon-aurora/aws-managed/) 42 | 43 | ### Amazon OpenSearch 44 | 45 | Amazon OpenSearch Service offers powerful vector search capabilities in both provisioned and serverless deployment options. These vector stores enable efficient similarity searches on high-dimensional data, making them ideal for applications like semantic search, recommendation systems, and image recognition. With provisioned OpenSearch, users have granular control over cluster configuration and scaling, while the serverless option provides on-demand, automatically scaled resources without the need to manage infrastructure. 46 | 47 | You can use the CloudFormation template to provision the Amazon OpenSearch provisioned domain and the serverless collection, that are prerequisites to run this sample. 48 | 49 | [Deploy Amazon OpenSearch using CloudFormation](./samples/multi-tenant-vector-database/amazon-opensearch/README.md) 50 | 51 | This sample describes both the self-managed Retrieval-augmented generation (RAG) implementation and also the fully-managed RAG approach using [Amazon Bedrock knowledge bases](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html). 52 | 53 | [Example 1 - Self-managed](./samples/multi-tenant-vector-database/amazon-opensearch/self-managed/opensearch_self_managed_notebook.ipynb) 54 | 55 | [Example 2 - Fully-managed](./samples/multi-tenant-vector-database/amazon-opensearch/fully-managed/opensearch_fully_managed_notebook.ipynb) 56 | 57 | ## Scheduled Autoscaling Aurora Serverless V2 58 | 59 | This sample provides a CDK application that creates a scheduled job to scale up and down an Aurora Serverless V2 instance minimum capacity. This is useful for SaaS applications with predictable usage patterns to reduce scaling times during busy periods. 60 | 61 | [Scheduled Autoscaling Aurora Serverless V2](./samples/scheduled-aurora-serverless-scaling/) 62 | 63 | ## Aurora Global Database Serverless V2 64 | 65 | This sample provides a CDK application that creates Amazon Aurora Global database custer across a primary and secondary region for SaaS applications that need global footprint and for disaster recovery strategies. The stack also includes a Fargate container application to test the primary and secondary regions with CRUD API operations. 66 | 67 | [Aurora Global Database Serverless V2](./samples/aurora-serverless-global-db-cdk/) 68 | 69 | ## Tenant isolation patterns 70 | 71 | This sample provides tenant isolation patterns using [Attribute Based Access Control (ABAC)](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction_attribute-based-access-control.html), implemented with [AssumeRoleWithWebIdentity](https://aws.amazon.com/blogs/security/saas-tenant-isolation-with-abac-using-aws-sts-support-for-tags-in-jwt/) for a robust tenant isolation mechanism. 72 | 73 | ## Data for SaaS Blogs :books: 74 | 75 | Below is a collection of published blog posts covering different aspects of building data architectures for SaaS applications on AWS: 76 | 77 | ### [Scale your relational database for SaaS](https://aws.amazon.com/blogs/database/scale-your-relational-database-for-saas-part-1-common-scaling-patterns/) 78 | 79 | This blog post provides guidance for software as a service (SaaS) providers who are using relational databases, such as Amazon RDS and Amazon Aurora, and are looking to scale their databases effectively as their business grows. The post explores common scaling patterns for relational databases in a SaaS context, including scaling vertical and horizontal resources, optimizing operations through techniques like micro-batching and table partitioning, and bringing in purpose-built databases for specific use cases. The post discusses the importance of understanding the trade-offs and aligning the scaling approach with the SaaS partitioning model (silo, bridge, or pool) and usage patterns. The post aims to help SaaS providers make informed decisions about scaling their relational databases while considering factors like performance, operational complexity, and tenant isolation. 80 | 81 | ### [Managed database backup and recovery in a multi-tenant SaaS application](https://aws.amazon.com/blogs/database/managed-database-backup-and-recovery-in-a-multi-tenant-saas-application/) 82 | 83 | This blog post discusses approaches for managing database backup and recovery in a multi-tenant SaaS application deployed on AWS. It explores how different database partitioning models (silo, bridge, pool) influence backup and recovery complexity. The post compares segregating tenant data during backup vs during recovery, providing examples using PostgreSQL on Amazon RDS and Aurora. It covers complete vs selective restore scenarios, minimizing costs during recovery, and maintaining a recovery inventory for large datasets. The key takeaway is that there is no one-size-fits-all approach, and the choice depends on factors like the partitioning model, data volumes, and requirements around backup frequency, costs, and recovery flexibility. Proper testing of the backup/recovery solution is emphasized as critical. 84 | 85 | ### [Choose the right PostgreSQL data access pattern for your SaaS application](https://aws.amazon.com/blogs/database/choose-the-right-postgresql-data-access-pattern-for-your-saas-application/) 86 | 87 | The post explores different data access patterns for multi-tenant SaaS applications using Amazon RDS for PostgreSQL or Amazon Aurora PostgreSQL-Compatible Edition. It covers the silo, bridge, and pool database isolation models combined with different compute isolation approaches (siloed or pooled). The access patterns vary based on factors like using AWS IAM authentication vs AWS Secrets Manager, the ability to scope permissions using IAM session policies or ABAC, and enforcement mechanisms like PostgreSQL's row-level security. The tradeoffs between isolation strength, cost efficiency, operational complexity, and noisy neighbor impact are evaluated for each pattern. Code examples are provided for implementing the different access patterns. 88 | 89 | ### [Modernize legacy databases using event sourcing and CQRS with AWS DMS](https://aws.amazon.com/blogs/database/modernize-legacy-databases-using-event-sourcing-and-cqrs-with-aws-dms/) 90 | 91 | This blog post discusses two approaches to implement event sourcing and Command Query Responsibility Segregation (CQRS) architecture using AWS Database Migration Service (AWS DMS). The first approach uses only AWS DMS to replicate data from a monolithic source database to an Amazon S3 event store, and then from the event store to downstream databases like DynamoDB. The second approach combines AWS DMS with Amazon Managed Streaming for Apache Kafka (Amazon MSK) to replicate data from the source database to a Kafka topic, which is then consumed by downstream systems like DynamoDB and an S3 event store. The post explains the benefits of each approach, provides instructions to deploy sample solutions using AWS Serverless Application Model (AWS SAM) templates, and discusses how these architectures future-proof applications by enabling data portability and the ability to replay events into any data store in the future. 92 | 93 | ### [Send webhooks to SaaS applications from Amazon Aurora via Amazon EventBridge](https://aws.amazon.com/blogs/database/send-webhooks-to-saas-applications-from-amazon-aurora-via-amazon-eventbridge/) 94 | 95 | This blog post explains how to use Amazon Aurora PostgreSQL and Amazon EventBridge to send outgoing webhooks (HTTP callbacks) to external SaaS applications like Salesforce, Marketo, or ServiceNow. The solution involves configuring an Aurora PostgreSQL cluster to invoke an AWS Lambda function when certain events occur (e.g. creating a new database record). This serverless architecture reduces the need for custom webhook processing code and infrastructure management overhead. The post provides a step-by-step walkthrough using an AWS CDK sample project to deploy and test the solution. 96 | 97 | ### [Enforce row-level security with the RDS Data API](https://aws.amazon.com/blogs/database/enforce-row-level-security-with-the-rds-data-api/) 98 | 99 | This blog post discusses how to enforce row-level security in Amazon Aurora PostgreSQL-Compatible Edition using the RDS Data API and PostgreSQL features. It provides an overview of row-level security policies in PostgreSQL and demonstrates how to create a shared tenant schema, define a row-level security policy, and test the tenant isolation using both traditional connection management and the RDS Data API. The post highlights the benefits of using the RDS Data API for securely querying filtered data without managing database connections or connection pools. It also covers cost considerations, metering strategies, and cleanup steps. Overall, the post aims to help readers build secure and scalable multi-tenant PostgreSQL architectures on AWS. 100 | 101 | ### [Partitioning and Isolating Multi-Tenant SaaS Data with Amazon S3](https://aws.amazon.com/blogs/apn/partitioning-and-isolating-multi-tenant-saas-data-with-amazon-s3/) 102 | 103 | This blog post discusses various strategies for partitioning and isolating multi-tenant data using Amazon S3 in SaaS applications, presenting three main approaches: bucket-per-tenant model, object key prefix-per-tenant model, and database-mapped tenant objects. The bucket-per-tenant model assigns separate buckets for each tenant but has limitations due to AWS bucket quotas, while the prefix-per-tenant model allows better scalability by using key name prefixes to associate objects with tenants within shared buckets. For enhanced security, the article explains how tenant isolation can be achieved through IAM policies, access points, and encryption using AWS KMS, with options for server-side encryption and envelope encryption. The blog also covers tenant activity tracking and cost management through features like S3 Inventory, server access logging, and cost allocation tags. Finally, this covers lifecycle management options for different tenant tiers and recommends additional security and cost management configurations such as S3 Intelligent-Tiering, disabling ACLs, and implementing S3 Block Public Access. 104 | 105 | Note: The blog mentions hard quota of 1,000 buckets per AWS account. However, this has been increased to 1 million starting [Nov 2024](https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-s3-up-1-million-buckets-per-aws-account/) 106 | 107 | ### [Multi-tenant RAG with Amazon Bedrock Knowledge Bases](https://aws.amazon.com/blogs/machine-learning/multi-tenant-rag-with-amazon-bedrock-knowledge-bases/) 108 | 109 | The blog provides a technical deep-dive into implementing multi-tenant RAG architecture using Amazon Bedrock Knowledge Bases, integrating with Amazon S3 for document storage, OpenSearch Service for vector database, and DynamoDB for tenant configuration management. The implementation includes detailed code examples for metadata filtering, tenant configuration storage, and API interactions using AWS SDK for Python (Boto3), specifically demonstrating the usage of RetrieveAndGenerate API with knowledge base configurations and vector search filtering. The solution addresses technical challenges around document chunking, vector embeddings, HNSW algorithm parameters, and considers performance limitations such as ingestion job sizes (max 100GB), document limits (5 million documents per data source), and embedding model throughput. 110 | 111 | ### Videos :movie_camera: 112 | 113 | * [ Building SaaS on AWS - Building a modern data architecture for SaaS ](https://www.youtube.com/watch?v=KGR4SQMNsXo) 114 | * [ Data for SaaS YouTube Playlist ](https://youtube.com/playlist?list=PLoqD0z_296PbKATwUcaGowmOJwlE_2ysP&si=kytUeU0uZsNrNF_R) 115 | 116 | ## Security 117 | 118 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 119 | 120 | ## License 121 | 122 | This library is licensed under the MIT-0 License. See the LICENSE file. 123 | 124 | -------------------------------------------------------------------------------- /reference-architectures/diagrams/dynamodb-pooled-isolation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/reference-architectures/diagrams/dynamodb-pooled-isolation.png -------------------------------------------------------------------------------- /reference-architectures/diagrams/relational-database-pooled-isolation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/reference-architectures/diagrams/relational-database-pooled-isolation.png -------------------------------------------------------------------------------- /reference-architectures/diagrams/relational-database-sharding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/reference-architectures/diagrams/relational-database-sharding.png -------------------------------------------------------------------------------- /reference-architectures/dynamodb-pooled-isolation.md: -------------------------------------------------------------------------------- 1 | # DynamoDB pooled isolation 2 | 3 | This example illustrates a secure approach to implementing item-level security in Amazon DynamoDB. This solution leverages scoped IAM credentials, and conditional access policies to ensure that each tenant can only access and modify data that belongs to them, while leveraging a single DynamoDB table for all tenants. By following this approach, SaaS providers can efficiently manage and secure tenant data, reducing operational overhead while ensuring tenant isolation and compliance with privacy and security requirements. 4 | 5 | ![arch diagram](./diagrams/dynamodb-pooled-isolation.png) 6 | 7 | ## Components 8 | 9 | 1. **JWT Token**: A JSON Web Token (JWT) is issued to the user or client application, which contains information about the tenant they belong to. This is included in the authorization header in the request. 10 | 11 | 2. **JWT Token Manager**: The JWT Token Manager is responsible for validating and managing the JWT tokens issued to clients and parsing the claims as part of the tenant context. 12 | 13 | 3. **Microservice**: The microservice receives the JWT token from the client and performs the following actions: 14 | - Assumes the `TenantRole` by providing the `Role` and `TenantId` tags from the JWT token. 15 | - Obtains Scoped Credentials based on the assumed `TenantRole`. 16 | 17 | 4. **STS (AWS Security Token Service)**: The AWS Security Token Service (STS) is used to assume the `TenantRole` and obtain Scoped Credentials for the microservice. These scoped credentials only grant access to items where the partition key begins with the tenantId supplied in the tenant context. 18 | 19 | 5. **Amazon DynamoDB Table**: The microservice uses the Scoped Credentials to access the Amazon DynamoDB table and perform operations based on the tenant's access permissions. The DynamoDB table contains data partitioned by tenant, with items belonging to different tenants (e.g., `tenant-1` and `tenant-2`). 20 | 21 | The key components in this architecture are: 22 | 23 | - **TenantRole**: An AWS Identity and Access Management (IAM) role which via session tags grants permissions to access only the data belonging to that tenant. 24 | - **TenantScoped Policy**: An IAM policy attached to the `TenantRole`, which defines the access permissions for the tenant. The policy uses the `dynamodb:LeadingKeys` condition key to restrict access based on the tenant identifier (`${aws:PrincipalTag/tenant}`). 25 | - **Amazon DynamoDB Table**: A DynamoDB table that stores data for multiple tenants, with each item containing a tenant identifier (e.g., `tenant-1`, `tenant-2`) to distinguish the data belonging to different tenants. 26 | 27 | The primary goal of this architecture is to implement item-level security in DynamoDB by ensuring that each tenant can only access and modify data that belongs to them, while leveraging a single DynamoDB table for all tenants. This is achieved through the use of scoped IAM credentials, and conditional access policies based on the tenant identifier. -------------------------------------------------------------------------------- /reference-architectures/relational-database-pooled-isolation.md: -------------------------------------------------------------------------------- 1 | # Relational database pooled isolation 2 | 3 | In this example a pooled partitioning model is used for both the compute and the database. In this scenario, data isolation is enforced at the database level rather than at the network or IAM level. In practice, this is achieved by using [PostgreSQL Row Level Security policies](https://www.postgresql.org/docs/9.5/ddl-rowsecurity.html). This policy evaluates a session variable set based on the tenant context. This variable is then used to filter all result sets to only show results for that tenant. The tenant is retrieved via the JWT token passed to the microservice. As a best practice, in a production application you should also include these query filters ("WHERE" clauses) rather than relying solely on the RLS policy in order to provide an extra layer of protection against a misconfiguration. 4 | 5 | ![arch diagram](./diagrams/relational-database-pooled-isolation.png) 6 | 7 | ## Components 8 | 9 | 1. **JWT Token**: The process begins with a JSON Web Token (JWT) provided for authentication. 10 | 11 | 2. **JWT Token Manager**: The JWT token is validated and managed by the JWT Token Manager component. 12 | 13 | 3. **Assumed IAM Role**: An AWS Identity and Access Management (IAM) role is assumed, which provides temporary security credentials to access the AWS Secrets Manager securely. 14 | 15 | 4. **AWS Secrets Manager and Password Authentication**: Using the assumed IAM role's credentials, the application retrieves the database credentials from AWS Secrets Manager. These credentials are then used to authenticate with the RDS instance using password authentication. 16 | 17 | * Additionally, during this step, the application sets the `app.current_tenant` variable based on the authenticated tenant's ID using the following SQL command: 18 | 19 | ```sql 20 | SET app.current_tenant = tenant1; 21 | ``` 22 | 23 | 5. **RDS Instance and Row-Level Security (RLS)**: The data for all tenants is stored in a single database. Row-Level Security (RLS) is implemented using a policy named `tenant_policy` defined on the `users` table. This policy ensures that each tenant can only access rows where the `tenant_id` matches the current tenant ID set by the application in step 4. 24 | 25 | The `tenant_policy` is created using the following SQL statement: 26 | 27 | ```sql 28 | CREATE POLICY tenant_policy 29 | ON users 30 | USING ( 31 | tenant_id = (current_setting('app.current_tenant')) 32 | ); 33 | ``` 34 | 35 | The `users` table contains columns for `tenant_id`, `user_id`, and potentially other user-specific data. 36 | 37 | This approach allows for multi-tenant isolation within a single database by leveraging PostgreSQL's Row-Level Security feature and securely managing database credentials using AWS Secrets Manager. 38 | 39 | For more details see [Choose the right PostgreSQL data access pattern for your SaaS application](https://aws.amazon.com/blogs/database/choose-the-right-postgresql-data-access-pattern-for-your-saas-application/) -------------------------------------------------------------------------------- /reference-architectures/relational-database-sharding.md: -------------------------------------------------------------------------------- 1 | # Relational database sharding 2 | 3 | The below diagram illustrates an architecture that enables a microservice to route queries to the correct sharded database instance based on the tenant context contained within a JSON Web Token (JWT). 4 | 5 | ![arch diagram](./diagrams/relational-database-sharding.png) 6 | 7 | ## Components 8 | 9 | 1. **User**: Represents a user interacting with the SaaS application. 10 | 2. **Microservice**: The core application component responsible for processing user requests and interacting with the database. 11 | 3. **JWT manager**: A component responsible for inspecting and validating JSON Web Tokens (JWTs). 12 | 4. **Data access manager**: A component that resolves the connection detailsbetween the microservice and the appropriate database instance. 13 | 5. **DynamoDB Mapping Table**: An Amazon DynamoDB table that stores the mapping between tenant IDs and the corresponding database instance details (e.g., shard IDs). 14 | 6. The database service hosting the tenant-specific database instances. e.g. **Amazon Aurora**. 15 | 16 | ## Steps 17 | 18 | 1. The user initiates a request to the SaaS application, and the request includes a JWT containing the tenant context. 19 | 2. The microservice receives the request and passes the JWT to the data access manager. 20 | 3. The data access manager invokes the JWT manager to inspect and validate the JWT, extracting the tenant_id field. 21 | 4. Using the extracted tenant_id, the data access manager queries the DynamoDB Mapping Table to retrieve the corresponding database instance details (e.g., shard_id). 22 | 5. The data access manager returns the database instance details to the microservice. 23 | 6. With the database instance details, the microservice can establish a connection to the appropriate Amazon Aurora database instance and perform the necessary operations for the specific tenant. 24 | 25 | This architecture promotes data isolation and secure access by ensuring that each tenant's data is stored in a dedicated database instance. The use of JWTs and the centralized mapping table (DynamoDB Mapping Table) enables the routing of requests to the correct database instance based on the tenant context. Additionally, the modular design with separate components for JWT management and data access management promotes code reusability and maintainability. 26 | 27 | For more details see [Scale your relational database for SaaS](https://aws.amazon.com/blogs/database/scale-your-relational-database-for-saas-part-2-sharding-and-routing/) -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/.gitignore: -------------------------------------------------------------------------------- 1 | !jest.config.js 2 | *.d.ts 3 | node_modules 4 | .DS_Store 5 | package-lock.json 6 | 7 | # CDK asset staging directory 8 | .cdk.staging 9 | cdk.out 10 | -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/README.md: -------------------------------------------------------------------------------- 1 | # Amazon Aurora Global Database - Serverless V2 2 | SaaS application often need to have a global footprint to serve different markets across regions. Amazon Aurora Global Database is designed for globally distributed applications, allowing a single Amazon Aurora database to span multiple AWS Regions. Also SaaS applications use this feature to improve their disaster recovery and resiliency strategy. 3 | 4 | This sample creates a serverless global database cluster enabling data replication from the primary region to secondary region cluster. You can update the dbInstanceClass to create a provisioned global database cluster. The regional primary cluster contains a serverless db instance supporting both writes and reads. The regional secondary cluster contains a serverless db instance supporting only reads. In the unlikely event of a regional degradation or outage, the secondary region can be promoted to read and write capabilities in less than 1 minute. 5 | 6 | Important: This sample uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. 7 | 8 | ## Requirements 9 | 10 | * [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. 11 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured 12 | * [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 13 | * [Node and NPM](https://nodejs.org/en/download/) installed. 14 | * [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed and configured 15 | 16 | ## Deployment Instructions 17 | 18 | 1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: 19 | ``` 20 | git clone https://github.com/aws-samples/data-for-saas-patterns 21 | ``` 22 | 2. Change directory to the sample: 23 | ``` 24 | cd data-for-saas-patterns/samples/aurora-serverless-global-db-cdk 25 | ``` 26 | 3. Install dependencies: 27 | ``` 28 | npm install 29 | ``` 30 | 4. Configure AWS CDK to bootstrap the AWS account, primary region and secondary region : 31 | ``` 32 | cdk bootstrap 111111111111/eu-west-1 33 | cdk bootstrap 111111111111/eu-west-2 34 | ``` 35 | 5. Configure ingressIpAddress context variable in the cdk.context.json. This ip address will be configured in the ingress SecurityGroup rule for the Fargate test app. 36 | ``` 37 | x.x.x.x/32 38 | ``` 39 | 6. From the command line, use AWS CDK to deploy the all the stacks synthesized: 40 | ``` 41 | cdk deploy --all 42 | ``` 43 | Alternatively you can also deploy each stack individually. From the command line, use AWS CDK to deploy each stack: 44 | ``` 45 | cdk deploy aurora-global-cluster 46 | cdk deploy primary-cluster 47 | cdk deploy secondary-cluster 48 | cdk deploy primary-test-app 49 | cdk deploy secondary-test-app 50 | ``` 51 | 7. Note the outputs from the CDK deployment process. These contain the primary and secondary URLs which are used for testing. 52 | 53 | ## How it works 54 | 55 | - The template in this sample synthesizes five stacks for deployment using the multiple stack approach of CDK. 56 | - The first stack named aurora-global-cluster creates the Aurora Global Cluster. 57 | - The second stack named primary-cluster deploys the primary cluster with a serverless v2 instance in the primary region defined. 58 | - The third stack named secondary-cluster deploys the secondary cluster with a serverless v2 instance in the secondary region defined. 59 | - The fourth and fifth stack named primary-test-app and secondary-test-app deploys a fargate container with a nodejs app for testing the global database tables. 60 | - The primary cluster supports both write and read operations. The secondary cluster supports read operation only. 61 | - Once you deploy all the stacks, you have built a global database that automatically replicates data from the primary to the secondary region. 62 | 63 | ## Testing 64 | 65 | Note down the primary-test-app.FargateServiceURL and secondary-test-app.FargateServiceURL values from the CDK output and update them in each of the test commands below. 66 | 67 | 1. Initialize the Global Database by creating a table 68 | 69 | ``` 70 | curl primary-test-app.FargateServiceURL/init 71 | ``` 72 | 73 | Expected Response : 74 | "Task table created.." 75 | 76 | 2. Write a task into the Primary Cluster 77 | ``` 78 | curl -X POST primary-test-app.FargateServiceURL/tasks -H 'Content-Type: application/json' -d '{"name":"Task1","status":"created"}' 79 | ``` 80 | Expected Response : Task added with ID: 1 81 | 3. Read the tasks from the Secondary cluster 82 | ``` 83 | curl secondary-test-app.FargateServiceURL/tasks 84 | ``` 85 | Expected Response : 86 | [{"id":1,"name":"Task1","status":"created"}] 87 | 88 | 4. Attempt to write a task into the Secondary cluster 89 | ``` 90 | curl -X POST secondary-test-app.FargateServiceURL/tasks -H 'Content-Type: application/json' -d '{"name":"Task1","status":"created"}' 91 | ``` 92 | Expected Response : error : cannot execute INSERT in a read-only transaction 93 | 94 | Note : You can enable [Write forwarding](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-global-database-write-forwarding.html) feature in Amazon Aurora Global database to continue to use the Secondary cluster endpoint for write transactions as well. 95 | 96 | 5. You can also try to update and delete the task on the Primary cluster 97 | ``` 98 | curl -X PUT primary-test-app.FargateServiceURL/tasks/1 -H 'Content-Type: application/json' -d '{"name":"Task1","status":"in-progress"}' 99 | 100 | curl -X DELETE primary-test-app.FargateServiceURL/tasks/1 101 | 102 | ``` 103 | 6. Check if the task is deleted from the Secondary cluster 104 | ``` 105 | curl secondary-test-app.FargateServiceURL/tasks 106 | ``` 107 | Expected Response : 108 | [] 109 | 110 | ## Cleanup 111 | 112 | 1. Delete the stack 113 | ``` 114 | cdk destroy --all 115 | ``` 116 | ---- 117 | Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 118 | 119 | SPDX-License-Identifier: MIT-0 120 | -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/bin/aurora-global-db-multistack.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'aws-cdk-lib'; 2 | import { AuroraGlobalClusterStack } from '../lib/aurora-global-cluster-stack'; 3 | import { AuroraRegionalClusterStack } from '../lib/aurora-regional-cluster-stack'; 4 | import { FargateTestAppStack } from '../lib/fargate-test-app-stack'; 5 | 6 | const app = new App(); 7 | 8 | const account = app.node.tryGetContext('account') || process.env.CDK_INTEG_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT; 9 | const primaryRegion = { account: account, region: 'eu-west-1' }; 10 | const secondaryRegion = { account: account, region: 'eu-west-2' }; 11 | 12 | const globalCluster = new AuroraGlobalClusterStack(app, "aurora-global-cluster", { 13 | env: primaryRegion, 14 | }); 15 | 16 | const primaryclusterstack = new AuroraRegionalClusterStack(app, `primary-cluster`, { 17 | env: primaryRegion, cfnGlobalCluster: globalCluster.cfnGlobalCluster, isPrimary: true 18 | }); 19 | 20 | const secondaryclusterstack = new AuroraRegionalClusterStack(app, `secondary-cluster`, { 21 | env: secondaryRegion, cfnGlobalCluster: globalCluster.cfnGlobalCluster, isPrimary: false 22 | }); 23 | 24 | primaryclusterstack.addDependency(globalCluster); 25 | secondaryclusterstack.addDependency(primaryclusterstack) 26 | 27 | const primarytestappstack = new FargateTestAppStack(app, `primary-test-app`, { 28 | env: primaryRegion, 29 | endpoint: primaryclusterstack.endpoint, 30 | port: primaryclusterstack.port, 31 | vpc: primaryclusterstack.vpc, 32 | isPrimary: true, 33 | region: primaryclusterstack.region, 34 | dbSecurityGroupId: primaryclusterstack.dbSecurityGroupId 35 | }); 36 | 37 | const secondarytestappstack = new FargateTestAppStack(app, `secondary-test-app`, { 38 | env: secondaryRegion, 39 | endpoint: secondaryclusterstack.endpoint, 40 | port: secondaryclusterstack.port, 41 | vpc: secondaryclusterstack.vpc, 42 | isPrimary: false, 43 | region: primaryclusterstack.region, 44 | dbSecurityGroupId: secondaryclusterstack.dbSecurityGroupId 45 | }); 46 | 47 | primarytestappstack.addDependency(primaryclusterstack); 48 | secondarytestappstack.addDependency(secondaryclusterstack); 49 | 50 | app.synth(); -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "ingressIpAddress": "27.63.217.195/32" 3 | } 4 | -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/aurora-global-db-multistack.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/fargate/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 node:16-alpine 2 | WORKDIR /usr 3 | COPY package.json ./ 4 | COPY src ./src 5 | RUN npm install 6 | RUN npm install pg 7 | COPY . . 8 | EXPOSE 80 9 | CMD ["npm","start"] -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/fargate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fargate", 3 | "version": "1.0.0", 4 | "description": "Fargate ECS container to run an express app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node src/index.js" 9 | }, 10 | "author": "nihilson", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.18.2", 14 | "pg": "^8.11.3", 15 | "aws-sdk": "latest" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/fargate/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const port = 80; 4 | 5 | var AWS = require('aws-sdk'); 6 | const Pool = require('pg').Pool 7 | 8 | var client = new AWS.SecretsManager({ 9 | region: process.env.REGION 10 | }); 11 | 12 | var pool; 13 | 14 | client.getSecretValue({ SecretId: 'aurora-serverless-global-db-secret' }, function (err, data) { 15 | if (err) { 16 | console.log(err, err.stack); 17 | } 18 | else { 19 | secretParams = JSON.parse(data.SecretString); 20 | pool = new Pool({ 21 | user: secretParams.username, 22 | host: process.env.DB_HOST, 23 | database: 'sample', 24 | password: secretParams.password, 25 | port: process.env.DB_PORT 26 | }); 27 | } 28 | }); 29 | 30 | app.use(express.urlencoded({ extended: false })); 31 | app.use(express.json()); 32 | 33 | const errorHandler = (error, request, response) => { 34 | console.log(`error ${error.message}`) 35 | const status = error.status || 400 36 | response.status(status).send(`error : ${error.message}`) 37 | } 38 | 39 | app.get('/', (req, res) => { 40 | res.status(200).send('Hello from Fargate Test App :-)'); 41 | }); 42 | 43 | 44 | app.get('/init', (req, res) => { 45 | pool.query('CREATE TABLE IF NOT EXISTS tasks ( ID SERIAL PRIMARY KEY,name VARCHAR(30), status VARCHAR(30))', (error, results) => { 46 | if (error) { 47 | return errorHandler(error, req, res) 48 | } 49 | res.status(200).json('Task table created..') 50 | }) 51 | 52 | }); 53 | 54 | app.get('/tasks', (req, res) => { 55 | pool.query('SELECT * FROM tasks ORDER BY id ASC', (error, results) => { 56 | if (error) { 57 | return errorHandler(error, req, res) 58 | } 59 | res.status(200).json(results.rows) 60 | }) 61 | }); 62 | 63 | app.get('/tasks/:id', (req, res) => { 64 | const id = parseInt(req.params.id) 65 | pool.query('SELECT * FROM tasks WHERE id = $1', [id], (error, results) => { 66 | if (error) { 67 | return errorHandler(error, req, res) 68 | } 69 | res.status(200).json(results.rows) 70 | }) 71 | }); 72 | 73 | app.post('/tasks', (req, res) => { 74 | const { name, status } = req.body 75 | pool.query('INSERT INTO tasks (name, status) VALUES ($1, $2) RETURNING id', [name, status], (error, results) => { 76 | if (error) { 77 | return errorHandler(error, req, res) 78 | } 79 | console.log(results) 80 | res.status(201).send(`Task added with ID: ${results.rows[0].id}`) 81 | }) 82 | 83 | }); 84 | 85 | app.put('/tasks/:id', (req, res) => { 86 | const id = parseInt(req.params.id) 87 | const { name, status } = req.body 88 | pool.query('UPDATE tasks SET name = $1, status = $2 WHERE id = $3', [name, status, id], (error, results) => { 89 | if (error) { 90 | return errorHandler(error, req, res) 91 | } 92 | res.status(200).send(`Task modified with ID: ${id}`) 93 | } 94 | ) 95 | 96 | }); 97 | 98 | app.delete('/tasks/:id', (req, res) => { 99 | const id = parseInt(req.params.id) 100 | pool.query('DELETE FROM tasks WHERE id = $1', [id], (error, results) => { 101 | if (error) { 102 | return errorHandler(error, req, res) 103 | } 104 | res.status(200).send(`Task deleted with ID: ${id}`) 105 | }) 106 | }); 107 | 108 | app.use(errorHandler) 109 | 110 | app.listen(port, () => { 111 | console.log(`server started at http://localhost:${port}`); 112 | }); -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/lib/aurora-global-cluster-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { StackProps, Stack, RemovalPolicy, Duration } from 'aws-cdk-lib' 3 | import { CfnGlobalCluster } from 'aws-cdk-lib/aws-rds'; 4 | import { Key, KeySpec } from 'aws-cdk-lib/aws-kms'; 5 | 6 | export class AuroraGlobalClusterStack extends Stack { 7 | public readonly cfnGlobalCluster: CfnGlobalCluster; 8 | constructor(scope: Construct, id: string, props?: StackProps) { 9 | super(scope, id, props); 10 | 11 | // Aurora Global Cluster 12 | const cfnGlobalCluster = new CfnGlobalCluster(this, 'AuroraGlobalCluster', { 13 | engine: 'aurora-postgresql', 14 | engineVersion: '14.6', 15 | globalClusterIdentifier: 'aurora-serverless-global-cluster', 16 | }); 17 | 18 | const key = new Key(this, 'Key', { 19 | keySpec: KeySpec.SYMMETRIC_DEFAULT, 20 | description: "DB Encryption Key", 21 | alias: "db-encryption-key", 22 | removalPolicy: RemovalPolicy.DESTROY, 23 | pendingWindow: Duration.days(7), 24 | enabled: true, 25 | }); 26 | 27 | this.cfnGlobalCluster = cfnGlobalCluster; 28 | } 29 | } 30 | 31 | export interface GlobalClusterProps extends StackProps { 32 | cfnGlobalCluster: CfnGlobalCluster, 33 | isPrimary?: boolean 34 | } -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/lib/aurora-regional-cluster-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Stack, StackProps, aws_secretsmanager as sm, CfnDynamicReference, CfnDynamicReferenceService } from 'aws-cdk-lib' 3 | import { SubnetType, Vpc, SecurityGroup, IpAddresses } from 'aws-cdk-lib/aws-ec2'; 4 | import { AuroraCapacityUnit, CfnDBCluster, CfnDBInstance, CfnDBSubnetGroup } from 'aws-cdk-lib/aws-rds'; 5 | import { GlobalClusterProps } from './aurora-global-cluster-stack'; 6 | import { FlowLogMaxAggregationInterval, FlowLogTrafficType } from 'aws-cdk-lib/aws-ec2'; 7 | 8 | export class AuroraRegionalClusterStack extends Stack { 9 | public readonly endpoint: string; 10 | public readonly port: string; 11 | public readonly vpc: Vpc; 12 | public readonly region: string; 13 | public readonly dbSecurityGroupId: string; 14 | constructor(scope: Construct, id: string, props: GlobalClusterProps) { 15 | super(scope, id, props); 16 | 17 | const cfnGlobalCluster = props.cfnGlobalCluster; 18 | const databasename = 'sample'; 19 | 20 | // VPC 21 | const vpc = new Vpc(this, 'Vpc', { 22 | ipAddresses: IpAddresses.cidr('10.0.0.0/16'), 23 | natGateways: 0, 24 | subnetConfiguration: [ 25 | { 26 | cidrMask: 24, 27 | name: 'aurora_isolated_', 28 | subnetType: SubnetType.PRIVATE_ISOLATED 29 | }, 30 | { 31 | cidrMask: 24, 32 | name: 'public', 33 | subnetType: SubnetType.PUBLIC 34 | } 35 | ] 36 | }); 37 | 38 | vpc.addFlowLog('FlowLogCloudWatch', { 39 | trafficType: FlowLogTrafficType.REJECT, 40 | maxAggregationInterval: FlowLogMaxAggregationInterval.ONE_MINUTE, 41 | }); 42 | 43 | // Security group 44 | const dbSecurityGroup: SecurityGroup = new SecurityGroup(this, 'db-security-group', { 45 | securityGroupName: 'db-security-group', 46 | description: 'db-security-group', 47 | allowAllOutbound: true, 48 | vpc: vpc, 49 | }); 50 | 51 | // DB Subnet Group 52 | const subnetIds: string[] = []; 53 | vpc.isolatedSubnets.forEach((subnet, index) => { subnetIds.push(subnet.subnetId); }); 54 | 55 | const dbSubnetGroup: CfnDBSubnetGroup = new CfnDBSubnetGroup(this, 'AuroraSubnetGroup', { 56 | dbSubnetGroupDescription: 'Subnet group to access aurora', 57 | dbSubnetGroupName: 'aurora-serverless-subnet-group', 58 | subnetIds 59 | }); 60 | 61 | // Secret Manager 62 | const secret = new sm.CfnSecret(this, "AuroraGlobalServerlessDBSecret", { 63 | name: `aurora-serverless-global-db-secret`, 64 | generateSecretString: { 65 | secretStringTemplate: JSON.stringify({ username: 'postgres' }), 66 | excludePunctuation: true, 67 | includeSpace: false, 68 | generateStringKey: "password", 69 | }, 70 | }); 71 | 72 | // Aurora DB Cluster 73 | const cfndbclusterprops = { 74 | dbClusterIdentifier: id, 75 | dbSubnetGroupName: dbSubnetGroup.dbSubnetGroupName, 76 | engine: cfnGlobalCluster.engine, 77 | engineVersion: cfnGlobalCluster.engineVersion, 78 | globalClusterIdentifier: cfnGlobalCluster.globalClusterIdentifier, 79 | ...(props.isPrimary as boolean && { databaseName: databasename }), 80 | ...(props.isPrimary as boolean && { masterUsername: new CfnDynamicReference(CfnDynamicReferenceService.SECRETS_MANAGER, 'aurora-serverless-global-db-secret:SecretString:username').toString() }), 81 | ...(props.isPrimary as boolean && { masterUserPassword: new CfnDynamicReference(CfnDynamicReferenceService.SECRETS_MANAGER, 'aurora-serverless-global-db-secret:SecretString:password').toString() }), 82 | serverlessV2ScalingConfiguration: { 83 | maxCapacity: AuroraCapacityUnit.ACU_4, 84 | minCapacity: AuroraCapacityUnit.ACU_2, 85 | }, 86 | vpcSecurityGroupIds: [dbSecurityGroup.securityGroupId], 87 | deletionProtection: true, 88 | port: 1234, 89 | enableIamDatabaseAuthentication: true, 90 | }; 91 | 92 | const dbcluster = new CfnDBCluster(this, "db-cluster", cfndbclusterprops); 93 | dbcluster.addDependency(dbSubnetGroup); 94 | dbcluster.addDependency(secret); 95 | 96 | 97 | // Aurora Serverless DB Instance 98 | const cfndbinstanceprops = { 99 | dbClusterIdentifier: dbcluster.dbClusterIdentifier, 100 | dbInstanceClass: 'db.serverless', 101 | dbInstanceIdentifier: 'serverless-db-instance', 102 | engine: cfnGlobalCluster.engine, 103 | engineVersion: cfnGlobalCluster.engineVersion, 104 | }; 105 | 106 | const dbinstance = new CfnDBInstance(this, "serverless-db-instance", cfndbinstanceprops); 107 | 108 | dbinstance.addDependency(dbcluster); 109 | 110 | this.endpoint = dbcluster.attrEndpointAddress; 111 | this.port = dbcluster.attrEndpointPort; 112 | this.vpc = vpc; 113 | this.region = Stack.of(this).region; 114 | this.dbSecurityGroupId = dbSecurityGroup.securityGroupId; 115 | } 116 | } 117 | export interface DBClusterProps extends StackProps { 118 | endpoint: string, 119 | port: string 120 | vpc: Vpc, 121 | isPrimary?: boolean, 122 | region: string, 123 | dbSecurityGroupId: string 124 | } 125 | 126 | -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/lib/fargate-test-app-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Stack, aws_iam as iam } from 'aws-cdk-lib' 3 | import { SubnetType, Port, SecurityGroup, Peer } from 'aws-cdk-lib/aws-ec2'; 4 | import { Cluster, ContainerImage, AssetImageProps } from 'aws-cdk-lib/aws-ecs'; 5 | import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns'; 6 | import { DBClusterProps } from './aurora-regional-cluster-stack'; 7 | 8 | export class FargateTestAppStack extends Stack { 9 | constructor(scope: Construct, id: string, props: DBClusterProps) { 10 | super(scope, id, props); 11 | 12 | const dbSecurityGroup = SecurityGroup.fromSecurityGroupId(this, "db-security-group", props.dbSecurityGroupId); 13 | 14 | const appSecurityGroup: SecurityGroup = new SecurityGroup(this, 'app-security-group', { 15 | securityGroupName: 'app-security-group', 16 | description: 'app-security-group', 17 | allowAllOutbound: true, 18 | vpc: props.vpc, 19 | }); 20 | 21 | dbSecurityGroup.addIngressRule( 22 | appSecurityGroup, Port.tcp(1234), 'allow port 1234 from the appSecurityGroup' 23 | ); 24 | 25 | const ingressIp = this.node.tryGetContext('ingressIpAddress') || '10.0.0.0/16'; 26 | 27 | appSecurityGroup.addIngressRule( 28 | Peer.ipv4(ingressIp), Port.tcp(80), 'allow port 80' 29 | ); 30 | // ECS Fargate Cluster 31 | const cluster = new Cluster(this, 'MyCluster', { 32 | vpc: props.vpc, 33 | containerInsights: true, 34 | }); 35 | 36 | var roleName; 37 | var policyName; 38 | 39 | if (props.isPrimary) { 40 | roleName = 'primary-test-app-task-role'; 41 | policyName = 'primary-test-app-task-policy'; 42 | } else { 43 | roleName = 'secondary-test-app-task-role'; 44 | policyName = 'secondary-test-app-task-policy'; 45 | } 46 | 47 | const taskRole = new iam.Role(this, roleName, { 48 | assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"), 49 | roleName: roleName, 50 | description: "Role that the api task definitions use to run the api code", 51 | }); 52 | 53 | taskRole.attachInlinePolicy( 54 | new iam.Policy(this, policyName, { 55 | statements: [ 56 | new iam.PolicyStatement({ 57 | effect: iam.Effect.ALLOW, 58 | actions: ["secretsmanager:GetSecretValue"], 59 | resources: ["*"], 60 | }), 61 | ], 62 | }) 63 | ); 64 | 65 | // To enable image build for both primary and secondary regions 66 | const assetImageprops: AssetImageProps = { 67 | extraHash: Stack.of(this).region, 68 | invalidation: { 69 | extraHash: true, 70 | }, 71 | } 72 | 73 | // Fargate based test app 74 | const fargate = new ApplicationLoadBalancedFargateService(this, 'Fargate', { 75 | cluster: cluster, 76 | cpu: 512, 77 | desiredCount: 1, 78 | taskImageOptions: { 79 | image: ContainerImage.fromAsset('fargate', assetImageprops), 80 | environment: { 81 | DB_HOST: props.endpoint, 82 | DB_PORT: props.port, 83 | REGION: props.region 84 | }, 85 | taskRole: taskRole 86 | }, 87 | taskSubnets: { subnetType: SubnetType.PUBLIC }, 88 | assignPublicIp: true, 89 | memoryLimitMiB: 2048, 90 | securityGroups: [appSecurityGroup], 91 | redirectHTTP: false, 92 | }); 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aurora-global-db-multistack", 3 | "version": "0.1.0", 4 | "bin": { 5 | "aurora-serverless-global-db-cdk": "bin/aurora-global-db-multistack.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.12", 15 | "@types/node": "20.14.9", 16 | "aws-cdk": "2.149.0", 17 | "jest": "^29.7.0", 18 | "ts-jest": "^29.1.2", 19 | "ts-node": "^10.9.2", 20 | "typescript": "~5.5.3" 21 | }, 22 | "dependencies": { 23 | "aws-cdk-lib": "2.149.0", 24 | "cdk-nag": "^2.28.168", 25 | "constructs": "^10.0.0", 26 | "source-map-support": "^0.5.21" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/test/global-aurora-serverless-cdk.test.ts: -------------------------------------------------------------------------------- 1 | import { Template } from 'aws-cdk-lib/assertions'; 2 | import { AuroraGlobalClusterStack, GlobalClusterProps } from '../lib/aurora-global-cluster-stack'; 3 | import { AuroraRegionalClusterStack } from '../lib/aurora-regional-cluster-stack' 4 | import { FargateTestAppStack } from '../lib/fargate-test-app-stack'; 5 | import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag' 6 | 7 | import { Annotations, Match } from 'aws-cdk-lib/assertions'; 8 | import { App, Aspects } from 'aws-cdk-lib'; 9 | 10 | 11 | test('Validate Stack Resources', () => { 12 | const app = new App(); 13 | 14 | const account = app.node.tryGetContext('account') || process.env.CDK_INTEG_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT; 15 | const primaryRegion = { account: account, region: 'eu-west-1' }; 16 | const secondaryRegion = { account: account, region: 'eu-west-2' }; 17 | 18 | const globalclusterstack = new AuroraGlobalClusterStack(app, "AuroraGlobalCluster", { 19 | env: primaryRegion, 20 | }); 21 | 22 | const primaryregionstack = new AuroraRegionalClusterStack(app, `AuroraPrimaryCluster-${primaryRegion.region}`, { 23 | env: primaryRegion, cfnGlobalCluster: globalclusterstack.cfnGlobalCluster, isPrimary: true 24 | }); 25 | 26 | const secondaryregionstack = new AuroraRegionalClusterStack(app, `AuroraSecondaryCluster-${secondaryRegion.region}`, { 27 | env: secondaryRegion, cfnGlobalCluster: globalclusterstack.cfnGlobalCluster, isPrimary: false 28 | }); 29 | 30 | const globalclustertemplate = Template.fromStack(globalclusterstack); 31 | const primarytemplate = Template.fromStack(primaryregionstack); 32 | const secondarytemplate = Template.fromStack(secondaryregionstack); 33 | 34 | globalclustertemplate.resourceCountIs('AWS::RDS::GlobalCluster', 1); 35 | 36 | primarytemplate.resourceCountIs('AWS::EC2::VPC', 1); 37 | primarytemplate.resourceCountIs('AWS::RDS::DBSubnetGroup', 1); 38 | primarytemplate.resourceCountIs('AWS::SecretsManager::Secret', 1); 39 | primarytemplate.resourceCountIs('AWS::RDS::DBCluster', 1); 40 | primarytemplate.resourceCountIs('AWS::RDS::DBInstance', 1); 41 | 42 | secondarytemplate.resourceCountIs('AWS::EC2::VPC', 1); 43 | secondarytemplate.resourceCountIs('AWS::RDS::DBSubnetGroup', 1); 44 | secondarytemplate.resourceCountIs('AWS::SecretsManager::Secret', 1); 45 | secondarytemplate.resourceCountIs('AWS::RDS::DBCluster', 1); 46 | secondarytemplate.resourceCountIs('AWS::RDS::DBInstance', 1); 47 | }); 48 | 49 | describe('cdk-nag AwsSolutions Pack', () => { 50 | let globalclusterstack: AuroraGlobalClusterStack; 51 | let primaryclusterstack: AuroraRegionalClusterStack; 52 | let primarytestappstack: FargateTestAppStack; 53 | let app: App; 54 | 55 | 56 | // In this case we can use beforeAll() over beforeEach() since our tests 57 | // do not modify the state of the application 58 | beforeAll(() => { 59 | // GIVEN 60 | app = new App(); 61 | 62 | const account = app.node.tryGetContext('account') || process.env.CDK_INTEG_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT; 63 | const primaryRegion = { account: account, region: 'eu-west-1' }; 64 | 65 | globalclusterstack = new AuroraGlobalClusterStack(app, "AuroraGlobalCluster", { 66 | env: primaryRegion, 67 | crossRegionReferences: true 68 | }); 69 | 70 | primaryclusterstack = new AuroraRegionalClusterStack(app, `AuroraPrimaryCluster-${primaryRegion.region}`, { 71 | env: primaryRegion, cfnGlobalCluster: globalclusterstack.cfnGlobalCluster, isPrimary: true 72 | }); 73 | 74 | primarytestappstack = new FargateTestAppStack(app, `primary-test-app`, { 75 | env: primaryRegion, 76 | endpoint: primaryclusterstack.endpoint, 77 | port: primaryclusterstack.port, 78 | vpc: primaryclusterstack.vpc, 79 | isPrimary: true, 80 | region: primaryclusterstack.region, 81 | dbSecurityGroupId: primaryclusterstack.dbSecurityGroupId 82 | }); 83 | 84 | // WHEN 85 | Aspects.of(primaryclusterstack).add(new AwsSolutionsChecks()); 86 | NagSuppressions.addStackSuppressions(primaryclusterstack, [ 87 | { id: 'AwsSolutions-SMG4', reason: '##Rule : The secret does not have automatic rotation scheduled. ##Suppress Reason## : CfnSecret does not support adding rotation rules and since this is a sample app. For production enable rotation.' }, 88 | { id: 'AwsSolutions-RDS2', reason: '##Rule : The RDS instance or Aurora DB cluster does not have storage encryption enabled. ##Suppress Reason## : This is sample app. For production enable encryption' } 89 | ]); 90 | 91 | Aspects.of(primarytestappstack).add(new AwsSolutionsChecks()); 92 | NagSuppressions.addStackSuppressions(primarytestappstack, [ 93 | { id: 'AwsSolutions-ECS2', reason: '##Rule : The ECS Task Definition includes a container definition that directly specifies environment variables. ##Suppress Reason## : This is a demo app and hence using env variable. For production use secrets manager.' }, 94 | { id: 'AwsSolutions-ELB2', reason: '##Rule : The ELB does not have access logs enabled. ##Suppress Reason## : The ALB for sample app is created implicitly using ApplicationLoadBalancedFargateService class and hence could not enable . For production use consider enabling access logs.' }, 95 | { id: 'AwsSolutions-EC23', reason: '##Rule : The Security Group allows for 0.0.0.0/0 or ::/0 inbound access. ##Suppress Reason## : To enable testing the API allowing ingress route on port 80 from public.' }, 96 | { id: 'AwsSolutions-IAM5', reason: '##Rule : The IAM entity contains wildcard permissions. ##Suppress Reason## : The TaskRole is added when the task is created, Hence unable to use its ARN as this will cause a loop. For production do not use wild cards and create the task role separately and add' } 97 | ]); 98 | }); 99 | 100 | // THEN 101 | test('primaryclusterstack : No unsuppressed Warnings', () => { 102 | const warnings = Annotations.fromStack(primaryclusterstack).findWarning( 103 | '*', 104 | Match.stringLikeRegexp('AwsSolutions-.*') 105 | ); 106 | expect(warnings).toHaveLength(0); 107 | }); 108 | 109 | test('primaryclusterstack : No unsuppressed Errors', () => { 110 | const errors = Annotations.fromStack(primaryclusterstack).findError( 111 | '*', 112 | Match.stringLikeRegexp('AwsSolutions-.*') 113 | ); 114 | expect(errors).toHaveLength(0); 115 | }); 116 | 117 | test('primarytestappstack : No unsuppressed Warnings', () => { 118 | const warnings = Annotations.fromStack(primarytestappstack).findWarning( 119 | '*', 120 | Match.stringLikeRegexp('AwsSolutions-.*') 121 | ); 122 | expect(warnings).toHaveLength(0); 123 | }); 124 | 125 | test('primarytestappstack : No unsuppressed Errors', () => { 126 | const errors = Annotations.fromStack(primarytestappstack).findError( 127 | '*', 128 | Match.stringLikeRegexp('AwsSolutions-.*') 129 | ); 130 | expect(errors).toHaveLength(0); 131 | }); 132 | }); -------------------------------------------------------------------------------- /samples/aurora-serverless-global-db-cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/aws-managed/1_build_vector_db_on_aurora.sql: -------------------------------------------------------------------------------- 1 | --Step 1 : Enable the pgvector extension (You will need rds_superuser privilege) 2 | 3 | CREATE EXTENSION IF NOT EXISTS vector; 4 | 5 | --Step 2: Verify the version of the pgvector extension 6 | 7 | SELECT extversion FROM pg_extension WHERE extname='vector'; 8 | 9 | --Step 3 : Create a schema and grant permissions (You will need database owner privilege) 10 | 11 | CREATE SCHEMA aws_managed; 12 | CREATE ROLE bedrock_user WITH PASSWORD '' LOGIN; 13 | GRANT ALL ON SCHEMA aws_managed to bedrock_user; 14 | 15 | --Step 4 : Create the Vector table 16 | 17 | CREATE TABLE aws_managed.kb (id uuid PRIMARY KEY, embedding vector(1536), chunks text, metadata jsonb, tenantid bigint); 18 | GRANT ALL ON TABLE aws_managed.kb to bedrock_user; 19 | 20 | --Step 5 : Create the Index 21 | 22 | CREATE INDEX on aws_managed.kb USING hnsw (embedding vector_cosine_ops); 23 | 24 | -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/aws-managed/policy-templates/bedrock_aurora_cluster_permissions_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "RdsDescribeStatementID", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "rds:DescribeDBClusters" 9 | ], 10 | "Resource": [ 11 | "#rds_aurora_cluster_arn#" 12 | ] 13 | }, 14 | { 15 | "Sid": "DataAPIStatementID", 16 | "Effect": "Allow", 17 | "Action": [ 18 | "rds-data:BatchExecuteStatement", 19 | "rds-data:ExecuteStatement" 20 | ], 21 | "Resource": [ 22 | "#rds_aurora_cluster_arn#" 23 | ] 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/aws-managed/policy-templates/bedrock_data_source_permissions_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [{ 4 | "Effect": "Allow", 5 | "Action": [ 6 | "s3:GetObject", 7 | "s3:ListBucket" 8 | ], 9 | "Resource": [ 10 | "arn:aws:s3:::#bucket_name#", 11 | "arn:aws:s3:::#bucket_name#/*" 12 | ], 13 | "Condition": { 14 | "StringEquals": { 15 | "aws:PrincipalAccount": "#account_id#" 16 | } 17 | } 18 | }] 19 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/aws-managed/policy-templates/bedrock_model_permissions_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "bedrock:ListFoundationModels", 8 | "bedrock:ListCustomModels" 9 | ], 10 | "Resource": "*" 11 | }, 12 | { 13 | "Effect": "Allow", 14 | "Action": [ 15 | "bedrock:InvokeModel" 16 | ], 17 | "Resource": [ 18 | "arn:aws:bedrock:#region_name#::foundation-model/#embedding_model_id#" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/aws-managed/policy-templates/bedrock_secrets_permissions_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "SecretsManagerGetStatement", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "secretsmanager:GetSecretValue" 9 | ], 10 | "Resource": [ 11 | "#bedrock_user_secret_arn#" 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/aws-managed/policy-templates/bedrock_trust_relationship_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [{ 4 | "Effect": "Allow", 5 | "Principal": { 6 | "Service": "bedrock.amazonaws.com" 7 | }, 8 | "Action": "sts:AssumeRole", 9 | "Condition": { 10 | "StringEquals": { 11 | "aws:SourceAccount": "#account_id#" 12 | }, 13 | "ArnLike": { 14 | "AWS:SourceArn": "arn:aws:bedrock:#region_name#:#account_id#:knowledge-base/*" 15 | } 16 | } 17 | }] 18 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | .DS_Store 6 | 7 | # CDK asset staging directory 8 | .cdk.staging 9 | cdk.out 10 | -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/cdk/README.md: -------------------------------------------------------------------------------- 1 | # Deploy Amazon Aurora using CDK 2 | 3 | Amazon Aurora PostgreSQL is one of the prerequisite for running the sample notebooks. You can make use of this CDK to provision an Aurora PostgreSQL cluster. 4 | 5 | 6 | ## Requirements 7 | 8 | * [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. 9 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured 10 | * [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 11 | * [Node and NPM](https://nodejs.org/en/download/) installed. 12 | * [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed and configured 13 | 14 | ## Deployment Instructions 15 | 16 | 1. Clone the GitHub repository: 17 | ``` 18 | git clone https://github.com/aws-samples/data-for-saas-patterns.git 19 | ``` 20 | 2. Change directory to the cdk project : 21 | ``` 22 | cd data-for-saas-patterns/samples/multi-tenant-vector-database/amazon-aurora/cdk 23 | ``` 24 | 3. Install dependencies: 25 | ``` 26 | npm install 27 | ``` 28 | 4. Configure AWS CDK to bootstrap the AWS account: 29 | ``` 30 | cdk bootstrap / 31 | ``` 32 | 5. From the command line, use AWS CDK to deploy the stack: 33 | ``` 34 | cdk deploy 35 | ``` 36 | 37 | 6. Note the outputs from the CDK deployment process. These contain resources ARNs needed for the notebooks. 38 | 39 | 7. Finally after testing all the samples, you can remove the Aurora PostgreSQL cluster using the destroy command. 40 | ``` 41 | cdk destroy 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/cdk/bin/cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { AuroraCdkStack } from '../lib/aurora-cdk-stack'; 5 | 6 | const app = new cdk.App(); 7 | const aurora_cdk_stack = new AuroraCdkStack(app, 'AuroraCdkStack', {}); -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 59 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 60 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 61 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 62 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 63 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 64 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 65 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 66 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 67 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 68 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, 69 | "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, 70 | "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, 71 | "@aws-cdk/aws-stepfunctions-tasks:ecsReduceRunTaskPermissions": true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/cdk/lib/aurora-cdk-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { Duration } from 'aws-cdk-lib' 4 | import { SubnetType, Vpc, SecurityGroup, IpAddresses } from 'aws-cdk-lib/aws-ec2'; 5 | import { FlowLogMaxAggregationInterval, FlowLogTrafficType } from 'aws-cdk-lib/aws-ec2'; 6 | import { AuroraCapacityUnit, CfnDBSubnetGroup } from 'aws-cdk-lib/aws-rds'; 7 | import { DatabaseCluster, DatabaseClusterEngine, AuroraPostgresEngineVersion, ClusterInstance } from 'aws-cdk-lib/aws-rds'; 8 | export class AuroraCdkStack extends cdk.Stack { 9 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 10 | super(scope, id, props); 11 | 12 | const databasename = 'postgres'; 13 | 14 | // VPC 15 | const vpc = new Vpc(this, 'Vpc', { 16 | ipAddresses: IpAddresses.cidr('10.0.0.0/16'), 17 | natGateways: 0, 18 | subnetConfiguration: [ 19 | { 20 | cidrMask: 24, 21 | name: 'aurora_isolated_', 22 | subnetType: SubnetType.PRIVATE_ISOLATED 23 | }, 24 | { 25 | cidrMask: 24, 26 | name: 'public', 27 | subnetType: SubnetType.PUBLIC 28 | } 29 | ] 30 | }); 31 | 32 | vpc.addFlowLog('FlowLogCloudWatch', { 33 | trafficType: FlowLogTrafficType.REJECT, 34 | maxAggregationInterval: FlowLogMaxAggregationInterval.ONE_MINUTE, 35 | }); 36 | 37 | // Security group 38 | const dbSecurityGroup: SecurityGroup = new SecurityGroup(this, 'db-security-group', { 39 | securityGroupName: 'db-security-group', 40 | description: 'db-security-group', 41 | allowAllOutbound: true, 42 | vpc: vpc, 43 | }); 44 | 45 | // DB Subnet Group 46 | const subnetIds: string[] = []; 47 | vpc.isolatedSubnets.forEach((subnet, index) => { subnetIds.push(subnet.subnetId); }); 48 | 49 | const dbSubnetGroup: CfnDBSubnetGroup = new CfnDBSubnetGroup(this, 'AuroraSubnetGroup', { 50 | dbSubnetGroupDescription: 'Subnet group to access aurora', 51 | dbSubnetGroupName: 'aurora-serverless-subnet-group', 52 | subnetIds 53 | }); 54 | 55 | const dbCluster = new DatabaseCluster(this, 'DbCluster', { 56 | engine: DatabaseClusterEngine.auroraPostgres({ 57 | version: AuroraPostgresEngineVersion.VER_16_2, 58 | }), 59 | iamAuthentication: true, 60 | storageEncrypted: true, 61 | deletionProtection: true, 62 | writer: ClusterInstance.serverlessV2('writer', { 63 | }), 64 | vpc: vpc, 65 | securityGroups: [dbSecurityGroup], 66 | vpcSubnets: vpc.selectSubnets({ 67 | subnetType: SubnetType.PRIVATE_ISOLATED, 68 | }), 69 | serverlessV2MaxCapacity: AuroraCapacityUnit.ACU_4, 70 | serverlessV2MinCapacity: AuroraCapacityUnit.ACU_2, 71 | port: 5432, // use port 5432 instead of 3306 72 | enableDataApi: true 73 | }) 74 | 75 | dbCluster.addRotationSingleUser({ 76 | automaticallyAfter: Duration.days(30), 77 | excludeCharacters: ' %\'@', 78 | vpcSubnets: vpc.selectSubnets({ 79 | subnetType: SubnetType.PRIVATE_ISOLATED, 80 | }), 81 | }) 82 | 83 | new cdk.CfnOutput(this, 'Aurora Cluster ARN', { value: dbCluster.clusterArn }); 84 | 85 | } 86 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cdk": "bin/cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.12", 15 | "@types/node": "20.14.9", 16 | "aws-cdk": "2.149.0", 17 | "jest": "^29.7.0", 18 | "ts-jest": "^29.1.5", 19 | "ts-node": "^10.9.2", 20 | "typescript": "~5.5.3" 21 | }, 22 | "dependencies": { 23 | "aws-cdk-lib": "2.149.0", 24 | "cdk-nag": "^2.28.168", 25 | "constructs": "^10.0.0", 26 | "source-map-support": "^0.5.21" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/cdk/test/cdk.test.ts: -------------------------------------------------------------------------------- 1 | import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag' 2 | import { AuroraCdkStack } from '../lib/aurora-cdk-stack'; 3 | import { Annotations, Match } from 'aws-cdk-lib/assertions'; 4 | import { App, Aspects, Stack } from 'aws-cdk-lib'; 5 | 6 | describe('cdk-nag AwsSolutions Pack', () => { 7 | let stack: Stack; 8 | let app: App; 9 | // In this case we can use beforeAll() over beforeEach() since our tests 10 | // do not modify the state of the application 11 | beforeAll(() => { 12 | // GIVEN 13 | app = new App(); 14 | stack = new AuroraCdkStack(app, 'test'); 15 | 16 | // WHEN 17 | Aspects.of(stack).add(new AwsSolutionsChecks()); 18 | NagSuppressions.addStackSuppressions(stack, [ 19 | { id: 'AwsSolutions-RDS11', reason: 'Default endpoint port: This is a sample application and hence suppressing this error.' } 20 | ]); 21 | }); 22 | 23 | // THEN 24 | test('No unsuppressed Warnings', () => { 25 | const warnings = Annotations.fromStack(stack).findWarning( 26 | '*', 27 | Match.stringLikeRegexp('AwsSolutions-.*') 28 | ); 29 | expect(warnings).toHaveLength(0); 30 | }); 31 | 32 | test('No unsuppressed Errors', () => { 33 | const errors = Annotations.fromStack(stack).findError( 34 | '*', 35 | Match.stringLikeRegexp('AwsSolutions-.*') 36 | ); 37 | expect(errors).toHaveLength(0); 38 | }); 39 | }); 40 | 41 | 42 | -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/metadata_tags/Home_Survey_Tenant1.pdf.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadataAttributes": { 3 | "tenantid": 1 4 | } 5 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/metadata_tags/Home_Survey_Tenant2.pdf.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadataAttributes": { 3 | "tenantid": 2 4 | } 5 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/metadata_tags/Home_Survey_Tenant3.pdf.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadataAttributes": { 3 | "tenantid": 3 4 | } 5 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/metadata_tags/Home_Survey_Tenant4.pdf.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadataAttributes": { 3 | "tenantid": 4 4 | } 5 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/metadata_tags/Home_Survey_Tenant5.pdf.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadataAttributes": { 3 | "tenantid": 5 4 | } 5 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/multi_tenant_survey_reports/Home_Survey_Tenant1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/samples/multi-tenant-vector-database/amazon-aurora/multi_tenant_survey_reports/Home_Survey_Tenant1.pdf -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/multi_tenant_survey_reports/Home_Survey_Tenant2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/samples/multi-tenant-vector-database/amazon-aurora/multi_tenant_survey_reports/Home_Survey_Tenant2.pdf -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/multi_tenant_survey_reports/Home_Survey_Tenant3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/samples/multi-tenant-vector-database/amazon-aurora/multi_tenant_survey_reports/Home_Survey_Tenant3.pdf -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/multi_tenant_survey_reports/Home_Survey_Tenant4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/samples/multi-tenant-vector-database/amazon-aurora/multi_tenant_survey_reports/Home_Survey_Tenant4.pdf -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/multi_tenant_survey_reports/Home_Survey_Tenant5.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/samples/multi-tenant-vector-database/amazon-aurora/multi_tenant_survey_reports/Home_Survey_Tenant5.pdf -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-aurora/self-managed/1_build_vector_db_on_aurora.sql: -------------------------------------------------------------------------------- 1 | --Step 1 : Enable the pgvector extension (You will need rds_superuser privilege) 2 | 3 | CREATE EXTENSION IF NOT EXISTS vector; 4 | 5 | --Step 2: Verify the version of the pgvector extension 6 | 7 | SELECT extversion FROM pg_extension WHERE extname='vector'; 8 | 9 | --Step 3 : Create a schema (You will need database owner privilege) 10 | CREATE SCHEMA self_managed; 11 | 12 | --Step 4 : Create the Vector table 13 | 14 | CREATE TABLE self_managed.kb (id uuid PRIMARY KEY, embedding vector(1536), chunks text, metadata jsonb, tenantid bigint); 15 | 16 | --Step 5 : Create the Index 17 | 18 | CREATE INDEX on self_managed.kb USING hnsw (embedding vector_cosine_ops); 19 | 20 | -- Step 6 : Enable Row Level Security 21 | CREATE POLICY tenant_policy ON self_managed.kb USING (tenantid = current_setting('self_managed.kb.tenantid')::bigint); 22 | 23 | ALTER TABLE self_managed.kb enable row level security; 24 | 25 | CREATE ROLE app_user WITH PASSWORD '' LOGIN; 26 | 27 | GRANT ALL ON SCHEMA self_managed to app_user; 28 | GRANT SELECT ON TABLE self_managed.kb to app_user; 29 | 30 | -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/README.md: -------------------------------------------------------------------------------- 1 | # Multi-tenant vector store using Amazon OpenSearch 2 | 3 | Using the vector data store capabilities of Amazon OpenSearch, you can implement semantic search, Retrieval Augmented Generation (RAG) with LLMs, recommendation engines, and search rich media. This sample explores the process of building a multi-tenant vector store using Amazon OpenSearch, describing the necessary steps. We present a self-managed approach to build the vector data store using Amazon OpenSearch domain and also the fully-managed approach using OpenSearch Serverless collection and Amazon Bedrock Knowledge Bases to simplify the integration of the data-sources, vector data store, and your generative AI application. 4 | 5 | ## Self-Managed Reference Architecture 6 | 7 | ![arch diagram](./images/os_self-managed_arch.jpg) 8 | 9 | ## Fully-Managed reference Architecture 10 | 11 | ![arch diagram](./images/os_fully_managed_arch.jpg) 12 | 13 | 14 | 15 | ## Requirements 16 | 17 | * [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. 18 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured 19 | * [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 20 | 21 | ## Deployment Instructions 22 | 23 | 1. Clone the GitHub repository: 24 | ``` 25 | git clone https://github.com/aws-samples/data-for-saas-patterns.git 26 | ``` 27 | 2. Change directory to the CloudFormation template : 28 | ``` 29 | cd data-for-saas-patterns/samples/multi-tenant-vector-database/amazon-opensearch/cfn 30 | ``` 31 | 3. Deploy the CloudFormation stack: 32 | ``` 33 | aws cloudformation create-stack --stack-name opensearch-stack --template-body file://opensearch-template.yaml --capabilities CAPABILITY_NAMED_IAM --region us-west-2 ParameterKey=KeyPair,ParameterValue=default-keypair 34 | ``` 35 | 36 | 4. Note the outputs from the CloudFormation stack. These contain resources ARNs and endpoints needed for the notebooks. 37 | 38 | 5. Review the self-managed approach to build the multi-tenant vector store by using the Jupyter notebook - [opensearch_self_managed_notebook.ipynb](self-managed/opensearch_self_managed_notebook.ipynb) 39 | ``` 40 | cd data-for-saas-patterns/samples/multi-tenant-vector-database/amazon-opensearch/self-managed 41 | ``` 42 | 6. Review the fully-managed approach to build the multi-tenant vector store using the Jupyter notebook - [opensearch_fully_managed_notebook.ipynb](fully-managed/opensearch_fully_managed_notebook.ipynb) 43 | ``` 44 | cd data-for-saas-patterns/samples/multi-tenant-vector-database/amazon-opensearch/fully-managed 45 | ``` 46 | -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/fully-managed/policy-templates/bedrock_data_source_permissions_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [{ 4 | "Effect": "Allow", 5 | "Action": [ 6 | "s3:GetObject", 7 | "s3:ListBucket" 8 | ], 9 | "Resource": [ 10 | "arn:aws:s3:::#bucket_name#", 11 | "arn:aws:s3:::#bucket_name#/*" 12 | ], 13 | "Condition": { 14 | "StringEquals": { 15 | "aws:PrincipalAccount": "#account_id#" 16 | } 17 | } 18 | }] 19 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/fully-managed/policy-templates/bedrock_model_permissions_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "bedrock:ListFoundationModels", 8 | "bedrock:ListCustomModels" 9 | ], 10 | "Resource": "*" 11 | }, 12 | { 13 | "Effect": "Allow", 14 | "Action": [ 15 | "bedrock:InvokeModel" 16 | ], 17 | "Resource": [ 18 | "arn:aws:bedrock:#region_name#::foundation-model/#embedding_model_id#" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/fully-managed/policy-templates/bedrock_opensearch_collection_permissions_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "OpenSearchCollectionStatementID", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "aoss:DescribeCollectionItems", 9 | "aoss:CreateCollectionItems", 10 | "aoss:UpdateCollectionItems", 11 | "aoss:APIAccessAll" 12 | ], 13 | "Resource": [ 14 | "#opensearch_collection_arn#" 15 | ] 16 | }, 17 | { 18 | "Sid": "OpenSearchIndexStatementID", 19 | "Effect": "Allow", 20 | "Action": [ 21 | "aoss:UpdateIndex", 22 | "aoss:DescribeIndex", 23 | "aoss:ReadDocument", 24 | "aoss:WriteDocument", 25 | "aoss:CreateIndex" 26 | ], 27 | "Resource": [ 28 | "#opensearch_collection_arn#" 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/fully-managed/policy-templates/bedrock_trust_relationship_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [{ 4 | "Effect": "Allow", 5 | "Principal": { 6 | "Service": "bedrock.amazonaws.com" 7 | }, 8 | "Action": "sts:AssumeRole", 9 | "Condition": { 10 | "StringEquals": { 11 | "aws:SourceAccount": "#account_id#" 12 | }, 13 | "ArnLike": { 14 | "AWS:SourceArn": "arn:aws:bedrock:#region_name#:#account_id#:knowledge-base/*" 15 | } 16 | } 17 | }] 18 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/images/os_fully_managed_arch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/samples/multi-tenant-vector-database/amazon-opensearch/images/os_fully_managed_arch.jpg -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/images/os_self-managed_arch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/samples/multi-tenant-vector-database/amazon-opensearch/images/os_self-managed_arch.jpg -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/metadata_tags/Home_Survey_Tenant1.pdf.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadataAttributes": { 3 | "tenantid": "Tenant1" 4 | } 5 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/metadata_tags/Home_Survey_Tenant2.pdf.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadataAttributes" : { 3 | "tenantid" : "Tenant2" 4 | } 5 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/metadata_tags/Home_Survey_Tenant3.pdf.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadataAttributes" : { 3 | "tenantid" : "Tenant3" 4 | } 5 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/metadata_tags/Home_Survey_Tenant4.pdf.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadataAttributes" : { 3 | "tenantid" : "Tenant4" 4 | } 5 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/metadata_tags/Home_Survey_Tenant5.pdf.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadataAttributes" : { 3 | "tenantid" : "Tenant5" 4 | } 5 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/multi_tenant_survey_reports/Home_Survey_Tenant1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/samples/multi-tenant-vector-database/amazon-opensearch/multi_tenant_survey_reports/Home_Survey_Tenant1.pdf -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/multi_tenant_survey_reports/Home_Survey_Tenant2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/samples/multi-tenant-vector-database/amazon-opensearch/multi_tenant_survey_reports/Home_Survey_Tenant2.pdf -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/multi_tenant_survey_reports/Home_Survey_Tenant3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/samples/multi-tenant-vector-database/amazon-opensearch/multi_tenant_survey_reports/Home_Survey_Tenant3.pdf -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/multi_tenant_survey_reports/Home_Survey_Tenant4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/samples/multi-tenant-vector-database/amazon-opensearch/multi_tenant_survey_reports/Home_Survey_Tenant4.pdf -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/multi_tenant_survey_reports/Home_Survey_Tenant5.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/data-for-saas-patterns/12d86ba9232f00bc0c939515bb5642fb48941222/samples/multi-tenant-vector-database/amazon-opensearch/multi_tenant_survey_reports/Home_Survey_Tenant5.pdf -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/self-managed/policy-templates/bedrock_model_permissions_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "bedrock:ListFoundationModels", 8 | "bedrock:ListCustomModels" 9 | ], 10 | "Resource": "*" 11 | }, 12 | { 13 | "Effect": "Allow", 14 | "Action": [ 15 | "bedrock:InvokeModel" 16 | ], 17 | "Resource": [ 18 | "arn:aws:bedrock:#region_name#::foundation-model/#embedding_model_id#" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/self-managed/policy-templates/bedrock_trust_relationship_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Service": "opensearchservice.amazonaws.com" 8 | }, 9 | "Action": "sts:AssumeRole", 10 | "Condition": { 11 | "StringEquals": { 12 | "aws:SourceAccount": "#account_id#" 13 | } 14 | } 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/self-managed/policy-templates/secretsmanager_permissions_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": [ 6 | "secretsmanager:GetSecretValue" 7 | ], 8 | "Effect": "Allow", 9 | "Resource": "*", 10 | "Condition": { 11 | "StringEquals": { 12 | "aws:SourceAccount": "#account_id#" 13 | } 14 | } 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /samples/multi-tenant-vector-database/amazon-opensearch/self-managed/policy-templates/secretsmanager_trust_relationship_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": [ 6 | "sts:AssumeRole" 7 | ], 8 | "Effect": "Allow", 9 | "Principal": { 10 | "Service": [ 11 | "opensearchservice.amazonaws.com" 12 | ] 13 | }, 14 | "Condition": { 15 | "StringEquals": { 16 | "aws:SourceAccount": "#account_id#" 17 | } 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /samples/rds-data-api-rls/rds-data-api-rls-function.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# RDS Data API - Row-level security using PostgreSQL function\n", 8 | "\n", 9 | "To run this workbook, you must have an Amazon Aurora PostgreSQL cluster with the RDS Data API enabled. Update the cell below with the Cluster ARN, the Secrets Manager secret ARNs for the database credentials to use and the AWS Region to create the boto3 client in.\n", 10 | "\n", 11 | "Note that the app user should not be the RDS master user as the row-level security policy will not apply to a super user. Instead create a dedicated application user to use with limited permissions." 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "%pip install boto3" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "import boto3\n", 30 | "\n", 31 | "cluster_arn = ''\n", 32 | "admin_secret_arn = ''\n", 33 | "app_user_secret_arn = ''\n", 34 | "rdsData = boto3.client('rds-data', region_name='eu-central-1')\n", 35 | "db_name = 'postgres'" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "The below cell creates a simple multi-tenant database schema, isolated using a row-level security policy:" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "response = rdsData.execute_statement(resourceArn=cluster_arn,\n", 52 | " secretArn=admin_secret_arn,\n", 53 | " database=db_name,\n", 54 | " sql='CREATE TABLE tenant ( tenant_id integer PRIMARY KEY, tenant_name text, account_balance numeric );')\n", 55 | "response = rdsData.execute_statement(resourceArn=cluster_arn,\n", 56 | " secretArn=admin_secret_arn,\n", 57 | " database=db_name,\n", 58 | " sql='''INSERT INTO tenant VALUES (1, 'Tenant1', 50000), (2, 'Tenant2', 60000), (3, 'Tenant3', 40000);''')\n", 59 | "response = rdsData.execute_statement(resourceArn=cluster_arn,\n", 60 | " secretArn=admin_secret_arn,\n", 61 | " database=db_name,\n", 62 | " sql='''CREATE POLICY tenant_policy ON tenant USING (tenant_id = current_setting('tenant.id')::integer);''')\n", 63 | "response = rdsData.execute_statement(resourceArn=cluster_arn,\n", 64 | " secretArn=admin_secret_arn,\n", 65 | " database=db_name,\n", 66 | " sql='ALTER TABLE tenant enable row level security;')" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "Next a function is created that encapsulates setting a session variable to enforce row-level security:" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "response = rdsData.execute_statement(resourceArn=cluster_arn,\n", 83 | " secretArn=admin_secret_arn,\n", 84 | " database=db_name,\n", 85 | " sql='''\n", 86 | " CREATE OR REPLACE FUNCTION get_tenant_data(p_tenant_id integer) \n", 87 | " RETURNS SETOF text AS\n", 88 | " $func$\n", 89 | " BEGIN\n", 90 | " EXECUTE format('SET \"tenant.id\" = %s', p_tenant_id);\n", 91 | " RETURN QUERY\n", 92 | " SELECT tenant_name\n", 93 | " FROM tenant;\n", 94 | " END\n", 95 | " $func$ LANGUAGE plpgsql;\n", 96 | "''')" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "Finally, the below code can be used to call the wrapping function from the application data access layer. The tenantId is pulled from the JWT as part of the tenant context. In this case it is hardcoded to \"2\", in a real-world application this would have logic to parse the tenantId from a claim in the token. \n", 104 | "\n", 105 | "The tenantId is then passed to the function to use as a set variable for row-level security. " 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": null, 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [ 114 | "def get_tenant_id_from_context():\n", 115 | " return 2;\n", 116 | "\n", 117 | "param1 = {'name':'id', 'value':{'longValue': get_tenant_id_from_context()}}\n", 118 | "paramSet = [param1]\n", 119 | "\n", 120 | "response = rdsData.execute_statement(resourceArn=cluster_arn,\n", 121 | " secretArn=app_user_secret_arn,\n", 122 | " database=db_name,\n", 123 | " sql='select get_tenant_data(:id::integer)',\n", 124 | " parameters = paramSet)\n", 125 | "\n", 126 | "print(response['records'])" 127 | ] 128 | } 129 | ], 130 | "metadata": { 131 | "kernelspec": { 132 | "display_name": "Python 3", 133 | "language": "python", 134 | "name": "python3" 135 | }, 136 | "language_info": { 137 | "codemirror_mode": { 138 | "name": "ipython", 139 | "version": 3 140 | }, 141 | "file_extension": ".py", 142 | "mimetype": "text/x-python", 143 | "name": "python", 144 | "nbconvert_exporter": "python", 145 | "pygments_lexer": "ipython3", 146 | "version": "3.9.6" 147 | } 148 | }, 149 | "nbformat": 4, 150 | "nbformat_minor": 2 151 | } 152 | -------------------------------------------------------------------------------- /samples/rds-data-api-rls/rds-data-api-rls-function.py: -------------------------------------------------------------------------------- 1 | """ 2 | CREATE TABLE tenant ( tenant_id integer PRIMARY KEY, tenant_name text, account_balance numeric ); 3 | INSERT INTO tenant VALUES (1, 'Tenant1', 50000), (2, 'Tenant2', 60000), (3, 'Tenant3', 40000); 4 | CREATE POLICY tenant_policy ON tenant USING (tenant_id = current_setting('tenant.id')::integer); 5 | ALTER TABLE tenant enable row level security; 6 | """ 7 | import boto3 8 | 9 | cluster_arn = '/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /samples/scheduled-aurora-serverless-scaling/lib/scheduled-autoscaling-amazon-aurora-serverless-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { CfnSchedule } from 'aws-cdk-lib/aws-scheduler'; 3 | import { Role, PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; 4 | import { Construct } from 'constructs'; 5 | 6 | export class ScheduledAutoscalingAmazonAuroraServerlessStack extends cdk.Stack { 7 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 8 | super(scope, id, props); 9 | 10 | const scheduleUp = this.node.tryGetContext('scheduleUp'); 11 | const desiredCapacityUpMin: number = this.node.tryGetContext('desiredCapacityUpMin'); 12 | const desiredCapacityUpMax: number = this.node.tryGetContext('desiredCapacityUpMax'); 13 | const scheduleDown = this.node.tryGetContext('scheduleDown'); 14 | const desiredCapacityDownMin: number = this.node.tryGetContext('desiredCapacityDownMin'); 15 | const desiredCapacityDownMax: number = this.node.tryGetContext('desiredCapacityDownMax'); 16 | const dbClusterIds: string[] = this.node.tryGetContext('dbClusterIds'); 17 | 18 | const schedulerRole = new Role(this, 'SchedulerRole', { 19 | assumedBy: new ServicePrincipal('scheduler.amazonaws.com') 20 | }); 21 | 22 | for (const dbClusterId of dbClusterIds) { 23 | schedulerRole.addToPolicy(new PolicyStatement({ 24 | actions: ['rds:ModifyDBCluster'], 25 | resources: [`arn:aws:rds:${this.region}:${this.account}:cluster:${dbClusterId}`] 26 | })); 27 | 28 | if (scheduleUp && desiredCapacityUpMin && desiredCapacityUpMax) { 29 | new CfnSchedule(this, `ScaleUpSchedule-${dbClusterId}`, { 30 | flexibleTimeWindow: { 31 | mode: 'OFF' 32 | }, 33 | scheduleExpression: scheduleUp, 34 | target: { 35 | arn: `arn:aws:scheduler:::aws-sdk:rds:modifyDBCluster`, 36 | roleArn: schedulerRole.roleArn, 37 | input: `{"DbClusterIdentifier":"${dbClusterId}","ServerlessV2ScalingConfiguration":{"MinCapacity":${desiredCapacityUpMin},"MaxCapacity":${desiredCapacityUpMax}}}`, 38 | retryPolicy: { 39 | maximumRetryAttempts: 3 40 | } 41 | }, 42 | name: `scale-up-${dbClusterId}`, 43 | description: 'Scale up Aurora Serverless cluster during business hours', 44 | state: 'ENABLED' 45 | }); 46 | } 47 | 48 | if (scheduleDown && desiredCapacityDownMin && desiredCapacityDownMax) { 49 | new CfnSchedule(this, `ScaleDownSchedule ${dbClusterId}`, { 50 | flexibleTimeWindow: { 51 | mode: 'OFF' 52 | }, 53 | scheduleExpression: scheduleDown, 54 | target: { 55 | arn: `arn:aws:scheduler:::aws-sdk:rds:modifyDBCluster`, 56 | roleArn: schedulerRole.roleArn, 57 | input: `{"DbClusterIdentifier":"${dbClusterId}","ServerlessV2ScalingConfiguration":{"MinCapacity":${desiredCapacityDownMin},"MaxCapacity":${desiredCapacityDownMax}}}`, 58 | retryPolicy: { 59 | maximumRetryAttempts: 3 60 | } 61 | }, 62 | name: `scale-down-${dbClusterId}`, 63 | description: 'Scale down Aurora Serverless cluster during non-business hours', 64 | state: 'ENABLED' 65 | }); 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /samples/scheduled-aurora-serverless-scaling/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scheduled-autoscaling-amazon-aurora-serverless", 3 | "version": "0.1.0", 4 | "bin": { 5 | "scheduled-autoscaling-amazon-aurora-serverless": "bin/scheduled-autoscaling-amazon-aurora-serverless.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.11", 15 | "@types/node": "20.11.14", 16 | "aws-cdk": "2.126.0", 17 | "cdk-nag": "^2.28.175", 18 | "jest": "^29.7.0", 19 | "ts-jest": "^29.1.2", 20 | "ts-node": "^10.9.2", 21 | "typescript": "~5.3.3" 22 | }, 23 | "dependencies": { 24 | "aws-cdk-lib": "2.126.0", 25 | "constructs": "^10.0.0", 26 | "source-map-support": "^0.5.21" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/scheduled-aurora-serverless-scaling/test/cdk.test.ts: -------------------------------------------------------------------------------- 1 | import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag' 2 | import { ScheduledAutoscalingAmazonAuroraServerlessStack } from '../lib/scheduled-autoscaling-amazon-aurora-serverless-stack'; 3 | import { Annotations, Match } from 'aws-cdk-lib/assertions'; 4 | import { App, Aspects, Stack } from 'aws-cdk-lib'; 5 | 6 | describe('cdk-nag AwsSolutions Pack', () => { 7 | let stack: Stack; 8 | let app: App; 9 | // In this case we can use beforeAll() over beforeEach() since our tests 10 | // do not modify the state of the application 11 | beforeAll(() => { 12 | // GIVEN 13 | app = new App(); 14 | stack = new ScheduledAutoscalingAmazonAuroraServerlessStack(app, 'test'); 15 | 16 | // WHEN 17 | Aspects.of(stack).add(new AwsSolutionsChecks()); 18 | NagSuppressions.addStackSuppressions(stack, [ 19 | { id: 'AwsSolutions-IAM4', reason: 'Lambda function uses AWSLambdaBasicExecutionRole.' } 20 | ]); 21 | NagSuppressions.addStackSuppressions(stack, [ 22 | { id: 'AwsSolutions-IAM5', reason: 'False positive due to Lambda function arn wildcard: Resource:::*' } 23 | ]); 24 | }); 25 | 26 | // THEN 27 | test('No unsuppressed Warnings', () => { 28 | const warnings = Annotations.fromStack(stack).findWarning( 29 | '*', 30 | Match.stringLikeRegexp('AwsSolutions-.*') 31 | ); 32 | expect(warnings).toHaveLength(0); 33 | }); 34 | 35 | test('No unsuppressed Errors', () => { 36 | const errors = Annotations.fromStack(stack).findError( 37 | '*', 38 | Match.stringLikeRegexp('AwsSolutions-.*') 39 | ); 40 | expect(errors).toHaveLength(0); 41 | }); 42 | }); 43 | 44 | 45 | -------------------------------------------------------------------------------- /samples/scheduled-aurora-serverless-scaling/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import", 11 | "@stylistic" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 2018, 16 | "sourceType": "module", 17 | "project": "./tsconfig.dev.json" 18 | }, 19 | "extends": [ 20 | "plugin:import/typescript" 21 | ], 22 | "settings": { 23 | "import/parsers": { 24 | "@typescript-eslint/parser": [ 25 | ".ts", 26 | ".tsx" 27 | ] 28 | }, 29 | "import/resolver": { 30 | "node": {}, 31 | "typescript": { 32 | "project": "./tsconfig.dev.json", 33 | "alwaysTryTypes": true 34 | } 35 | } 36 | }, 37 | "ignorePatterns": [ 38 | "*.js", 39 | "*.d.ts", 40 | "node_modules/", 41 | "*.generated.ts", 42 | "coverage", 43 | "!.projenrc.js" 44 | ], 45 | "rules": { 46 | "indent": [ 47 | "off" 48 | ], 49 | "@stylistic/indent": [ 50 | "error", 51 | 2 52 | ], 53 | "quotes": [ 54 | "error", 55 | "single", 56 | { 57 | "avoidEscape": true 58 | } 59 | ], 60 | "comma-dangle": [ 61 | "error", 62 | "always-multiline" 63 | ], 64 | "comma-spacing": [ 65 | "error", 66 | { 67 | "before": false, 68 | "after": true 69 | } 70 | ], 71 | "no-multi-spaces": [ 72 | "error", 73 | { 74 | "ignoreEOLComments": false 75 | } 76 | ], 77 | "array-bracket-spacing": [ 78 | "error", 79 | "never" 80 | ], 81 | "array-bracket-newline": [ 82 | "error", 83 | "consistent" 84 | ], 85 | "object-curly-spacing": [ 86 | "error", 87 | "always" 88 | ], 89 | "object-curly-newline": [ 90 | "error", 91 | { 92 | "multiline": true, 93 | "consistent": true 94 | } 95 | ], 96 | "object-property-newline": [ 97 | "error", 98 | { 99 | "allowAllPropertiesOnSameLine": true 100 | } 101 | ], 102 | "keyword-spacing": [ 103 | "error" 104 | ], 105 | "brace-style": [ 106 | "error", 107 | "1tbs", 108 | { 109 | "allowSingleLine": true 110 | } 111 | ], 112 | "space-before-blocks": [ 113 | "error" 114 | ], 115 | "curly": [ 116 | "error", 117 | "multi-line", 118 | "consistent" 119 | ], 120 | "@stylistic/member-delimiter-style": [ 121 | "error" 122 | ], 123 | "semi": [ 124 | "error", 125 | "always" 126 | ], 127 | "max-len": [ 128 | "error", 129 | { 130 | "code": 150, 131 | "ignoreUrls": true, 132 | "ignoreStrings": true, 133 | "ignoreTemplateLiterals": true, 134 | "ignoreComments": true, 135 | "ignoreRegExpLiterals": true 136 | } 137 | ], 138 | "quote-props": [ 139 | "error", 140 | "consistent-as-needed" 141 | ], 142 | "@typescript-eslint/no-require-imports": [ 143 | "error" 144 | ], 145 | "import/no-extraneous-dependencies": [ 146 | "error", 147 | { 148 | "devDependencies": [ 149 | "**/test/**", 150 | "**/build-tools/**" 151 | ], 152 | "optionalDependencies": false, 153 | "peerDependencies": true 154 | } 155 | ], 156 | "import/no-unresolved": [ 157 | "error" 158 | ], 159 | "import/order": [ 160 | "warn", 161 | { 162 | "groups": [ 163 | "builtin", 164 | "external" 165 | ], 166 | "alphabetize": { 167 | "order": "asc", 168 | "caseInsensitive": true 169 | } 170 | } 171 | ], 172 | "import/no-duplicates": [ 173 | "error" 174 | ], 175 | "no-shadow": [ 176 | "off" 177 | ], 178 | "@typescript-eslint/no-shadow": [ 179 | "error" 180 | ], 181 | "key-spacing": [ 182 | "error" 183 | ], 184 | "no-multiple-empty-lines": [ 185 | "error" 186 | ], 187 | "@typescript-eslint/no-floating-promises": [ 188 | "error" 189 | ], 190 | "no-return-await": [ 191 | "off" 192 | ], 193 | "@typescript-eslint/return-await": [ 194 | "error" 195 | ], 196 | "no-trailing-spaces": [ 197 | "error" 198 | ], 199 | "dot-notation": [ 200 | "error" 201 | ], 202 | "no-bitwise": [ 203 | "error" 204 | ], 205 | "@typescript-eslint/member-ordering": [ 206 | "error", 207 | { 208 | "default": [ 209 | "public-static-field", 210 | "public-static-method", 211 | "protected-static-field", 212 | "protected-static-method", 213 | "private-static-field", 214 | "private-static-method", 215 | "field", 216 | "constructor", 217 | "method" 218 | ] 219 | } 220 | ] 221 | }, 222 | "overrides": [ 223 | { 224 | "files": [ 225 | ".projenrc.js" 226 | ], 227 | "rules": { 228 | "@typescript-eslint/no-require-imports": "off", 229 | "import/no-extraneous-dependencies": "off" 230 | } 231 | } 232 | ] 233 | } 234 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | * text=auto eol=lf 4 | *.snap linguist-generated 5 | /.eslintrc.json linguist-generated 6 | /.gitattributes linguist-generated 7 | /.gitignore linguist-generated 8 | /.npmignore linguist-generated 9 | /.projen/** linguist-generated 10 | /.projen/deps.json linguist-generated 11 | /.projen/files.json linguist-generated 12 | /.projen/tasks.json linguist-generated 13 | /cdk.json linguist-generated 14 | /LICENSE linguist-generated 15 | /package.json linguist-generated 16 | /tsconfig.dev.json linguist-generated 17 | /tsconfig.json linguist-generated 18 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/package.json 7 | !/LICENSE 8 | !/.npmignore 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | lib-cov 21 | coverage 22 | *.lcov 23 | .nyc_output 24 | build/Release 25 | node_modules/ 26 | jspm_packages/ 27 | *.tsbuildinfo 28 | .eslintcache 29 | *.tgz 30 | .yarn-integrity 31 | .cache 32 | /test-reports/ 33 | junit.xml 34 | /coverage/ 35 | !/test/ 36 | !/tsconfig.json 37 | !/tsconfig.dev.json 38 | !/src/ 39 | /lib 40 | /dist/ 41 | !/.eslintrc.json 42 | /assets/ 43 | !/cdk.json 44 | /cdk.out/ 45 | .cdk.staging/ 46 | .parcel-cache/ 47 | !/.projenrc.js 48 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | /test/ 7 | /tsconfig.dev.json 8 | /src/ 9 | !/lib/ 10 | !/lib/**/*.js 11 | !/lib/**/*.d.ts 12 | dist 13 | /tsconfig.json 14 | /.github/ 15 | /.vscode/ 16 | /.idea/ 17 | /.projenrc.js 18 | tsconfig.tsbuildinfo 19 | /.eslintrc.json 20 | !/assets/ 21 | cdk.out/ 22 | .cdk.staging/ 23 | /.gitattributes 24 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/.projenrc.js: -------------------------------------------------------------------------------- 1 | const { awscdk } = require('projen'); 2 | const project = new awscdk.AwsCdkTypeScriptApp({ 3 | cdkVersion: '2.171.0', 4 | defaultReleaseBranch: 'main', 5 | github: false, 6 | name: 'tenant-isolation-patterns', 7 | deps: [ 8 | '@aws-sdk/client-sts', 9 | '@types/aws-lambda', 10 | 'aws-jwt-verify', 11 | 'aws-lambda', 12 | '@aws-sdk/client-dynamodb', 13 | '@aws-sdk/lib-dynamodb', 14 | 'cdk-nag', 15 | 'http-status-codes', 16 | 'jwt-decode', 17 | 'source-map-support', 18 | ], 19 | 20 | // deps: [], /* Runtime dependencies of this module. */ 21 | // description: undefined, /* The description is just a string that helps people understand the purpose of the package. */ 22 | // devDeps: [], /* Build dependencies for this module. */ 23 | // packageName: undefined, /* The "name" in package.json. */ 24 | }); 25 | project.tasks.tryFind('deploy')?.reset('cdk deploy --require-approval=never'); 26 | project.tasks.tryFind('destroy')?.reset('cdk destroy --force'); 27 | project.synth(); -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/README.md: -------------------------------------------------------------------------------- 1 | # Tenant isolation patterns 2 | 3 | ## Introduction 4 | 5 | These are tenant isolation patterns using [AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html) and [Attribute Based Access Control (ABAC)](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction_attribute-based-access-control.html). 6 | 7 | Cognito is used for the identity provider, along with pre-token-generation to include the tags necessary for `AssumeRoleWithWebIdentity`. 8 | 9 | ## Deployment 10 | 11 | Install pre-requisite packages: 12 | 13 | ```bash 14 | yarn install 15 | ``` 16 | 17 | Deploy the shared stack, and any other specific stack you'd like to use. 18 | 19 | Ie. To deploy the DynamoDB example: 20 | 21 | ```bash 22 | cdk deploy Shared DynamoDB 23 | ``` 24 | 25 | ## Testing 26 | 27 | To test a stack, source the test_interface and run the corresponding test. 28 | 29 | Ie. For DynamoDB: 30 | 31 | ```bash 32 | source test_interface.sh 33 | test_dynamodb 34 | ``` 35 | 36 | ## Clean up 37 | 38 | ```bash 39 | cdk destroy --all 40 | ``` 41 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node -P tsconfig.json --prefer-ts-exts src/main.ts", 3 | "output": "cdk.out", 4 | "build": "npx projen bundle", 5 | "watch": { 6 | "include": [ 7 | "src/**/*.ts", 8 | "test/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "README.md", 12 | "cdk*.json", 13 | "**/*.d.ts", 14 | "**/*.js", 15 | "tsconfig.json", 16 | "package*.json", 17 | "yarn.lock", 18 | "node_modules" 19 | ] 20 | }, 21 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 22 | } 23 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tenant-isolation-patterns", 3 | "scripts": { 4 | "build": "npx projen build", 5 | "bundle": "npx projen bundle", 6 | "clobber": "npx projen clobber", 7 | "compile": "npx projen compile", 8 | "default": "npx projen default", 9 | "deploy": "npx projen deploy", 10 | "destroy": "npx projen destroy", 11 | "diff": "npx projen diff", 12 | "eject": "npx projen eject", 13 | "eslint": "npx projen eslint", 14 | "package": "npx projen package", 15 | "post-compile": "npx projen post-compile", 16 | "post-upgrade": "npx projen post-upgrade", 17 | "pre-compile": "npx projen pre-compile", 18 | "synth": "npx projen synth", 19 | "synth:silent": "npx projen synth:silent", 20 | "test": "npx projen test", 21 | "test:watch": "npx projen test:watch", 22 | "upgrade": "npx projen upgrade", 23 | "watch": "npx projen watch", 24 | "projen": "npx projen" 25 | }, 26 | "devDependencies": { 27 | "@stylistic/eslint-plugin": "^2", 28 | "@types/jest": "^29.5.14", 29 | "@types/node": "^14", 30 | "@typescript-eslint/eslint-plugin": "^8", 31 | "@typescript-eslint/parser": "^8", 32 | "aws-cdk": "^2.171.0", 33 | "esbuild": "^0.24.0", 34 | "eslint": "^9", 35 | "eslint-import-resolver-typescript": "^3.6.3", 36 | "eslint-plugin-import": "^2.31.0", 37 | "jest": "^29.7.0", 38 | "jest-junit": "^15", 39 | "projen": "^0.90.5", 40 | "ts-jest": "^29.2.5", 41 | "ts-node": "^10.9.2", 42 | "typescript": "^5.7.2" 43 | }, 44 | "dependencies": { 45 | "@aws-sdk/client-dynamodb": "^3.699.0", 46 | "@aws-sdk/client-sts": "^3.699.0", 47 | "@aws-sdk/lib-dynamodb": "^3.699.0", 48 | "@types/aws-lambda": "^8.10.145", 49 | "aws-cdk-lib": "^2.171.0", 50 | "aws-jwt-verify": "^4.0.1", 51 | "aws-lambda": "^1.0.7", 52 | "cdk-nag": "^2.34.7", 53 | "constructs": "^10.0.5", 54 | "http-status-codes": "^2.3.0", 55 | "jwt-decode": "^4.0.0", 56 | "source-map-support": "^0.5.21" 57 | }, 58 | "license": "Apache-2.0", 59 | "publishConfig": { 60 | "access": "public" 61 | }, 62 | "version": "0.0.0", 63 | "jest": { 64 | "coverageProvider": "v8", 65 | "testMatch": [ 66 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 67 | "/@(src|test)/**/__tests__/**/*.ts?(x)" 68 | ], 69 | "clearMocks": true, 70 | "collectCoverage": true, 71 | "coverageReporters": [ 72 | "json", 73 | "lcov", 74 | "clover", 75 | "cobertura", 76 | "text" 77 | ], 78 | "coverageDirectory": "coverage", 79 | "coveragePathIgnorePatterns": [ 80 | "/node_modules/" 81 | ], 82 | "testPathIgnorePatterns": [ 83 | "/node_modules/" 84 | ], 85 | "watchPathIgnorePatterns": [ 86 | "/node_modules/" 87 | ], 88 | "reporters": [ 89 | "default", 90 | [ 91 | "jest-junit", 92 | { 93 | "outputDirectory": "test-reports" 94 | } 95 | ] 96 | ], 97 | "transform": { 98 | "^.+\\.[t]sx?$": [ 99 | "ts-jest", 100 | { 101 | "tsconfig": "tsconfig.dev.json" 102 | } 103 | ] 104 | } 105 | }, 106 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 107 | } 108 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/src/api/api.ts: -------------------------------------------------------------------------------- 1 | import { Aws, CfnOutput, Duration, RemovalPolicy } from 'aws-cdk-lib'; 2 | import { AccessLogField, AccessLogFormat, ApiKeySourceType, HttpIntegration, IdentitySource, LambdaIntegration, LogGroupLogDestination, RequestAuthorizer, RestApi } from 'aws-cdk-lib/aws-apigateway'; 3 | import { HttpMethod } from 'aws-cdk-lib/aws-apigatewayv2'; 4 | import { Role } from 'aws-cdk-lib/aws-iam'; 5 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 6 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 7 | import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; 8 | import { Construct } from 'constructs'; 9 | import { MyIdentity } from '../identity/identity'; 10 | 11 | interface MyApiProps { 12 | identity: MyIdentity; 13 | myCell: string; 14 | myName: string; 15 | role: Role; 16 | } 17 | 18 | export class MyApi extends Construct { 19 | public readonly api: RestApi; 20 | public readonly authorizer: RequestAuthorizer; 21 | constructor(scope: Construct, id: string, props: MyApiProps) { 22 | super(scope, id); 23 | 24 | const logGroup = new LogGroup(this, 'LogGroup', { 25 | logGroupName: '/' + props.myName + '/' + props.myCell + '/api', 26 | retention: RetentionDays.ONE_DAY, 27 | removalPolicy: RemovalPolicy.DESTROY, 28 | }); 29 | 30 | const authorizerFn = new NodejsFunction(this, 'AuthorizerFn', { 31 | entry: __dirname + '/authorizer.function.ts', 32 | runtime: Runtime.NODEJS_LATEST, 33 | handler: 'handler', 34 | timeout: Duration.seconds(30), 35 | logGroup: logGroup, 36 | environment: { 37 | USERPOOL_ID: props.identity.userPoolId, 38 | CLIENT_ID: props.identity.clientId, 39 | ASSUMED_ROLE: props.role.roleArn, 40 | }, 41 | }); 42 | this.authorizer = new RequestAuthorizer(this, 'Authorizer', { 43 | handler: authorizerFn, 44 | identitySources: [IdentitySource.header('Authorization')], 45 | }); 46 | 47 | this.api = new RestApi(this, 'Api', { 48 | restApiName: props.myName + '-' + props.myCell, 49 | cloudWatchRoleRemovalPolicy: RemovalPolicy.DESTROY, 50 | apiKeySourceType: ApiKeySourceType.AUTHORIZER, 51 | deployOptions: { 52 | accessLogDestination: new LogGroupLogDestination(logGroup), 53 | accessLogFormat: AccessLogFormat.custom(JSON.stringify({ 54 | path: AccessLogField.contextResourcePath(), 55 | requestId: AccessLogField.contextRequestId(), 56 | sourceIp: AccessLogField.contextIdentitySourceIp(), 57 | method: AccessLogField.contextHttpMethod(), 58 | authorizerLatency: AccessLogField.contextAuthorizerIntegrationLatency(), 59 | integrationLatency: AccessLogField.contextIntegrationLatency(), 60 | responseLatency: AccessLogField.contextResponseLatency(), 61 | authorizerStatus: AccessLogField.contextAuthorizerStatus(), 62 | integrationStatus: AccessLogField.contextIntegrationStatus(), 63 | status: AccessLogField.contextStatus(), 64 | transactionId: AccessLogField.contextAuthorizer('transactionId'), 65 | tenantId: AccessLogField.contextAuthorizer('tenantId'), 66 | tier: AccessLogField.contextAuthorizer('tier'), 67 | role: AccessLogField.contextAuthorizer('role'), 68 | stackName: Aws.STACK_NAME, 69 | }), 70 | ), 71 | }, 72 | }); 73 | this.api.root.addMethod('ANY', new HttpIntegration('https://github.com/aws-samples/data-for-saas-patterns'), { authorizer: this.authorizer }); 74 | new CfnOutput(this, 'ApiUrl', { key: 'ApiUrl', value: this.api.url }); 75 | } 76 | addTest(path: string, failFn: NodejsFunction, successFn: NodejsFunction) { 77 | const pathResource = this.api.root.addResource(path); 78 | const successPath = pathResource.addResource('success'); 79 | const successLambdaIntegration = new LambdaIntegration(successFn); 80 | successPath.addMethod(HttpMethod.ANY, successLambdaIntegration, { authorizer: this.authorizer }); 81 | const failPath = pathResource.addResource('fail'); 82 | const failLambdaIntegration = new LambdaIntegration(failFn); 83 | failPath.addMethod(HttpMethod.ANY, failLambdaIntegration, { authorizer: this.authorizer }); 84 | } 85 | } -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/src/api/authorizer.function.ts: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register'; 2 | import { AssumeRoleWithWebIdentityCommand, AssumeRoleWithWebIdentityCommandInput, STSClient } from '@aws-sdk/client-sts'; 3 | import { APIGatewayAuthorizerResult, APIGatewayRequestAuthorizerEvent, APIGatewayRequestAuthorizerEventHeaders } from 'aws-lambda/trigger/api-gateway-authorizer'; 4 | import { jwtDecode } from 'jwt-decode'; 5 | 6 | const stsClient = new STSClient(); 7 | 8 | const defaultDenyAllPolicy: APIGatewayAuthorizerResult = { 9 | principalId: 'user', 10 | policyDocument: { 11 | Version: '2012-10-17', 12 | Statement: [ 13 | { 14 | Action: '*', 15 | Effect: 'Deny', 16 | Resource: '*', 17 | }, 18 | ], 19 | }, 20 | }; 21 | 22 | export const handler = async (event: APIGatewayRequestAuthorizerEvent): Promise => { 23 | try { 24 | const headers: APIGatewayRequestAuthorizerEventHeaders = event.headers as APIGatewayRequestAuthorizerEventHeaders; 25 | const parsed = JSON.parse(JSON.stringify(headers)); 26 | const authorization: string = parsed.Authorization.split(' '); 27 | if (authorization[0] != 'Bearer') { 28 | return defaultDenyAllPolicy; 29 | } 30 | const jwt = authorization[1]; 31 | console.log('JWT: ', jwtDecode(jwt)); 32 | const decodedJwt = JSON.parse(JSON.stringify(jwtDecode(jwt)), function(key, value) { 33 | if ( key == 'custom:tenantId' ) { 34 | this.tenantId = value; 35 | } else {return value;} 36 | }); 37 | const tenantId = decodedJwt.tenantId; 38 | const input: AssumeRoleWithWebIdentityCommandInput = { 39 | RoleArn: process.env.ASSUMED_ROLE, 40 | WebIdentityToken: jwt, 41 | RoleSessionName: tenantId, 42 | }; 43 | const command = new AssumeRoleWithWebIdentityCommand(input); 44 | const credentials = await stsClient.send(command); 45 | console.log(credentials); 46 | 47 | const context = { 48 | tenantId: tenantId, 49 | accessKeyId: credentials.Credentials?.AccessKeyId, 50 | secretAccessKey: credentials.Credentials?.SecretAccessKey, 51 | sessionToken: credentials.Credentials?.SessionToken, 52 | }; 53 | console.log('Context: ', JSON.stringify(context)); 54 | 55 | const arn = event.methodArn.split('/'); 56 | 57 | const response: APIGatewayAuthorizerResult = { 58 | principalId: 'user', 59 | context, 60 | policyDocument: { 61 | Version: '2012-10-17', 62 | Statement: [ 63 | { 64 | Action: 'execute-api:Invoke', 65 | Effect: 'Allow', 66 | Resource: arn[0]+'/*', 67 | }, 68 | ], 69 | }, 70 | }; 71 | console.log('Response: ', JSON.stringify(response)); 72 | return response; 73 | } catch (error) { 74 | console.log(error); 75 | return defaultDenyAllPolicy; 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/src/dynamodb/dynamodb.function.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 2 | import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb'; 3 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 4 | import { StatusCodes } from 'http-status-codes'; 5 | 6 | const tableName = process.env.TABLE_NAME; 7 | 8 | export const fail = async (event: APIGatewayProxyEvent): Promise => { 9 | console.log(event); 10 | const client = new DynamoDBClient({ 11 | credentials: { 12 | accessKeyId: event.requestContext.authorizer?.accessKeyId, 13 | secretAccessKey: event.requestContext.authorizer?.secretAccessKey, 14 | sessionToken: event.requestContext.authorizer?.sessionToken, 15 | }, 16 | }); 17 | const docClient = DynamoDBDocumentClient.from(client); 18 | const tenantId=event.requestContext.authorizer?.tenantId; 19 | console.log(tenantId); 20 | console.log(tableName); 21 | const command = new PutCommand({ 22 | TableName: tableName, 23 | Item: { 24 | pk: tenantId+'foo', // trying to put another tenant's item 25 | sk: 'Stevie Ray Vaughan', 26 | guitar: 'Stratocaster', 27 | }, 28 | }); 29 | // We expect this to throw an error, because tenant isolation should not let us write to another tenant 30 | try { 31 | await docClient.send(command); 32 | return { 33 | statusCode: StatusCodes.INTERNAL_SERVER_ERROR, 34 | body: JSON.stringify({ message: 'Unexpected behaviour. Tenant isolation breached.' }), 35 | }; 36 | } catch (error) { 37 | return { 38 | statusCode: StatusCodes.OK, 39 | body: JSON.stringify({ message: 'Expected behaviour. Tenant isolation worked. Cant write to another tenant.' }), 40 | }; 41 | } 42 | }; 43 | 44 | export const success = async (event: APIGatewayProxyEvent): Promise => { 45 | console.log(event); 46 | const client = new DynamoDBClient({ 47 | credentials: { 48 | accessKeyId: event.requestContext.authorizer?.accessKeyId, 49 | secretAccessKey: event.requestContext.authorizer?.secretAccessKey, 50 | sessionToken: event.requestContext.authorizer?.sessionToken, 51 | }, 52 | }); 53 | const docClient = DynamoDBDocumentClient.from(client); 54 | const tenantId=event.requestContext.authorizer?.tenantId; 55 | console.log(tenantId); 56 | console.log(tableName); 57 | const command = new PutCommand({ 58 | TableName: tableName, 59 | Item: { 60 | pk: tenantId, 61 | sk: 'Duane Allman', 62 | guitar: 'Les Paul', 63 | }, 64 | }); 65 | try { 66 | await docClient.send(command); 67 | return { 68 | statusCode: StatusCodes.OK, 69 | body: JSON.stringify({ message: 'Expected behaviour. Can write to our own tenant.' }), 70 | }; 71 | } catch (error) { 72 | return { 73 | statusCode: StatusCodes.INTERNAL_SERVER_ERROR, 74 | body: JSON.stringify({ message: 'Unexpected behaviour. Cant write to our own tenant.', error: error }), 75 | }; 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/src/dynamodb/dynamodb.ts: -------------------------------------------------------------------------------- 1 | import { Duration, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; 2 | import { AttributeType, TableV2 } from 'aws-cdk-lib/aws-dynamodb'; 3 | import { Effect, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam'; 4 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 5 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 6 | import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; 7 | import { Construct } from 'constructs'; 8 | import { MyApi } from '../api/api'; 9 | 10 | const description = 'dynamodb'; 11 | 12 | interface MyDynamodbProps extends StackProps { 13 | api: MyApi; 14 | myCell: string; 15 | myName: string; 16 | role: Role; 17 | } 18 | 19 | export class MyDynamodb extends Stack { 20 | constructor(scope: Construct, id: string, props: MyDynamodbProps) { 21 | super(scope, id, props); 22 | 23 | const logGroup = new LogGroup(this, 'LogGroup', { 24 | logGroupName: '/' + props.myName + '/' + props.myCell + '/' + description, 25 | retention: RetentionDays.ONE_DAY, 26 | removalPolicy: RemovalPolicy.DESTROY, 27 | }); 28 | 29 | const table = new TableV2(this, 'DynamoDB', { 30 | partitionKey: { 31 | name: 'pk', 32 | type: AttributeType.STRING, 33 | }, 34 | sortKey: { 35 | name: 'sk', 36 | type: AttributeType.STRING, 37 | }, 38 | removalPolicy: RemovalPolicy.DESTROY, 39 | pointInTimeRecovery: true, 40 | }); 41 | 42 | const successFn = new NodejsFunction(this, 'SuccessFn', { 43 | entry: __dirname + '/dynamodb.function.ts', 44 | runtime: Runtime.NODEJS_LATEST, 45 | handler: 'success', 46 | timeout: Duration.seconds(30), 47 | logGroup: logGroup, 48 | environment: { 49 | TABLE_NAME: table.tableName, 50 | }, 51 | }); 52 | 53 | const failFn = new NodejsFunction(this, 'FailFn', { 54 | entry: __dirname + '/dynamodb.function.ts', 55 | runtime: Runtime.NODEJS_LATEST, 56 | handler: 'fail', 57 | timeout: Duration.seconds(30), 58 | logGroup: logGroup, 59 | environment: { 60 | TABLE_NAME: table.tableName, 61 | }, 62 | }); 63 | 64 | props.api.addTest('dynamodb', failFn, successFn); 65 | props.role.addToPolicy( 66 | new PolicyStatement({ 67 | effect: Effect.ALLOW, 68 | actions: [ 69 | 'dynamodb:BatchGetItem', 70 | 'dynamodb:BatchWriteItem', 71 | 'dynamodb:ConditionCheckItem', 72 | 'dynamodb:DeleteItem', 73 | 'dynamodb:DescribeTable', 74 | 'dynamodb:GetItem', 75 | 'dynamodb:PutItem', 76 | 'dynamodb:Query', 77 | 'dynamodb:Scan', 78 | 'dynamodb:UpdateItem', 79 | ], 80 | resources: [ 81 | table.tableArn, 82 | ], 83 | conditions: { 84 | 'ForAllValues:StringEquals': { 85 | 'dynamodb:LeadingKeys': [ 86 | '${aws:PrincipalTag/tenantId}', 87 | ], 88 | }, 89 | }, 90 | }), 91 | ); 92 | } 93 | } -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/src/identity/identity.ts: -------------------------------------------------------------------------------- 1 | import { Aws, CfnOutput, Duration, RemovalPolicy } from 'aws-cdk-lib'; 2 | import { AdvancedSecurityMode, ClientAttributes, LambdaVersion, StringAttribute, UserPool, UserPoolOperation } from 'aws-cdk-lib/aws-cognito'; 3 | import { OpenIdConnectPrincipal, OpenIdConnectProvider } from 'aws-cdk-lib/aws-iam'; 4 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 5 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 6 | import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; 7 | import { Construct } from 'constructs'; 8 | 9 | interface MyIdentityProps { 10 | myCell: string; 11 | myName: string; 12 | } 13 | 14 | export class MyIdentity extends Construct { 15 | public readonly clientId: string; 16 | public readonly oidcPrincipal: OpenIdConnectPrincipal; 17 | public readonly oidcProvider: OpenIdConnectProvider; 18 | public readonly userPoolId: string; 19 | constructor(scope: Construct, id: string, props: MyIdentityProps) { 20 | super(scope, id); 21 | 22 | const userPool = new UserPool(this, 'UserPool', { 23 | userPoolName: props.myName + '-' + props.myCell, 24 | selfSignUpEnabled: false, 25 | autoVerify: { email: true }, 26 | signInAliases: { email: true, username: true }, 27 | customAttributes: { 28 | tenantId: new StringAttribute({ minLen: 1, maxLen: 36, mutable: false }), // Don't let anyone change the tenantId after creation! 29 | }, 30 | removalPolicy: RemovalPolicy.DESTROY, 31 | advancedSecurityMode: AdvancedSecurityMode.AUDIT, 32 | }); 33 | this.userPoolId = userPool.userPoolId; 34 | userPool.addDomain('CognitoDomain', { 35 | cognitoDomain: { 36 | domainPrefix: props.myName.toLowerCase() + '-auth', 37 | }, 38 | }); 39 | const userPoolClient = userPool.addClient('UserPoolClient', { 40 | userPoolClientName: props.myName + '-' + props.myCell, 41 | authFlows: { userPassword: true }, 42 | readAttributes: new ClientAttributes() 43 | .withStandardAttributes({ email: true }) 44 | .withCustomAttributes(...['tenantId']), 45 | writeAttributes: new ClientAttributes() 46 | .withStandardAttributes({ email: true }) 47 | .withCustomAttributes(...['tenantId']), 48 | accessTokenValidity: Duration.days(1), 49 | idTokenValidity: Duration.days(1), 50 | refreshTokenValidity: Duration.days(3), 51 | }); 52 | this.clientId = userPoolClient.userPoolClientId; 53 | const logGroup = new LogGroup(this, 'LogGroup', { 54 | logGroupName: '/'+props.myName+'/'+props.myCell+'/pre-token-generation', 55 | retention: RetentionDays.ONE_DAY, 56 | removalPolicy: RemovalPolicy.DESTROY, 57 | }); 58 | const preTokenGenerationFn = new NodejsFunction(this, 'PreTokenGenerationFn', { 59 | entry: __dirname + '/pre-token-generation.function.ts', 60 | runtime: Runtime.NODEJS_LATEST, 61 | handler: 'handler', 62 | timeout: Duration.seconds(30), 63 | logGroup: logGroup, 64 | }); 65 | userPool.addTrigger(UserPoolOperation.PRE_TOKEN_GENERATION_CONFIG, preTokenGenerationFn, LambdaVersion.V2_0); 66 | const oidcEndpoint = 'https://cognito-idp.' + Aws.REGION + '.amazonaws.com/' + userPool.userPoolId; 67 | this.oidcProvider = new OpenIdConnectProvider(this, 'MyOidcProvider', { 68 | url: oidcEndpoint, 69 | clientIds: [ 70 | userPoolClient.userPoolClientId, 71 | ], 72 | }); 73 | const principal = new OpenIdConnectPrincipal(this.oidcProvider) 74 | .withConditions({ 75 | 'ForAllValues:StringEquals': { 76 | 'cognito-identity.amazonaws.com:aud': userPoolClient.userPoolClientId, 77 | }, 78 | }); 79 | //@ts-ignore 80 | this.oidcPrincipal = principal.withSessionTags(); 81 | new CfnOutput(this, 'ClientId', { key: 'ClientId', value: this.clientId }); 82 | new CfnOutput(this, 'UserPoolId', { key: 'UserPoolId', value: this.userPoolId }); 83 | new CfnOutput(this, 'OidcProvider', { key: 'OidcProvider', value: this.oidcProvider.openIdConnectProviderIssuer }); 84 | } 85 | } -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/src/identity/pre-token-generation.function.ts: -------------------------------------------------------------------------------- 1 | export const handler = function(event: any, context: any) { 2 | console.log(event); 3 | // Retrieve user attribute from event request 4 | const userAttributes = event.request.userAttributes; 5 | // Add scope to event response 6 | event.response = { 7 | claimsAndScopeOverrideDetails: { 8 | idTokenGeneration: { 9 | claimsToAddOrOverride: { 10 | 'https://aws.amazon.com/tags': { 11 | principal_tags: { 12 | tenantId: [userAttributes['custom:tenantId']], 13 | }, 14 | }, 15 | }, 16 | }, 17 | }, 18 | }; 19 | // Return to Amazon Cognito 20 | context.done(null, event); 21 | }; -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/src/main.ts: -------------------------------------------------------------------------------- 1 | import { App, Aspects, Stack, StackProps } from 'aws-cdk-lib'; 2 | import { ManagedPolicy, Role } from 'aws-cdk-lib/aws-iam'; 3 | import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag'; 4 | import { Construct } from 'constructs'; 5 | import { MyApi } from './api/api'; 6 | import { MyDynamodb } from './dynamodb/dynamodb'; 7 | import { MyIdentity } from './identity/identity'; 8 | 9 | const myName = 'TenantIsolationPatterns'; 10 | const myCell = 'Basic'; 11 | 12 | export class TenantIsolationPatterns extends Stack { 13 | public readonly api: MyApi; 14 | public readonly identity: MyIdentity; 15 | public readonly role: Role; 16 | constructor(scope: Construct, id: string, props: StackProps = {}) { 17 | super(scope, id, props); 18 | 19 | this.identity = new MyIdentity(this, 'Identity', { 20 | myCell: myCell, 21 | myName: myName, 22 | }); 23 | 24 | this.role = new Role(this, 'Role', { 25 | assumedBy: this.identity.oidcPrincipal, 26 | managedPolicies: [ 27 | ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), 28 | ], 29 | }); 30 | 31 | this.api = new MyApi(this, 'Api', { 32 | identity: this.identity, 33 | myCell: myCell, 34 | myName: myName, 35 | role: this.role, 36 | }); 37 | } 38 | } 39 | 40 | // for development, use account/region from cdk cli 41 | const devEnv = { 42 | account: process.env.CDK_DEFAULT_ACCOUNT, 43 | region: process.env.CDK_DEFAULT_REGION, 44 | }; 45 | 46 | const app = new App(); 47 | Aspects.of(app).add(new AwsSolutionsChecks({})); 48 | 49 | const shared = new TenantIsolationPatterns(app, 'Shared', { env: devEnv }); 50 | NagSuppressions.addStackSuppressions(shared, [ 51 | { id: 'AwsSolutions-APIG2', reason: 'Request validation not required for proof-of-concept.' }, 52 | { id: 'AwsSolutions-APIG3', reason: 'WAF not required for proof-of-concept.' }, 53 | { id: 'AwsSolutions-APIG6', reason: 'Detailed logging not required for proof-of-concept.' }, 54 | { id: 'AwsSolutions-COG1', reason: 'Password policy not required for proof-of-concept.' }, 55 | { id: 'AwsSolutions-COG2', reason: 'MFA not required for proof-of-concept.' }, 56 | { id: 'AwsSolutions-COG3', reason: 'No need for AdvancedSecurityMode set to ENFORCED.' }, 57 | { id: 'AwsSolutions-COG4', reason: 'Uses request authorizer.' }, 58 | { id: 'AwsSolutions-IAM4', reason: 'Uses AWSLambdaBasicExecutionRole.' }, 59 | { id: 'AwsSolutions-L1', reason: 'Uses NODEJS_LATEST.' }, 60 | ]); 61 | 62 | const dynamodb = new MyDynamodb(app, 'DynamoDB', { 63 | api: shared.api, 64 | myCell: myCell, 65 | myName: myName, 66 | role: shared.role, 67 | env: devEnv, 68 | }); 69 | NagSuppressions.addStackSuppressions(dynamodb, [ 70 | { id: 'AwsSolutions-IAM4', reason: 'Uses AWSLambdaBasicExecutionRole.' }, 71 | { id: 'AwsSolutions-L1', reason: 'Uses NODEJS_LATEST.' }, 72 | ]); 73 | 74 | app.synth(); -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/test/__snapshots__/main.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot 1`] = ` 4 | { 5 | "Mappings": { 6 | "LatestNodeRuntimeMap": { 7 | "af-south-1": { 8 | "value": "nodejs20.x", 9 | }, 10 | "ap-east-1": { 11 | "value": "nodejs20.x", 12 | }, 13 | "ap-northeast-1": { 14 | "value": "nodejs20.x", 15 | }, 16 | "ap-northeast-2": { 17 | "value": "nodejs20.x", 18 | }, 19 | "ap-northeast-3": { 20 | "value": "nodejs20.x", 21 | }, 22 | "ap-south-1": { 23 | "value": "nodejs20.x", 24 | }, 25 | "ap-south-2": { 26 | "value": "nodejs20.x", 27 | }, 28 | "ap-southeast-1": { 29 | "value": "nodejs20.x", 30 | }, 31 | "ap-southeast-2": { 32 | "value": "nodejs20.x", 33 | }, 34 | "ap-southeast-3": { 35 | "value": "nodejs20.x", 36 | }, 37 | "ap-southeast-4": { 38 | "value": "nodejs20.x", 39 | }, 40 | "ap-southeast-5": { 41 | "value": "nodejs20.x", 42 | }, 43 | "ap-southeast-7": { 44 | "value": "nodejs20.x", 45 | }, 46 | "ca-central-1": { 47 | "value": "nodejs20.x", 48 | }, 49 | "ca-west-1": { 50 | "value": "nodejs20.x", 51 | }, 52 | "cn-north-1": { 53 | "value": "nodejs18.x", 54 | }, 55 | "cn-northwest-1": { 56 | "value": "nodejs18.x", 57 | }, 58 | "eu-central-1": { 59 | "value": "nodejs20.x", 60 | }, 61 | "eu-central-2": { 62 | "value": "nodejs20.x", 63 | }, 64 | "eu-isoe-west-1": { 65 | "value": "nodejs18.x", 66 | }, 67 | "eu-north-1": { 68 | "value": "nodejs20.x", 69 | }, 70 | "eu-south-1": { 71 | "value": "nodejs20.x", 72 | }, 73 | "eu-south-2": { 74 | "value": "nodejs20.x", 75 | }, 76 | "eu-west-1": { 77 | "value": "nodejs20.x", 78 | }, 79 | "eu-west-2": { 80 | "value": "nodejs20.x", 81 | }, 82 | "eu-west-3": { 83 | "value": "nodejs20.x", 84 | }, 85 | "il-central-1": { 86 | "value": "nodejs20.x", 87 | }, 88 | "me-central-1": { 89 | "value": "nodejs20.x", 90 | }, 91 | "me-south-1": { 92 | "value": "nodejs20.x", 93 | }, 94 | "mx-central-1": { 95 | "value": "nodejs20.x", 96 | }, 97 | "sa-east-1": { 98 | "value": "nodejs20.x", 99 | }, 100 | "us-east-1": { 101 | "value": "nodejs20.x", 102 | }, 103 | "us-east-2": { 104 | "value": "nodejs20.x", 105 | }, 106 | "us-gov-east-1": { 107 | "value": "nodejs18.x", 108 | }, 109 | "us-gov-west-1": { 110 | "value": "nodejs18.x", 111 | }, 112 | "us-iso-east-1": { 113 | "value": "nodejs18.x", 114 | }, 115 | "us-iso-west-1": { 116 | "value": "nodejs18.x", 117 | }, 118 | "us-isob-east-1": { 119 | "value": "nodejs18.x", 120 | }, 121 | "us-west-1": { 122 | "value": "nodejs20.x", 123 | }, 124 | "us-west-2": { 125 | "value": "nodejs20.x", 126 | }, 127 | }, 128 | }, 129 | "Outputs": { 130 | "ApiEndpoint1541504C": { 131 | "Value": { 132 | "Fn::Join": [ 133 | "", 134 | [ 135 | "https://", 136 | { 137 | "Ref": "ApiCD79AAA0", 138 | }, 139 | ".execute-api.", 140 | { 141 | "Ref": "AWS::Region", 142 | }, 143 | ".", 144 | { 145 | "Ref": "AWS::URLSuffix", 146 | }, 147 | "/", 148 | { 149 | "Ref": "ApiDeploymentStageprod95EFE650", 150 | }, 151 | "/", 152 | ], 153 | ], 154 | }, 155 | }, 156 | "ApiUrl": { 157 | "Value": { 158 | "Fn::Join": [ 159 | "", 160 | [ 161 | "https://", 162 | { 163 | "Ref": "ApiCD79AAA0", 164 | }, 165 | ".execute-api.", 166 | { 167 | "Ref": "AWS::Region", 168 | }, 169 | ".", 170 | { 171 | "Ref": "AWS::URLSuffix", 172 | }, 173 | "/", 174 | { 175 | "Ref": "ApiDeploymentStageprod95EFE650", 176 | }, 177 | "/", 178 | ], 179 | ], 180 | }, 181 | }, 182 | "ClientId": { 183 | "Value": { 184 | "Ref": "IdentityUserPoolUserPoolClient1035B963", 185 | }, 186 | }, 187 | "OidcProvider": { 188 | "Value": { 189 | "Fn::Select": [ 190 | 1, 191 | { 192 | "Fn::Split": [ 193 | ":oidc-provider/", 194 | { 195 | "Ref": "IdentityMyOidcProvider7F02079C", 196 | }, 197 | ], 198 | }, 199 | ], 200 | }, 201 | }, 202 | "UserPoolId": { 203 | "Value": { 204 | "Ref": "IdentityUserPool96FE3B9B", 205 | }, 206 | }, 207 | }, 208 | "Parameters": { 209 | "BootstrapVersion": { 210 | "Default": "/cdk-bootstrap/hnb659fds/version", 211 | "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", 212 | "Type": "AWS::SSM::Parameter::Value", 213 | }, 214 | }, 215 | "Resources": { 216 | "ApiANY29CC0E6B": { 217 | "Properties": { 218 | "AuthorizationType": "CUSTOM", 219 | "AuthorizerId": { 220 | "Ref": "ApiAuthorizer787544C1", 221 | }, 222 | "HttpMethod": "ANY", 223 | "Integration": { 224 | "IntegrationHttpMethod": "GET", 225 | "Type": "HTTP_PROXY", 226 | "Uri": "https://github.com/aws-samples/data-for-saas-patterns", 227 | }, 228 | "ResourceId": { 229 | "Fn::GetAtt": [ 230 | "ApiCD79AAA0", 231 | "RootResourceId", 232 | ], 233 | }, 234 | "RestApiId": { 235 | "Ref": "ApiCD79AAA0", 236 | }, 237 | }, 238 | "Type": "AWS::ApiGateway::Method", 239 | }, 240 | "ApiAccount6C17A443": { 241 | "DeletionPolicy": "Delete", 242 | "DependsOn": [ 243 | "ApiCD79AAA0", 244 | ], 245 | "Properties": { 246 | "CloudWatchRoleArn": { 247 | "Fn::GetAtt": [ 248 | "ApiCloudWatchRoleCCA56614", 249 | "Arn", 250 | ], 251 | }, 252 | }, 253 | "Type": "AWS::ApiGateway::Account", 254 | "UpdateReplacePolicy": "Delete", 255 | }, 256 | "ApiAuthorizer787544C1": { 257 | "Properties": { 258 | "AuthorizerResultTtlInSeconds": 300, 259 | "AuthorizerUri": { 260 | "Fn::Join": [ 261 | "", 262 | [ 263 | "arn:", 264 | { 265 | "Fn::Select": [ 266 | 1, 267 | { 268 | "Fn::Split": [ 269 | ":", 270 | { 271 | "Fn::GetAtt": [ 272 | "ApiAuthorizerFn0AD8B47E", 273 | "Arn", 274 | ], 275 | }, 276 | ], 277 | }, 278 | ], 279 | }, 280 | ":apigateway:", 281 | { 282 | "Fn::Select": [ 283 | 3, 284 | { 285 | "Fn::Split": [ 286 | ":", 287 | { 288 | "Fn::GetAtt": [ 289 | "ApiAuthorizerFn0AD8B47E", 290 | "Arn", 291 | ], 292 | }, 293 | ], 294 | }, 295 | ], 296 | }, 297 | ":lambda:path/2015-03-31/functions/", 298 | { 299 | "Fn::GetAtt": [ 300 | "ApiAuthorizerFn0AD8B47E", 301 | "Arn", 302 | ], 303 | }, 304 | "/invocations", 305 | ], 306 | ], 307 | }, 308 | "IdentitySource": "method.request.header.Authorization", 309 | "Name": "testApiAuthorizer1FF05C25", 310 | "RestApiId": { 311 | "Ref": "ApiCD79AAA0", 312 | }, 313 | "Type": "REQUEST", 314 | }, 315 | "Type": "AWS::ApiGateway::Authorizer", 316 | }, 317 | "ApiAuthorizerFn0AD8B47E": { 318 | "DependsOn": [ 319 | "ApiAuthorizerFnServiceRole1C6A7C39", 320 | ], 321 | "Properties": { 322 | "Code": { 323 | "S3Bucket": { 324 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 325 | }, 326 | "S3Key": "c216a23c377b518c9770a510e7a4a47d58e54517cfb7c5eb92ca6ce52220d98a.zip", 327 | }, 328 | "Environment": { 329 | "Variables": { 330 | "ASSUMED_ROLE": { 331 | "Fn::GetAtt": [ 332 | "Role1ABCC5F0", 333 | "Arn", 334 | ], 335 | }, 336 | "CLIENT_ID": { 337 | "Ref": "IdentityUserPoolUserPoolClient1035B963", 338 | }, 339 | "USERPOOL_ID": { 340 | "Ref": "IdentityUserPool96FE3B9B", 341 | }, 342 | }, 343 | }, 344 | "Handler": "index.handler", 345 | "LoggingConfig": { 346 | "LogGroup": { 347 | "Ref": "ApiLogGroup1717FE17", 348 | }, 349 | }, 350 | "Role": { 351 | "Fn::GetAtt": [ 352 | "ApiAuthorizerFnServiceRole1C6A7C39", 353 | "Arn", 354 | ], 355 | }, 356 | "Runtime": "nodejs18.x", 357 | "Timeout": 30, 358 | }, 359 | "Type": "AWS::Lambda::Function", 360 | }, 361 | "ApiAuthorizerFnServiceRole1C6A7C39": { 362 | "Properties": { 363 | "AssumeRolePolicyDocument": { 364 | "Statement": [ 365 | { 366 | "Action": "sts:AssumeRole", 367 | "Effect": "Allow", 368 | "Principal": { 369 | "Service": "lambda.amazonaws.com", 370 | }, 371 | }, 372 | ], 373 | "Version": "2012-10-17", 374 | }, 375 | "ManagedPolicyArns": [ 376 | { 377 | "Fn::Join": [ 378 | "", 379 | [ 380 | "arn:", 381 | { 382 | "Ref": "AWS::Partition", 383 | }, 384 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 385 | ], 386 | ], 387 | }, 388 | ], 389 | }, 390 | "Type": "AWS::IAM::Role", 391 | }, 392 | "ApiAuthorizerFntestApiAuthorizer1FF05C25Permissions19B273EE": { 393 | "Properties": { 394 | "Action": "lambda:InvokeFunction", 395 | "FunctionName": { 396 | "Fn::GetAtt": [ 397 | "ApiAuthorizerFn0AD8B47E", 398 | "Arn", 399 | ], 400 | }, 401 | "Principal": "apigateway.amazonaws.com", 402 | "SourceArn": { 403 | "Fn::Join": [ 404 | "", 405 | [ 406 | "arn:", 407 | { 408 | "Ref": "AWS::Partition", 409 | }, 410 | ":execute-api:", 411 | { 412 | "Ref": "AWS::Region", 413 | }, 414 | ":", 415 | { 416 | "Ref": "AWS::AccountId", 417 | }, 418 | ":", 419 | { 420 | "Ref": "ApiCD79AAA0", 421 | }, 422 | "/authorizers/", 423 | { 424 | "Ref": "ApiAuthorizer787544C1", 425 | }, 426 | ], 427 | ], 428 | }, 429 | }, 430 | "Type": "AWS::Lambda::Permission", 431 | }, 432 | "ApiCD79AAA0": { 433 | "Properties": { 434 | "ApiKeySourceType": "AUTHORIZER", 435 | "Name": "TenantIsolationPatterns-Basic", 436 | }, 437 | "Type": "AWS::ApiGateway::RestApi", 438 | }, 439 | "ApiCloudWatchRoleCCA56614": { 440 | "DeletionPolicy": "Delete", 441 | "Properties": { 442 | "AssumeRolePolicyDocument": { 443 | "Statement": [ 444 | { 445 | "Action": "sts:AssumeRole", 446 | "Effect": "Allow", 447 | "Principal": { 448 | "Service": "apigateway.amazonaws.com", 449 | }, 450 | }, 451 | ], 452 | "Version": "2012-10-17", 453 | }, 454 | "ManagedPolicyArns": [ 455 | { 456 | "Fn::Join": [ 457 | "", 458 | [ 459 | "arn:", 460 | { 461 | "Ref": "AWS::Partition", 462 | }, 463 | ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", 464 | ], 465 | ], 466 | }, 467 | ], 468 | }, 469 | "Type": "AWS::IAM::Role", 470 | "UpdateReplacePolicy": "Delete", 471 | }, 472 | "ApiDeploymentFE1E728219e06fec5435ddb5c05d36225cdb3105": { 473 | "DependsOn": [ 474 | "ApiANY29CC0E6B", 475 | ], 476 | "Properties": { 477 | "Description": "Automatically created by the RestApi construct", 478 | "RestApiId": { 479 | "Ref": "ApiCD79AAA0", 480 | }, 481 | }, 482 | "Type": "AWS::ApiGateway::Deployment", 483 | }, 484 | "ApiDeploymentStageprod95EFE650": { 485 | "DependsOn": [ 486 | "ApiAccount6C17A443", 487 | ], 488 | "Properties": { 489 | "AccessLogSetting": { 490 | "DestinationArn": { 491 | "Fn::GetAtt": [ 492 | "ApiLogGroup1717FE17", 493 | "Arn", 494 | ], 495 | }, 496 | "Format": { 497 | "Fn::Join": [ 498 | "", 499 | [ 500 | "{"path":"$context.resourcePath","requestId":"$context.requestId","sourceIp":"$context.identity.sourceIp","method":"$context.httpMethod","authorizerLatency":"$context.authorizer.integrationLatency","integrationLatency":"$context.integrationLatency","responseLatency":"$context.responseLatency","authorizerStatus":"$context.authorizer.status","integrationStatus":"$context.integrationStatus","status":"$context.status","transactionId":"$context.authorizer.transactionId","tenantId":"$context.authorizer.tenantId","tier":"$context.authorizer.tier","role":"$context.authorizer.role","stackName":"", 501 | { 502 | "Ref": "AWS::StackName", 503 | }, 504 | ""}", 505 | ], 506 | ], 507 | }, 508 | }, 509 | "DeploymentId": { 510 | "Ref": "ApiDeploymentFE1E728219e06fec5435ddb5c05d36225cdb3105", 511 | }, 512 | "RestApiId": { 513 | "Ref": "ApiCD79AAA0", 514 | }, 515 | "StageName": "prod", 516 | }, 517 | "Type": "AWS::ApiGateway::Stage", 518 | }, 519 | "ApiLogGroup1717FE17": { 520 | "DeletionPolicy": "Delete", 521 | "Properties": { 522 | "LogGroupName": "/TenantIsolationPatterns/Basic/api", 523 | "RetentionInDays": 1, 524 | }, 525 | "Type": "AWS::Logs::LogGroup", 526 | "UpdateReplacePolicy": "Delete", 527 | }, 528 | "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderHandlerF2C543E0": { 529 | "DependsOn": [ 530 | "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderRole517FED65", 531 | ], 532 | "Properties": { 533 | "Code": { 534 | "S3Bucket": { 535 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 536 | }, 537 | "S3Key": "2926656cdb97b26d98f8b957b0d3f7326b458679745f2817e50333a21767350c.zip", 538 | }, 539 | "Handler": "__entrypoint__.handler", 540 | "MemorySize": 128, 541 | "Role": { 542 | "Fn::GetAtt": [ 543 | "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderRole517FED65", 544 | "Arn", 545 | ], 546 | }, 547 | "Runtime": { 548 | "Fn::FindInMap": [ 549 | "LatestNodeRuntimeMap", 550 | { 551 | "Ref": "AWS::Region", 552 | }, 553 | "value", 554 | ], 555 | }, 556 | "Timeout": 900, 557 | }, 558 | "Type": "AWS::Lambda::Function", 559 | }, 560 | "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderRole517FED65": { 561 | "Properties": { 562 | "AssumeRolePolicyDocument": { 563 | "Statement": [ 564 | { 565 | "Action": "sts:AssumeRole", 566 | "Effect": "Allow", 567 | "Principal": { 568 | "Service": "lambda.amazonaws.com", 569 | }, 570 | }, 571 | ], 572 | "Version": "2012-10-17", 573 | }, 574 | "ManagedPolicyArns": [ 575 | { 576 | "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 577 | }, 578 | ], 579 | "Policies": [ 580 | { 581 | "PolicyDocument": { 582 | "Statement": [ 583 | { 584 | "Action": [ 585 | "iam:CreateOpenIDConnectProvider", 586 | "iam:DeleteOpenIDConnectProvider", 587 | "iam:UpdateOpenIDConnectProviderThumbprint", 588 | "iam:AddClientIDToOpenIDConnectProvider", 589 | "iam:RemoveClientIDFromOpenIDConnectProvider", 590 | ], 591 | "Effect": "Allow", 592 | "Resource": "*", 593 | }, 594 | ], 595 | "Version": "2012-10-17", 596 | }, 597 | "PolicyName": "Inline", 598 | }, 599 | ], 600 | }, 601 | "Type": "AWS::IAM::Role", 602 | }, 603 | "IdentityLogGroup363C12C6": { 604 | "DeletionPolicy": "Delete", 605 | "Properties": { 606 | "LogGroupName": "/TenantIsolationPatterns/Basic/pre-token-generation", 607 | "RetentionInDays": 1, 608 | }, 609 | "Type": "AWS::Logs::LogGroup", 610 | "UpdateReplacePolicy": "Delete", 611 | }, 612 | "IdentityMyOidcProvider7F02079C": { 613 | "DeletionPolicy": "Delete", 614 | "Properties": { 615 | "ClientIDList": [ 616 | { 617 | "Ref": "IdentityUserPoolUserPoolClient1035B963", 618 | }, 619 | ], 620 | "CodeHash": "2926656cdb97b26d98f8b957b0d3f7326b458679745f2817e50333a21767350c", 621 | "ServiceToken": { 622 | "Fn::GetAtt": [ 623 | "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderHandlerF2C543E0", 624 | "Arn", 625 | ], 626 | }, 627 | "Url": { 628 | "Fn::Join": [ 629 | "", 630 | [ 631 | "https://cognito-idp.", 632 | { 633 | "Ref": "AWS::Region", 634 | }, 635 | ".amazonaws.com/", 636 | { 637 | "Ref": "IdentityUserPool96FE3B9B", 638 | }, 639 | ], 640 | ], 641 | }, 642 | }, 643 | "Type": "Custom::AWSCDKOpenIdConnectProvider", 644 | "UpdateReplacePolicy": "Delete", 645 | }, 646 | "IdentityPreTokenGenerationFnCEDC88ED": { 647 | "DependsOn": [ 648 | "IdentityPreTokenGenerationFnServiceRole76C4CAD8", 649 | ], 650 | "Properties": { 651 | "Code": { 652 | "S3Bucket": { 653 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 654 | }, 655 | "S3Key": "d713298c5895331a7a9c16f2dcf3b0b8de7bdae23542bd1f81398fbad74b9d13.zip", 656 | }, 657 | "Handler": "index.handler", 658 | "LoggingConfig": { 659 | "LogGroup": { 660 | "Ref": "IdentityLogGroup363C12C6", 661 | }, 662 | }, 663 | "Role": { 664 | "Fn::GetAtt": [ 665 | "IdentityPreTokenGenerationFnServiceRole76C4CAD8", 666 | "Arn", 667 | ], 668 | }, 669 | "Runtime": "nodejs18.x", 670 | "Timeout": 30, 671 | }, 672 | "Type": "AWS::Lambda::Function", 673 | }, 674 | "IdentityPreTokenGenerationFnServiceRole76C4CAD8": { 675 | "Properties": { 676 | "AssumeRolePolicyDocument": { 677 | "Statement": [ 678 | { 679 | "Action": "sts:AssumeRole", 680 | "Effect": "Allow", 681 | "Principal": { 682 | "Service": "lambda.amazonaws.com", 683 | }, 684 | }, 685 | ], 686 | "Version": "2012-10-17", 687 | }, 688 | "ManagedPolicyArns": [ 689 | { 690 | "Fn::Join": [ 691 | "", 692 | [ 693 | "arn:", 694 | { 695 | "Ref": "AWS::Partition", 696 | }, 697 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 698 | ], 699 | ], 700 | }, 701 | ], 702 | }, 703 | "Type": "AWS::IAM::Role", 704 | }, 705 | "IdentityUserPool96FE3B9B": { 706 | "DeletionPolicy": "Delete", 707 | "Properties": { 708 | "AccountRecoverySetting": { 709 | "RecoveryMechanisms": [ 710 | { 711 | "Name": "verified_phone_number", 712 | "Priority": 1, 713 | }, 714 | { 715 | "Name": "verified_email", 716 | "Priority": 2, 717 | }, 718 | ], 719 | }, 720 | "AdminCreateUserConfig": { 721 | "AllowAdminCreateUserOnly": true, 722 | }, 723 | "AliasAttributes": [ 724 | "email", 725 | ], 726 | "AutoVerifiedAttributes": [ 727 | "email", 728 | ], 729 | "EmailVerificationMessage": "The verification code to your new account is {####}", 730 | "EmailVerificationSubject": "Verify your new account", 731 | "LambdaConfig": { 732 | "PreTokenGenerationConfig": { 733 | "LambdaArn": { 734 | "Fn::GetAtt": [ 735 | "IdentityPreTokenGenerationFnCEDC88ED", 736 | "Arn", 737 | ], 738 | }, 739 | "LambdaVersion": "V2_0", 740 | }, 741 | }, 742 | "Schema": [ 743 | { 744 | "AttributeDataType": "String", 745 | "Mutable": false, 746 | "Name": "tenantId", 747 | "StringAttributeConstraints": { 748 | "MaxLength": "36", 749 | "MinLength": "1", 750 | }, 751 | }, 752 | ], 753 | "SmsVerificationMessage": "The verification code to your new account is {####}", 754 | "UserPoolAddOns": { 755 | "AdvancedSecurityMode": "AUDIT", 756 | }, 757 | "UserPoolName": "TenantIsolationPatterns-Basic", 758 | "VerificationMessageTemplate": { 759 | "DefaultEmailOption": "CONFIRM_WITH_CODE", 760 | "EmailMessage": "The verification code to your new account is {####}", 761 | "EmailSubject": "Verify your new account", 762 | "SmsMessage": "The verification code to your new account is {####}", 763 | }, 764 | }, 765 | "Type": "AWS::Cognito::UserPool", 766 | "UpdateReplacePolicy": "Delete", 767 | }, 768 | "IdentityUserPoolCognitoDomainE237BB31": { 769 | "Properties": { 770 | "Domain": "tenantisolationpatterns-auth", 771 | "UserPoolId": { 772 | "Ref": "IdentityUserPool96FE3B9B", 773 | }, 774 | }, 775 | "Type": "AWS::Cognito::UserPoolDomain", 776 | }, 777 | "IdentityUserPoolPreTokenGenerationConfigCognito1B3039FD": { 778 | "Properties": { 779 | "Action": "lambda:InvokeFunction", 780 | "FunctionName": { 781 | "Fn::GetAtt": [ 782 | "IdentityPreTokenGenerationFnCEDC88ED", 783 | "Arn", 784 | ], 785 | }, 786 | "Principal": "cognito-idp.amazonaws.com", 787 | "SourceArn": { 788 | "Fn::GetAtt": [ 789 | "IdentityUserPool96FE3B9B", 790 | "Arn", 791 | ], 792 | }, 793 | }, 794 | "Type": "AWS::Lambda::Permission", 795 | }, 796 | "IdentityUserPoolUserPoolClient1035B963": { 797 | "Properties": { 798 | "AccessTokenValidity": 1440, 799 | "AllowedOAuthFlows": [ 800 | "implicit", 801 | "code", 802 | ], 803 | "AllowedOAuthFlowsUserPoolClient": true, 804 | "AllowedOAuthScopes": [ 805 | "profile", 806 | "phone", 807 | "email", 808 | "openid", 809 | "aws.cognito.signin.user.admin", 810 | ], 811 | "CallbackURLs": [ 812 | "https://example.com", 813 | ], 814 | "ClientName": "TenantIsolationPatterns-Basic", 815 | "ExplicitAuthFlows": [ 816 | "ALLOW_USER_PASSWORD_AUTH", 817 | "ALLOW_REFRESH_TOKEN_AUTH", 818 | ], 819 | "IdTokenValidity": 1440, 820 | "ReadAttributes": [ 821 | "custom:tenantId", 822 | "email", 823 | ], 824 | "RefreshTokenValidity": 4320, 825 | "SupportedIdentityProviders": [ 826 | "COGNITO", 827 | ], 828 | "TokenValidityUnits": { 829 | "AccessToken": "minutes", 830 | "IdToken": "minutes", 831 | "RefreshToken": "minutes", 832 | }, 833 | "UserPoolId": { 834 | "Ref": "IdentityUserPool96FE3B9B", 835 | }, 836 | "WriteAttributes": [ 837 | "custom:tenantId", 838 | "email", 839 | ], 840 | }, 841 | "Type": "AWS::Cognito::UserPoolClient", 842 | }, 843 | "Role1ABCC5F0": { 844 | "Properties": { 845 | "AssumeRolePolicyDocument": { 846 | "Statement": [ 847 | { 848 | "Action": [ 849 | "sts:AssumeRoleWithWebIdentity", 850 | "sts:TagSession", 851 | ], 852 | "Condition": { 853 | "ForAllValues:StringEquals": { 854 | "cognito-identity.amazonaws.com:aud": { 855 | "Ref": "IdentityUserPoolUserPoolClient1035B963", 856 | }, 857 | }, 858 | }, 859 | "Effect": "Allow", 860 | "Principal": { 861 | "Federated": { 862 | "Ref": "IdentityMyOidcProvider7F02079C", 863 | }, 864 | }, 865 | }, 866 | ], 867 | "Version": "2012-10-17", 868 | }, 869 | "ManagedPolicyArns": [ 870 | { 871 | "Fn::Join": [ 872 | "", 873 | [ 874 | "arn:", 875 | { 876 | "Ref": "AWS::Partition", 877 | }, 878 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 879 | ], 880 | ], 881 | }, 882 | ], 883 | }, 884 | "Type": "AWS::IAM::Role", 885 | }, 886 | }, 887 | "Rules": { 888 | "CheckBootstrapVersion": { 889 | "Assertions": [ 890 | { 891 | "Assert": { 892 | "Fn::Not": [ 893 | { 894 | "Fn::Contains": [ 895 | [ 896 | "1", 897 | "2", 898 | "3", 899 | "4", 900 | "5", 901 | ], 902 | { 903 | "Ref": "BootstrapVersion", 904 | }, 905 | ], 906 | }, 907 | ], 908 | }, 909 | "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", 910 | }, 911 | ], 912 | }, 913 | }, 914 | } 915 | `; 916 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/test/main.test.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import { TenantIsolationPatterns } from '../src/main'; 4 | 5 | test('Snapshot', () => { 6 | const app = new App(); 7 | const stack = new TenantIsolationPatterns(app, 'test'); 8 | 9 | const template = Template.fromStack(stack); 10 | expect(template.toJSON()).toMatchSnapshot(); 11 | }); -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/test_interface.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | STACK_NAME='Shared' 4 | USER_NAME='testuser'$((1 + $RANDOM % 100)) 5 | TENANT_ID='tenant'$((1 + $RANDOM % 10000)) 6 | PASSWORD='Password123!' 7 | API_URL=$(aws cloudformation describe-stacks --stack-name $STACK_NAME --query 'Stacks[0].Outputs[?OutputKey==`ApiUrl`].OutputValue' --output text) 8 | CLIENT_ID=$(aws cloudformation describe-stacks --stack-name $STACK_NAME --query 'Stacks[0].Outputs[?OutputKey==`ClientId`].OutputValue' --output text) 9 | USER_POOL_ID=$(aws cloudformation describe-stacks --stack-name $STACK_NAME --query 'Stacks[0].Outputs[?OutputKey==`UserPoolId`].OutputValue' --output text) 10 | OIDC_PROVIDER='https://'$(aws cloudformation describe-stacks --stack-name $STACK_NAME --query 'Stacks[0].Outputs[?OutputKey==`OidcProvider`].OutputValue' --output text)'/.well-known/openid-configuration' 11 | aws cognito-idp admin-create-user --user-pool-id $USER_POOL_ID --user-attributes Name=custom:tenantId,Value=$TENANT_ID --username $USER_NAME > /dev/null 12 | aws cognito-idp admin-set-user-password --user-pool-id $USER_POOL_ID --username $USER_NAME --password $PASSWORD --permanent 13 | ID_TOKEN=$(aws cognito-idp initiate-auth --auth-flow USER_PASSWORD_AUTH --auth-parameters USERNAME=$USER_NAME,PASSWORD=$PASSWORD --client-id $CLIENT_ID|grep 'IdToken'|cut -d ':' -f 2|cut -d '"' -f 2) 14 | 15 | test_dynamodb() { 16 | curl -X GET -H Authorization:\ Bearer\ $ID_TOKEN $API_URL/dynamodb/success 17 | curl -X GET -H Authorization:\ Bearer\ $ID_TOKEN $API_URL/dynamodb/fail 18 | } 19 | 20 | 21 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2019" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2019" 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts", 31 | ".projenrc.js" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /samples/tenant-isolation-patterns/tsconfig.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "alwaysStrict": true, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "inlineSourceMap": true, 11 | "inlineSources": true, 12 | "lib": [ 13 | "es2019" 14 | ], 15 | "module": "CommonJS", 16 | "noEmitOnError": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "stripInternal": true, 28 | "target": "ES2019" 29 | }, 30 | "include": [ 31 | "src/**/*.ts" 32 | ], 33 | "exclude": [] 34 | } 35 | --------------------------------------------------------------------------------