From 5d9c18840ea7ae8be6f9026f05bbda1f206b7dbf Mon Sep 17 00:00:00 2001 From: Dan R <140449977+drmagic21@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:03:43 -0600 Subject: [PATCH] DKMS Customer API This repository provides a reference implementation for the client's side of Magic's Wallet-as-a-Service Split-Key DKMS solution. Signed-off-by: Dan R <140449977+drmagic21@users.noreply.github.com> --- .github/workflows/deploy.yml | 151 ++++ .github/workflows/pull_requests.yml | 40 ++ .gitignore | 17 + LICENSE | 177 +++++ Makefile | 43 ++ README.md | 55 ++ app.py | 42 ++ cdk.json | 57 ++ deploy/__init__.py | 0 deploy/dkms_api.py | 140 ++++ end-to-end.py | 52 ++ lambdas/dkms_handler/Makefile | 5 + lambdas/dkms_handler/index.py | 184 +++++ lambdas/dkms_handler/poetry.lock | 432 ++++++++++++ lambdas/dkms_handler/pyproject.toml | 21 + lambdas/dkms_handler/tests/__init__.py | 0 lambdas/dkms_handler/tests/conftest.py | 85 +++ .../dkms_handler/tests/test_dkms_handler.py | 248 +++++++ package-lock.json | 53 ++ package.json | 5 + poetry.lock | 667 ++++++++++++++++++ pyproject.toml | 25 + tests/__init__.py | 0 tests/test_dkms_api.py | 63 ++ 24 files changed, 2562 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/pull_requests.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 app.py create mode 100644 cdk.json create mode 100644 deploy/__init__.py create mode 100644 deploy/dkms_api.py create mode 100644 end-to-end.py create mode 100644 lambdas/dkms_handler/Makefile create mode 100644 lambdas/dkms_handler/index.py create mode 100644 lambdas/dkms_handler/poetry.lock create mode 100644 lambdas/dkms_handler/pyproject.toml create mode 100644 lambdas/dkms_handler/tests/__init__.py create mode 100644 lambdas/dkms_handler/tests/conftest.py create mode 100644 lambdas/dkms_handler/tests/test_dkms_handler.py create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/test_dkms_api.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..f85be68 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,151 @@ +name: Deploy API + +on: + push: + branches: + - master + +concurrency: deployment + +permissions: + pull-requests: write # so we can comment on PR + id-token: write # This is required for aws creds requesting the JWT + contents: read # This is required for aws creds actions/checkout + +env: + AWS_ACCOUNT: '276304361801' + AWS_REGION: 'us-west-2' + PYTHON_VERSION: '3.11' + NODE_VERSION: '18' + +jobs: + deploy-dev: + runs-on: ubuntu-latest + environment: dev + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: install poetry + run: | + python -m pip install poetry + + - name: make install + run: | + make install + poetry run npx cdk --version + + - name: configure aws credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT }}:role/github-${{ github.repository_owner }}-${{ github.event.repository.name }}-role + aws-region: ${{ env.AWS_REGION }} + + - name: cdk deploy + run: | + poetry run npx cdk --require-approval never deploy \ + --outputs-file=outputs.json \ + --context jwks_url="https://assets.auth.magic.link/split-key/.well-known/jwks_dev.json" \ + --context env_name=dev \ + --context domain_name=dkms-customer-api.dev.magic.link \ + --context acm_cert_arn="arn:aws:acm:us-west-2:276304361801:certificate/30c64e13-f9f5-4c66-b7f5-65b6a84f50d3" \ + --context cors_allow_origins='*' + + - name: health check + run: | + API_URL=$(jq -r '.[].dkmscustomerapiurl' outputs.json) + curl -s --fail-with-body ${API_URL}healthz + + deploy-stagef: + runs-on: ubuntu-latest + needs: deploy-dev + environment: stagef + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: install poetry + run: | + python -m pip install poetry + + - name: make install + run: | + make install + poetry run npx cdk --version + + - name: configure aws credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT }}:role/github-${{ github.repository_owner }}-${{ github.event.repository.name }}-role + aws-region: ${{ env.AWS_REGION }} + + - name: cdk deploy + run: | + poetry run npx cdk --require-approval never deploy \ + --outputs-file=outputs.json \ + --context jwks_url="https://assets.auth.magic.link/split-key/.well-known/jwks_stagef.json" \ + --context env_name=stagef \ + --context domain_name=dkms-customer-api.stagef.magic.link \ + --context acm_cert_arn="arn:aws:acm:us-west-2:276304361801:certificate/6ce412b4-6b84-4f8d-a775-71c8af130b56" \ + --context cors_allow_origins='*' + + - name: health check + run: | + API_URL=$(jq -r '.[].dkmscustomerapiurl' outputs.json) + curl -s --fail-with-body ${API_URL}healthz + + deploy-prod: + runs-on: ubuntu-latest + needs: deploy-stagef + environment: prod + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: install poetry + run: | + python -m pip install poetry + + - name: make install + run: | + make install + poetry run npx cdk --version + + - name: configure aws credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT }}:role/github-${{ github.repository_owner }}-${{ github.event.repository.name }}-role + aws-region: ${{ env.AWS_REGION }} + + - name: cdk deploy + run: | + poetry run npx cdk --require-approval never deploy \ + --outputs-file=outputs.json \ + --context domain_name=dkms-customer-api.magic.link \ + --context acm_cert_arn="arn:aws:acm:us-west-2:276304361801:certificate/f278cd4d-e846-4063-bb00-bd15c382bb41" + + - name: health check + run: | + API_URL=$(jq -r '.[].dkmscustomerapiurl' outputs.json) + curl -s --fail-with-body ${API_URL}healthz diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml new file mode 100644 index 0000000..501c573 --- /dev/null +++ b/.github/workflows/pull_requests.yml @@ -0,0 +1,40 @@ +name: Test Pull Request + +on: + pull_request: + branches: + +env: + PYTHON_VERSION: '3.11' + NODE_VERSION: '18' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: install poetry + run: | + python -m pip install poetry + + - name: make install + run: | + make install + poetry run npx cdk --version + + - name: make + run: | + make test-all + + - name: cdk synth + run: | + make synth diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..19c641a --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Python +__pycache__ + +# OS X +.history +.DS_Store + +# vim +*.swp + +# npm +node_modules/ + +# cdk +cdk.out/ + +.install diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7335bc8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..22ade87 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +test-all: test-cdk test-dkms-lambda + +PRUN = poetry run + +.PHONY: test-cdk +test-cdk: + $(PRUN) pytest -x tests/ + +.PHONY: test-dkms-lambda +test-dkms-lambda: + cd lambdas/dkms_handler && make test + +.PHONY: black +black: + $(PRUN) black . + +.PHONY: synth +synth: + $(PRUN) npx cdk synth + +.PHONY: diff +diff: + $(PRUN) npx cdk diff + +.PHONY: list +list: + $(PRUN) npx cdk list + +.PHONY: deploy +deploy: + $(PRUN) npx cdk deploy + +.PHONY: install +install: .install +.install: + npm install + poetry install --no-root + cd lambdas/dkms_handler && poetry install --no-root + touch .install + +.PHONY: clean +clean: + rm -rf node_modules cdk_out .install diff --git a/README.md b/README.md new file mode 100644 index 0000000..89405c9 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# DKMS Customer API +Magic offers a Wallet-as-a-Service solution, enabling web or mobile application developers to seamlessly integrate web3 wallets into their apps with a familiar web2 user experience. This is achieved through a variety of passwordless authentication mechanisms. The cornerstone of Magic’s offering is the patented Delegated Key Management Infrastructure (DKMS), detailed further [here](https://magic.link/docs/home/security/product-security#hardware-security-modules-hs-ms). + +This repository introduces a novel shared security model for application developers seeking greater control over how Magic manages user private keys at runtime. Specifically, Magic introduces a shared security model where customers can deploy a cloud-native, elastic infrastructure to encrypt segments of the private key, divided using the Shamir Secret Sharing Algorithm. This infrastructure seamlessly integrates with Magic’s flagship DKMS, fortifying the entire offering. We refer to this enhanced architecture as Split-Key DKMS. + +Below is a conceptual diagram illustrating how this model operates. For more in-depth information on the Split-Key DKMS offering, refer to **this link**. + +Screenshot 2023-11-28 at 1 49 21 PM + + +# **Getting Started** + +Below outlines the expected workflow for developers participating in the Split-Key DKMS offering: + +1. Opt-in for Split-Key DKMS when opening your developer account with Magic. Note that, currently, this feature is in invite-only mode; therefore, please contact customer service to enable this functionality. +2. Fork this repository, make necessary modifications, and deploy it to your AWS account. The tech stack is optimized for AWS Serverless Architecture, ensuring easy scalability. +3. Register the endpoints with Magic to receive callbacks for encryption and decryption at runtime when users sign up and perform transactions. + +## **Requirements** + +- Sign up for a cloud vendor; currently, we support Amazon Web Services (AWS). + +## **License** + +- We have open-sourced this repository under the Apache 2.0 license, making it suitable for modification to fit the unique requirements of your production environment. View the license [here](https://github.com/magiclabs/dkms-customer-api/blob/master/LICENSE). + +## **Maintenance and Support for Versions** + +- Currently we support AWS as the default cloud providers, we are looking to integrate this offering with other cloud providers such as Google Cloud and Microsoft Azure + +## **Installation** + +We use AWS CDK for this repository. The following commands will help you deploy the CDK to your AWS account. + +```jsx +make install +make synth +make diff +make deploy +``` + +## **Configuration** + +- The reference implementation of the CDK comes with a bring-your-own KMS model for encrypting and decrypting your share of the key. +- You are welcome to customize the configuration further to suit your needs, such as using an external HSM in place of AWS KMS. + +# **Getting Help** + +- Reach out to Magic customer support for assistance. + +# **More Resources** + +## **Documentation** + +- [AWS CDK API v2 Documentation](https://docs.aws.amazon.com/cdk/api/v2/) diff --git a/app.py b/app.py new file mode 100644 index 0000000..b057d2d --- /dev/null +++ b/app.py @@ -0,0 +1,42 @@ +import aws_cdk as cdk +from deploy.dkms_api import DKMSCustomerAPIStack + +app = cdk.App() + +# Set environment. Defaults to "prod". +# example: "cdk synth --context env_name=dev" +env_name = app.node.try_get_context("env_name") or "prod" + +# Set jwks url. Defaults to Magic production. +# example: "cdk synth --context jwks_url=https://assets.auth.magic.link/split-key/.well-known/jwks.json" +jwks_url = ( + app.node.try_get_context("jwks_url") + or "https://assets.auth.magic.link/split-key/.well-known/jwks.json" +) + +# Set optional domain name for the API +domain_name = app.node.try_get_context("domain_name") +acm_cert_arn = app.node.try_get_context("acm_cert_arn") +# If a domain name is provided, an ACM certificate must also be provided +if domain_name is not None: + assert ( + acm_cert_arn is not None + ), "If a domain name is provided, an ACM certificate must also be provided" + +# Set cors allow_origins. Defaults to Magic production. +# example: "cdk synth --context cors_allow_origins='*'" +cors_allow_origins = ( + app.node.try_get_context("cors_allow_origins") or "https://auth.magic.link" +) + + +DKMSCustomerAPIStack( + app, + f"dkms-customer-api-{env_name}", + env_name=env_name, + jwks_url=jwks_url, + cors_allow_origins=cors_allow_origins, + domain_name=domain_name, + acm_cert_arn=acm_cert_arn, +) +app.synth() diff --git a/cdk.json b/cdk.json new file mode 100644 index 0000000..39ff459 --- /dev/null +++ b/cdk.json @@ -0,0 +1,57 @@ +{ + "app": "python3 app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "python/__pycache__", + "tests" + ] + }, + "versionReporting": false, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true + } + +} diff --git a/deploy/__init__.py b/deploy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deploy/dkms_api.py b/deploy/dkms_api.py new file mode 100644 index 0000000..07d696e --- /dev/null +++ b/deploy/dkms_api.py @@ -0,0 +1,140 @@ +from aws_cdk import ( + CfnOutput, + Duration, + RemovalPolicy, + Stack, + aws_lambda as lambda_, + aws_kms as kms, + aws_certificatemanager as acm, +) +from constructs import Construct + +from aws_cdk.aws_lambda_python_alpha import PythonFunction +import aws_cdk.aws_apigatewayv2_alpha as apigwv2 +from aws_cdk.aws_apigatewayv2_integrations_alpha import ( + HttpLambdaIntegration, +) + + +class DKMSCustomerAPIStack(Stack): + """Deploy a stack containing an API endpoint for DKMS.""" + + def __init__( + self, + scope: Construct, + construct_id: str, + env_name: str, + jwks_url: str, + cors_allow_origins: str, + domain_name: str = None, + acm_cert_arn: str = None, + **kwargs, + ) -> None: + """Initialize the stack.""" + super().__init__(scope, construct_id, **kwargs) + self.env_name = env_name + self.jwks_url = jwks_url + self.domain_name = domain_name + self.acm_cert_arn = acm_cert_arn + self.cors_allow_origins = cors_allow_origins + + # Create a KMS key + self.kms_key = self.deploy_kms_key() + + # Create the lambda function that will handle API requests + self.dkms_lambda = self.deploy_dkms_lambda() + + # Grant the lambda permission to use the kms key + self.kms_key.grant_encrypt_decrypt(self.dkms_lambda) + + # Create an API Gateway V2 API + self.dkms_api = self.deploy_dkms_api() + + # Output the API URL + CfnOutput( + self, + id="dkms-customer-api-url", + value=self.dkms_api.url, + description="DKMS Customer API URL", + ) + + def deploy_kms_key(self) -> kms.Key: + """Create a KMS key for encrypting and decrypting customer data.""" + return kms.Key( + self, + id="dkms-customer-key", + alias=f"dkms-customer-key-{self.env_name}", + description="Key for encrypting and decrypting customer data", + removal_policy=RemovalPolicy.RETAIN, + ) + + def deploy_dkms_lambda(self) -> lambda_.Function: + """Create a lambda function to handle API requests.""" + return PythonFunction( + self, + id=f"magic-dkms-customer-endpoint-{self.env_name}", + function_name=f"magic-dkms-customer-endpoint-{self.env_name}", + entry="lambdas/dkms_handler", + runtime=lambda_.Runtime.PYTHON_3_11, + handler="handler", + timeout=Duration.seconds(30), + memory_size=128, + environment={ + "DKMS_KMS_KEY_ID": self.kms_key.key_id, + "JWKS_URL": self.jwks_url, + }, + ) + + def deploy_dkms_api(self) -> apigwv2.HttpApi: + """Create an API Gateway V2 API for the DKMS customer endpoint.""" + default_domain_mapping = None + if self.domain_name is not None: + dn = apigwv2.DomainName( + self, + "dkms-customer-api-domain", + domain_name=self.domain_name, + certificate=acm.Certificate.from_certificate_arn( + self, "dkms-acm-cert", self.acm_cert_arn + ), + ) + default_domain_mapping = apigwv2.DomainMappingOptions( + domain_name=dn, + ) + + dkms_default_integration = HttpLambdaIntegration( + "magic-dkms-customer-endpoint-lambda", + handler=self.dkms_lambda, + ) + dkms_api = apigwv2.HttpApi( + self, + "dkms-customer-api", + api_name=f"dkms-customer-api-{self.env_name}", + default_domain_mapping=default_domain_mapping, + cors_preflight=apigwv2.CorsPreflightOptions( + allow_headers=["Authorization"], + allow_methods=[ + apigwv2.CorsHttpMethod.GET, + apigwv2.CorsHttpMethod.HEAD, + apigwv2.CorsHttpMethod.OPTIONS, + apigwv2.CorsHttpMethod.POST, + ], + allow_origins=[self.cors_allow_origins], + max_age=Duration.days(10), + ), + ) + dkms_api.add_routes( + path="/healthz", + methods=[apigwv2.HttpMethod.GET], + integration=dkms_default_integration, + ) + dkms_api.add_routes( + path="/encrypt", + methods=[apigwv2.HttpMethod.POST], + integration=dkms_default_integration, + ) + dkms_api.add_routes( + path="/decrypt", + methods=[apigwv2.HttpMethod.POST], + integration=dkms_default_integration, + ) + return dkms_api diff --git a/end-to-end.py b/end-to-end.py new file mode 100644 index 0000000..de1448a --- /dev/null +++ b/end-to-end.py @@ -0,0 +1,52 @@ +import json +import os +import requests + +BASE_URL = os.getenv("BASE_URL") +assert ( + BASE_URL is not None +), "BASE_URL environment variable must be set to a value like 'https://oon4ztxwte.execute-api.us-west-2.amazonaws.com/'" + +JWT = os.getenv("JWT") +assert JWT is not None, "JWT environment variable must be set" + + +def test_healthz(): + url = f"{BASE_URL}/healthz" + response = requests.get(url) + assert response.status_code == 200 + assert response.json() == { + "data": {}, + "error_code": "", + "message": "", + "status": "OK", + } + + +def test_encrypt(plaintext: str) -> str: + url = f"{BASE_URL}/encrypt" + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {JWT}"} + data = json.dumps({"plaintext": plaintext}) + response = requests.post(url, headers=headers, data=data) + response.raise_for_status() + assert "ciphertext" in response.json()["data"] + return response.json()["data"]["ciphertext"] + + +def test_decrypt(ciphertext: str) -> str: + url = f"{BASE_URL}/decrypt" + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {JWT}"} + data = json.dumps({"ciphertext": ciphertext}) + response = requests.post(url, headers=headers, data=data) + response.raise_for_status() + assert "plaintext" in response.json()["data"] + return response.json()["data"]["plaintext"] + + +if __name__ == "__main__": + test_healthz() + input = "abracadabra" + ciphertext = test_encrypt(input) + output = test_decrypt(ciphertext) + assert input == output + print(f"Success: {input} -> {ciphertext} -> {output}") diff --git a/lambdas/dkms_handler/Makefile b/lambdas/dkms_handler/Makefile new file mode 100644 index 0000000..02ac28f --- /dev/null +++ b/lambdas/dkms_handler/Makefile @@ -0,0 +1,5 @@ +all: test + +.PHONY: test +test: + poetry run pytest diff --git a/lambdas/dkms_handler/index.py b/lambdas/dkms_handler/index.py new file mode 100644 index 0000000..51ea850 --- /dev/null +++ b/lambdas/dkms_handler/index.py @@ -0,0 +1,184 @@ +import base64 +import boto3 +import json +import jwt +import logging +import os +import traceback +import urllib.request +from http import HTTPStatus + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +kms_key_id = os.getenv("DKMS_KMS_KEY_ID", None) +assert kms_key_id is not None, "DKMS_KMS_KEY_ID environment variable must be set" +kms_client = boto3.client("kms") + +jwks_url = os.getenv("JWKS_URL", None) +assert jwks_url is not None, "JWKS_URL environment variable must be set" +jwks_client = jwt.PyJWKClient(jwks_url) + + +class AuthenticationError(Exception): + """Raised when authentication fails.""" + + pass + + +def handler(event, context) -> dict: + """Process an API Gateway event and return a response.""" + + log_event = event.copy() + log_event["body"] = "omitted" # Do not log potentially sensitive data + logger.info(log_event) + try: + return router(event) + except AuthenticationError as e: + return return_handler( + message="Access Denied", + status=HTTPStatus.UNAUTHORIZED, + error_code="ACCESS_DENIED", + ) + except Exception as e: + logger.info(traceback.format_exc()) + return return_handler( + message="an unknown error occurred", + status=HTTPStatus.INTERNAL_SERVER_ERROR, + error_code="UNKNOWN_ERROR", + ) + + +def authenticate(event) -> dict: + """Authenticate the request.""" + auth_header = event["headers"].get("authorization", None) + logger.info(auth_header) + if not auth_header: + raise AuthenticationError("No Authorization header provided") + + token = auth_header.split("Bearer ")[1] + signing_key = jwks_client.get_signing_key_from_jwt(token) + payload = jwt.decode(token, signing_key.key, algorithms=["RS256"]) + logger.info(payload) + assert payload.get("ewi"), "No ewi provided in JWT" + return payload + + +def router(event) -> dict: + """Route the API request to the correct handler.""" + http_method = event["requestContext"]["http"]["method"] + path = event["rawPath"] + + if http_method == "GET" and path == "/healthz": + return return_handler(status=HTTPStatus.OK) + elif http_method == "POST" and path == "/encrypt": + payload = authenticate(event) + return encrypt( + event["body"], + kms_key_id, + encryption_context={"ewi": payload.get("ewi")}, + ) + elif http_method == "POST" and path == "/decrypt": + payload = authenticate(event) + return decrypt( + event["body"], + kms_key_id, + encryption_context={"ewi": payload.get("ewi")}, + ) + + return return_handler( + message=f"path {path} not found", + status=HTTPStatus.NOT_FOUND, + error_code="INVALID_PATH", + ) + + +def encrypt(body: str, kms_key_id: str, encryption_context: dict) -> dict: + """Handle an encrypt request.""" + if body is None: + return return_handler( + status=HTTPStatus.BAD_REQUEST, message="no body", error_code="INVALID_INPUT" + ) + try: + parsed_body = json.loads(body) + except json.decoder.JSONDecodeError: + return return_handler( + status=HTTPStatus.BAD_REQUEST, + message="invalid json in body", + error_code="INVALID_INPUT", + ) + if parsed_body.get("plaintext") is None: + return return_handler( + status=HTTPStatus.BAD_REQUEST, + message="no plaintext provided", + error_code="INVALID_INPUT", + ) + + response = kms_client.encrypt( + KeyId=kms_key_id, + EncryptionContext=encryption_context, + Plaintext=parsed_body.get("plaintext"), + ) + encrypted_response = base64.b64encode(response["CiphertextBlob"]).decode("utf-8") + return return_handler(status=HTTPStatus.OK, data={"ciphertext": encrypted_response}) + + +def decrypt(body: str, kms_key_id: str, encryption_context: dict) -> dict: + """Handle a decrypt request.""" + if body is None: + return return_handler( + status=HTTPStatus.BAD_REQUEST, message="no body", error_code="INVALID_INPUT" + ) + try: + parsed_body = json.loads(body) + except json.decoder.JSONDecodeError: + return return_handler( + status=HTTPStatus.BAD_REQUEST, + message="invalid json in body", + error_code="INVALID_INPUT", + ) + if parsed_body.get("ciphertext") is None: + return return_handler( + status=HTTPStatus.BAD_REQUEST, + message="no ciphertext provided", + error_code="INVALID_INPUT", + ) + + decoded_payload = base64.b64decode(parsed_body["ciphertext"]) + response = kms_client.decrypt( + KeyId=kms_key_id, + EncryptionContext=encryption_context, + CiphertextBlob=decoded_payload, + ) + decrypted_data = response["Plaintext"].decode("utf-8") + return return_handler(status=HTTPStatus.OK, data={"plaintext": decrypted_data}) + + +def return_handler( + data: dict = {}, + error_code: str = "", + message: str = "", + status: HTTPStatus = HTTPStatus.OK, +) -> dict: + """Return a standard data structure for API Gateway responses""" + logger.info( + { + "data": "omitted", # Do not log potentially sensitive data + "error_code": error_code, + "message": message, + "status": status.name, + "statusCode": status.value, + } + ) + return { + "statusCode": status.value, + "headers": {"Content-Type": "application/json"}, + "body": json.dumps( + { + "data": data, + "error_code": error_code, + "message": message, + "status": status.name, + } + ), + } diff --git a/lambdas/dkms_handler/poetry.lock b/lambdas/dkms_handler/poetry.lock new file mode 100644 index 0000000..4ab9b08 --- /dev/null +++ b/lambdas/dkms_handler/poetry.lock @@ -0,0 +1,432 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "boto3" +version = "1.29.5" +description = "The AWS SDK for Python" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "boto3-1.29.5-py3-none-any.whl", hash = "sha256:030b0f0faf8d44f97e67a5411644243482f33ebf1c45338bb40662239a16dda4"}, + {file = "boto3-1.29.5.tar.gz", hash = "sha256:76fc6a17781c27558c526e899579ccf530df10eb279261fe7800540f0043917e"}, +] + +[package.dependencies] +botocore = ">=1.32.5,<1.33.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.7.0,<0.8.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.32.5" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">= 3.7" +files = [ + {file = "botocore-1.32.5-py3-none-any.whl", hash = "sha256:b8960c955ba275915bf022c54c896c2dac1038289d8a5ace92d1431257c0a439"}, + {file = "botocore-1.32.5.tar.gz", hash = "sha256:75a68f942cd87baff83b3a20dfda11b3aeda48aad32e4dcd6fe8992c0cb0e7db"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""} + +[package.extras] +crt = ["awscrt (==0.19.12)"] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "41.0.6" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c"}, + {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d"}, + {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c"}, + {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596"}, + {file = "cryptography-41.0.6-cp37-abi3-win32.whl", hash = "sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660"}, + {file = "cryptography-41.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4"}, + {file = "cryptography-41.0.6.tar.gz", hash = "sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "deprecated" +version = "1.2.14" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "jwcrypto" +version = "1.5.0" +description = "Implementation of JOSE Web standards" +optional = false +python-versions = ">= 3.6" +files = [ + {file = "jwcrypto-1.5.0.tar.gz", hash = "sha256:2c1dc51cf8e38ddf324795dfe9426dee9dd46caf47f535ccbc18781fba810b8d"}, +] + +[package.dependencies] +cryptography = ">=3.4" +deprecated = "*" + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pyjwt" +version = "2.8.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, + {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "s3transfer" +version = "0.7.0" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "s3transfer-0.7.0-py3-none-any.whl", hash = "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a"}, + {file = "s3transfer-0.7.0.tar.gz", hash = "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e"}, +] + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "urllib3" +version = "2.0.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "ee233a388a27196ee79981b041baaa20b70cc6833984d8fa17de51bff79ed08c" diff --git a/lambdas/dkms_handler/pyproject.toml b/lambdas/dkms_handler/pyproject.toml new file mode 100644 index 0000000..213f3a5 --- /dev/null +++ b/lambdas/dkms_handler/pyproject.toml @@ -0,0 +1,21 @@ +[tool.poetry] +name = "dkms-handler" +version = "0.1.0" +description = "Handle DKMS customer API requests" +authors = ["Magic Labs "] +license = "Apache-2.0" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +boto3 = "^1.29.5" +pyjwt = {extras = ["crypto"], version = "^2.8.0"} + + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.3" +jwcrypto = "^1.5.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/lambdas/dkms_handler/tests/__init__.py b/lambdas/dkms_handler/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lambdas/dkms_handler/tests/conftest.py b/lambdas/dkms_handler/tests/conftest.py new file mode 100644 index 0000000..4bf37cf --- /dev/null +++ b/lambdas/dkms_handler/tests/conftest.py @@ -0,0 +1,85 @@ +import jwt +import json +from jwcrypto import jwk +import os +import pytest +from unittest.mock import patch, MagicMock + +os.environ["DKMS_KMS_KEY_ID"] = "fc2039ff-c159-4467-9dee-39baebc10194" +os.environ["AWS_DEFAULT_REGION"] = "us-west-2" +os.environ[ + "JWKS_URL" +] = "https://example.com/.well-known/jwks.json" # this can be anything because we mock the response in a fixture + +# openssl genrsa -out private_key.pem 2048 +test_private_key = """-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDsKDYU4UiGp4fc +IIRqV431vWPVk1WT5x+q5OQj8E6F5l3uZvzlJpQ2NF3U0/GpUVyYjtyj8CLD4ZvV +pPA0tboSJkqH3jIm+mm0Hi4asQAEVaB6/bIoUQs59xRdp10kxgHENE0J5Zn0yCXz +TFHQ7pzOIvusuZ+8XILMTSag85bB/nLyOaK5Pxb9YCqqwptpwhff2tdXUo8Hm4xM +PolKANMcusarKWg4iYF3308SOQgeM5ZpD/7+yuME0LNSCV5c/NrMAfvvtiWsMY7w +4ucCVITp04N0vbE0irAVB2ucE+bLJUiy0/f9KrP+YkJrv/a2xnPfojUT+QiJ0Rfh +KgpjnH5xAgMBAAECggEAFXRpxWfaMPGTdDo4DXk62nKEWWjzQ2aiB+KXn3Q7jgqp +yfjtTNw+ZtZHGAjRUbKkmO+RuAse/XDHuZcsg31nFDMKXmGfaM8jP0vmoGIoQDyP +Qd0+jE8gl/mMjh2gZrDehDbEMPv9CrIMUJhEbpjfAhNHjh+nFXPKJkl0EvdOYP1S +PpC+LDBBo+JdhYCgQDgxUt4HTsqh09MnlB6ycWuMAC+XQ0ndoqJaOdeDh504fXsA +gBlj9VFC3QHQafMwuZ6E8SHtZyQV2RpZ8McV6NMjbsedCiIacPweAxrpA7xh9/Tx +WNW8BmfbQ6qI6psNlT6Mcru4Ed1+fktT1WXxpv2s4QKBgQD8uv7KyPpH7YAizVDj +PMooU2OMPtnK/BYj0wj1M5HeYl0NgJr6c561TdQ7CrYoFHRm1DGiMUUj/O8q10c6 +BwmxLsUzf9fU3xBV2Ajp/Dr4ZoMqtejhWOjSw2r+/HPVbcjEvNZUAhkobkZ7/9g6 +F2Q2WST7llx8mnRB8InGD1KkLQKBgQDvNlPV67FWwJb2agkpvEySdYioBLIup/xG +iZ89OuUMeTOGTonq3fEJugGPYVdbwFS6U4TXe9zvWNtd1luGExSA6WO27z7XwjM8 +kwC90reXnAjlAqFdGNXC3dNITo9l4X4rI91FNfqDagW8ZbcOj44yHoRT0oFFhCM3 +FJSc3IeZ1QKBgQDwgoQ3N0v3Z22psPppRlCcT79MmANryLrJHOxJbOpEWBd14g2a +iq1enNJ73ZW8Trr3oLgbQggqV2rDultuPYRbucaxW9hqHF3PU+gnxIHaIrRw0Ozu +h04KRS5tupIBapjFoW/WQqjucQNivfdoURptHiizxEP/0H0Sw3ZZpftfgQKBgQCO +v8fVv7nrQDCWSf6/1iuHtvXe9jZymzJz0YqiWnP3NpilzFaHPvypRkPKEVe1XBfz +vQVoJfVZK5h07gdeAiLZLu2fbDP/Q1eaDUuC+60tnyK7rw8mZDyj9gYwfxkZvi+x +hMx1kdm19F4J6FUOLmK3y/hBoTwdhNYS94gb94pAJQKBgQDq5cm5U3uhqmH3dx8h +JkcMGY+YESODC3H1MuX177AkkSm9gGg1HGSU9QR64SVUthm0AZSY7Nzyq8Tql0Ar +uAcEfSkqqLds9f8qCgx4dnA3RHz86Od0duQMmRomlPFvxoyMXuSpOFFa2FASdcdZ +2XkkxrNLuFc2AmJlYYPyiXOx1g== +-----END PRIVATE KEY----- +""" + +# openssl rsa -in private_key.pem -pubout -out public_key.pem +test_public_key = """-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7Cg2FOFIhqeH3CCEaleN +9b1j1ZNVk+cfquTkI/BOheZd7mb85SaUNjRd1NPxqVFcmI7co/Aiw+Gb1aTwNLW6 +EiZKh94yJvpptB4uGrEABFWgev2yKFELOfcUXaddJMYBxDRNCeWZ9Mgl80xR0O6c +ziL7rLmfvFyCzE0moPOWwf5y8jmiuT8W/WAqqsKbacIX39rXV1KPB5uMTD6JSgDT +HLrGqyloOImBd99PEjkIHjOWaQ/+/srjBNCzUgleXPzazAH777YlrDGO8OLnAlSE +6dODdL2xNIqwFQdrnBPmyyVIstP3/Sqz/mJCa7/2tsZz36I1E/kIidEX4SoKY5x+ +cQIDAQAB +-----END PUBLIC KEY----- +""" + +key = jwk.JWK.from_pem(test_public_key.encode()) +jwks_dict = key.export_public(as_dict=True) + + +@pytest.fixture(autouse=True) +def jwks_provider(): + """Mock the jwks provider.""" + with patch("urllib.request.urlopen") as mock_urlopen: + # Mock response object to mimic the urlopen response + mock_response = MagicMock() + mock_response.read.return_value = json.dumps({"keys": [jwks_dict]}).encode( + "utf-8" + ) + mock_response.__enter__.return_value = mock_response # Handle context manager + + # Set this mock response as the return value of urlopen + mock_urlopen.return_value = mock_response + yield + + +@pytest.fixture +def user_jwt(): + """Generate and return a valid JWT signed by test_private_key.""" + yield jwt.encode( + {"sub": "test_user", "ewi": "abcd1234"}, + test_private_key, + algorithm="RS256", + headers={"kid": jwks_dict["kid"]}, + ) diff --git a/lambdas/dkms_handler/tests/test_dkms_handler.py b/lambdas/dkms_handler/tests/test_dkms_handler.py new file mode 100644 index 0000000..91935d5 --- /dev/null +++ b/lambdas/dkms_handler/tests/test_dkms_handler.py @@ -0,0 +1,248 @@ +import base64 +import json +import os +from unittest.mock import patch +from http import HTTPStatus + +import index + + +def test_lambda_dkms_healthz(): + event = { + "rawPath": "/healthz", + "requestContext": {"http": {"method": "GET"}}, + } + context = None + + response = index.handler(event, context) + assert response == { + "statusCode": 200, + "headers": {"Content-Type": "application/json"}, + "body": '{"data": {}, "error_code": "", "message": "", "status": "OK"}', + } + + +def test_router_healthz(): + with patch("index.return_handler") as mock_return_handler: + event = { + "rawPath": "/healthz", + "requestContext": {"http": {"method": "GET"}}, + } + index.router(event) + mock_return_handler.assert_called_once_with(status=HTTPStatus.OK) + + +def test_router_encrypt(user_jwt): + with patch("index.encrypt") as mock_encrypt: + event = { + "rawPath": "/encrypt", + "requestContext": {"http": {"method": "POST"}}, + "headers": { + "Content-Type": "application/json", + "authorization": f"Bearer {user_jwt}", + }, + "body": "test_data", + } + index.router(event) + mock_encrypt.assert_called_once_with( + "test_data", + os.getenv("DKMS_KMS_KEY_ID"), + encryption_context={"ewi": "abcd1234"}, + ) + + +def test_router_decrypt(user_jwt): + with patch("index.decrypt") as mock_decrypt: + event = { + "rawPath": "/decrypt", + "requestContext": {"http": {"method": "POST"}}, + "headers": { + "Content-Type": "application/json", + "authorization": f"Bearer {user_jwt}", + }, + "body": "test_data", + } + index.router(event) + mock_decrypt.assert_called_once_with( + "test_data", + os.getenv("DKMS_KMS_KEY_ID"), + encryption_context={"ewi": "abcd1234"}, + ) + + +def test_router_invalid_path(): + with patch("index.return_handler") as mock_return_handler: + event = { + "rawPath": "/invalid", + "headers": {"Content-Type": "application/json"}, + "requestContext": {"http": {"method": "GET"}}, + } + index.router(event) + mock_return_handler.assert_called_once_with( + message=f"path /invalid not found", + status=HTTPStatus.NOT_FOUND, + error_code="INVALID_PATH", + ) + + +def test_encrypt_with_no_body(): + with patch("index.return_handler") as mock_return_handler: + result = index.encrypt(None, "mock_kms_key_id", encryption_context={}) + mock_return_handler.assert_called_once_with( + status=HTTPStatus.BAD_REQUEST, message="no body", error_code="INVALID_INPUT" + ) + + +def test_encrypt_with_invalid_json(): + with patch("index.return_handler") as mock_return_handler: + result = index.encrypt("invalid json", "mock_kms_key_id", encryption_context={}) + mock_return_handler.assert_called_once_with( + status=HTTPStatus.BAD_REQUEST, + message="invalid json in body", + error_code="INVALID_INPUT", + ) + + +def test_encrypt_with_no_plaintext(): + with patch("index.return_handler") as mock_return_handler: + result = index.encrypt(json.dumps({}), "mock_kms_key_id", encryption_context={}) + mock_return_handler.assert_called_once_with( + status=HTTPStatus.BAD_REQUEST, + message="no plaintext provided", + error_code="INVALID_INPUT", + ) + + +def test_encrypt_success(): + kms_response_mock = {"CiphertextBlob": b"encrypted"} + with patch("index.return_handler") as mock_return_handler, patch( + "index.kms_client.encrypt", return_value=kms_response_mock + ) as mock_encrypt: + result = index.encrypt( + json.dumps({"plaintext": "secret"}), + "mock_kms_key_id", + encryption_context={"ewi": "abcd1234"}, + ) + mock_encrypt.assert_called_once_with( + KeyId="mock_kms_key_id", + EncryptionContext={"ewi": "abcd1234"}, + Plaintext="secret", + ) + mock_return_handler.assert_called_once() # Assert it was called, but not concerned with the exact args here + + +def test_decrypt_with_no_body(): + with patch("index.return_handler") as mock_return_handler: + result = index.decrypt(None, "mock_kms_key_id", encryption_context={}) + mock_return_handler.assert_called_once_with( + status=HTTPStatus.BAD_REQUEST, message="no body", error_code="INVALID_INPUT" + ) + + +def test_decrypt_with_invalid_json(): + with patch("index.return_handler") as mock_return_handler: + result = index.decrypt("invalid json", "mock_kms_key_id", encryption_context={}) + mock_return_handler.assert_called_once_with( + status=HTTPStatus.BAD_REQUEST, + message="invalid json in body", + error_code="INVALID_INPUT", + ) + + +def test_decrypt_with_no_ciphertext(): + with patch("index.return_handler") as mock_return_handler: + result = index.decrypt(json.dumps({}), "mock_kms_key_id", encryption_context={}) + mock_return_handler.assert_called_once_with( + status=HTTPStatus.BAD_REQUEST, + message="no ciphertext provided", + error_code="INVALID_INPUT", + ) + + +def test_decrypt_success(): + ciphertext = "encrypted data" + encoded_ciphertext = base64.b64encode(ciphertext.encode("utf-8")).decode("utf-8") + body = json.dumps({"ciphertext": encoded_ciphertext}) + kms_response_mock = {"Plaintext": b"decrypted data"} + + with patch("index.return_handler") as mock_return_handler, patch( + "index.kms_client.decrypt", return_value=kms_response_mock + ) as mock_decrypt: + result = index.decrypt( + body, "mock_kms_key_id", encryption_context={"ewi": "abcd1234"} + ) + mock_decrypt.assert_called_once_with( + KeyId="mock_kms_key_id", + EncryptionContext={"ewi": "abcd1234"}, + CiphertextBlob=ciphertext.encode("utf-8"), + ) + mock_return_handler.assert_called_once() # Assert it was called, but not concerned with the exact args here + + +def test_return_handler_success(): + with patch("index.logger") as mock_logger: + response = index.return_handler(data={"key": "value"}, status=HTTPStatus.OK) + + # Check if the response is correctly structured + assert response["statusCode"] == HTTPStatus.OK.value + assert response["headers"]["Content-Type"] == "application/json" + assert json.loads(response["body"]) == { + "data": {"key": "value"}, + "error_code": "", + "message": "", + "status": "OK", + } + + # Check if the log is correct + mock_logger.info.assert_called_once_with( + { + "data": "omitted", + "error_code": "", + "message": "", + "status": "OK", + "statusCode": 200, + } + ) + + +def test_return_handler_error(): + with patch("index.logger") as mock_logger: + response = index.return_handler( + error_code="ERROR_CODE", + message="Error message", + status=HTTPStatus.BAD_REQUEST, + ) + + # Check if the response is correctly structured + assert response["statusCode"] == HTTPStatus.BAD_REQUEST.value + assert json.loads(response["body"]) == { + "data": {}, + "error_code": "ERROR_CODE", + "message": "Error message", + "status": "BAD_REQUEST", + } + + # Check if the log is correct + mock_logger.info.assert_called_once_with( + { + "data": "omitted", + "error_code": "ERROR_CODE", + "message": "Error message", + "status": "BAD_REQUEST", + "statusCode": 400, + } + ) + + +def test_authenticate_jwt(user_jwt): + event = { + "rawPath": "/encrypt", + "requestContext": {"http": {"method": "POST"}}, + "headers": { + "Content-Type": "application/json", + "authorization": f"Bearer {user_jwt}", + }, + "body": "test_data", + } + payload = index.authenticate(event) + assert payload == {"sub": "test_user", "ewi": "abcd1234"} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..05b439e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,53 @@ +{ + "name": "dkms-customer-api", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "cdk": "^2.99.1" + } + }, + "node_modules/aws-cdk": { + "version": "2.110.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.110.0.tgz", + "integrity": "sha512-ods6/Lh5hWv9qOMmifgg6ur/M6020Yi5mFXUolVSy/0gjzo9wFRcPAxKmQ3++Yz+rf5dadUZmmpc53evvUgR4A==", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/cdk": { + "version": "2.110.0", + "resolved": "https://registry.npmjs.org/cdk/-/cdk-2.110.0.tgz", + "integrity": "sha512-R+2MTk0Vy0kIIWt4YOosAFUW8r23CkycOKHCna7fpJoYV2aWvmZivj/i+wPWcTaawX27oEQBK0lb0fewS9zU5Q==", + "dependencies": { + "aws-cdk": "2.110.0" + }, + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8bdc095 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "cdk": "^2.99.1" + } +} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..de0e180 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,667 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "aws-cdk-asset-awscli-v1" +version = "2.2.201" +description = "A library that contains the AWS CLI for use in Lambda Layers" +optional = false +python-versions = "~=3.7" +files = [ + {file = "aws-cdk.asset-awscli-v1-2.2.201.tar.gz", hash = "sha256:88d1c269fd5cf8c9f6e0464ed22e2d4f269dfd5b36b8c4d37687bdba9c269839"}, + {file = "aws_cdk.asset_awscli_v1-2.2.201-py3-none-any.whl", hash = "sha256:56fe2ef91d3c8d33559aa32d2130e5f35f23af1fb82f06648ebbc82ffe0a5879"}, +] + +[package.dependencies] +jsii = ">=1.91.0,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "aws-cdk-asset-kubectl-v20" +version = "2.1.2" +description = "A library that contains kubectl for use in Lambda Layers" +optional = false +python-versions = "~=3.7" +files = [ + {file = "aws-cdk.asset-kubectl-v20-2.1.2.tar.gz", hash = "sha256:346283e43018a43e3b3ca571de3f44e85d49c038dc20851894cb8f9b2052b164"}, + {file = "aws_cdk.asset_kubectl_v20-2.1.2-py3-none-any.whl", hash = "sha256:7f0617ab6cb942b066bd7174bf3e1f377e57878c3e1cddc21d6b2d13c92d0cc1"}, +] + +[package.dependencies] +jsii = ">=1.70.0,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "aws-cdk-asset-node-proxy-agent-v6" +version = "2.0.1" +description = "@aws-cdk/asset-node-proxy-agent-v6" +optional = false +python-versions = "~=3.7" +files = [ + {file = "aws-cdk.asset-node-proxy-agent-v6-2.0.1.tar.gz", hash = "sha256:42cdbc1de2ed3f845e3eb883a72f58fc7e5554c2e0b6fcdb366c159778dce74d"}, + {file = "aws_cdk.asset_node_proxy_agent_v6-2.0.1-py3-none-any.whl", hash = "sha256:e442673d4f93137ab165b75386761b1d46eea25fc5015e5145ae3afa9da06b6e"}, +] + +[package.dependencies] +jsii = ">=1.86.1,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "aws-cdk-aws-apigatewayv2-alpha" +version = "2.110.0a0" +description = "The CDK Construct Library for AWS::APIGatewayv2" +optional = false +python-versions = "~=3.7" +files = [ + {file = "aws-cdk.aws-apigatewayv2-alpha-2.110.0a0.tar.gz", hash = "sha256:ffc94a63073aea737b2f43d3015f4dbd83870681a476ca6bdaff144a6d836f3b"}, + {file = "aws_cdk.aws_apigatewayv2_alpha-2.110.0a0-py3-none-any.whl", hash = "sha256:51afddf56819d411d39f12f8154c5c2aa5880e21ef38b87e602cdd3617984198"}, +] + +[package.dependencies] +aws-cdk-lib = ">=2.110.0,<3.0.0" +constructs = ">=10.0.0,<11.0.0" +jsii = ">=1.91.0,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "aws-cdk-aws-apigatewayv2-integrations-alpha" +version = "2.110.0a0" +description = "Integrations for AWS APIGateway V2" +optional = false +python-versions = "~=3.7" +files = [ + {file = "aws-cdk.aws-apigatewayv2-integrations-alpha-2.110.0a0.tar.gz", hash = "sha256:f853c7466aa3af6618792efbc6820d332e25c304e45199f9b9f5f84f9072aace"}, + {file = "aws_cdk.aws_apigatewayv2_integrations_alpha-2.110.0a0-py3-none-any.whl", hash = "sha256:f3802d2a6c68b6fea53cf92e4987de7cd62c4ff0e4c1eaba85517f96a8b1c969"}, +] + +[package.dependencies] +"aws-cdk.aws-apigatewayv2-alpha" = "2.110.0.a0" +aws-cdk-lib = ">=2.110.0,<3.0.0" +constructs = ">=10.0.0,<11.0.0" +jsii = ">=1.91.0,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "aws-cdk-aws-lambda-python-alpha" +version = "2.110.0a0" +description = "The CDK Construct Library for AWS Lambda in Python" +optional = false +python-versions = "~=3.7" +files = [ + {file = "aws-cdk.aws-lambda-python-alpha-2.110.0a0.tar.gz", hash = "sha256:5224d96d42e43069593194d907cb91e794cd1fd34fae7b33a6bc3ad5a876900b"}, + {file = "aws_cdk.aws_lambda_python_alpha-2.110.0a0-py3-none-any.whl", hash = "sha256:e67f89e6e95cd745fa499e7702c3b4c239e14ff4f93195579bef891df38c6810"}, +] + +[package.dependencies] +aws-cdk-lib = ">=2.110.0,<3.0.0" +constructs = ">=10.0.0,<11.0.0" +jsii = ">=1.91.0,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "aws-cdk-lib" +version = "2.110.0" +description = "Version 2 of the AWS Cloud Development Kit library" +optional = false +python-versions = "~=3.7" +files = [ + {file = "aws-cdk-lib-2.110.0.tar.gz", hash = "sha256:2f6650e8d365fb2b143e65cf22d91de45c090636d8a2f1ac68efc302187780f7"}, + {file = "aws_cdk_lib-2.110.0-py3-none-any.whl", hash = "sha256:80a8eac6dcc2dd38496d9296efb1d90b45051dd1748743555bf69cfe83b1aa0a"}, +] + +[package.dependencies] +"aws-cdk.asset-awscli-v1" = ">=2.2.201,<3.0.0" +"aws-cdk.asset-kubectl-v20" = ">=2.1.2,<3.0.0" +"aws-cdk.asset-node-proxy-agent-v6" = ">=2.0.1,<3.0.0" +constructs = ">=10.0.0,<11.0.0" +jsii = ">=1.91.0,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "black" +version = "23.11.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, + {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, + {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, + {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, + {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, + {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, + {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, + {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, + {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, + {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, + {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, + {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, + {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, + {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, + {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, + {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, + {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, + {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "boto3" +version = "1.29.4" +description = "The AWS SDK for Python" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "boto3-1.29.4-py3-none-any.whl", hash = "sha256:d1135647309b89376a014d21407aabfa322998206175f2297def812bf4d824a9"}, + {file = "boto3-1.29.4.tar.gz", hash = "sha256:ca9b04fc2c75990c2be84c43b9d6edecce828960fc27e07ab29036587a1ca635"}, +] + +[package.dependencies] +botocore = ">=1.32.4,<1.33.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.7.0,<0.8.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.32.4" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">= 3.7" +files = [ + {file = "botocore-1.32.4-py3-none-any.whl", hash = "sha256:3ee73c0d93bdb944d0c46772f08f09cdcf25ef58bd86962e6f4a24e531198bfa"}, + {file = "botocore-1.32.4.tar.gz", hash = "sha256:6bfa75e28c9ad0321cefefa51b00ff233b16b2416f8b95229796263edba45a39"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""} + +[package.extras] +crt = ["awscrt (==0.19.12)"] + +[[package]] +name = "cattrs" +version = "23.1.2" +description = "Composable complex class support for attrs and dataclasses." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cattrs-23.1.2-py3-none-any.whl", hash = "sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4"}, + {file = "cattrs-23.1.2.tar.gz", hash = "sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657"}, +] + +[package.dependencies] +attrs = ">=20" + +[package.extras] +bson = ["pymongo (>=4.2.0,<5.0.0)"] +cbor2 = ["cbor2 (>=5.4.6,<6.0.0)"] +msgpack = ["msgpack (>=1.0.2,<2.0.0)"] +orjson = ["orjson (>=3.5.2,<4.0.0)"] +pyyaml = ["PyYAML (>=6.0,<7.0)"] +tomlkit = ["tomlkit (>=0.11.4,<0.12.0)"] +ujson = ["ujson (>=5.4.0,<6.0.0)"] + +[[package]] +name = "certifi" +version = "2023.11.17" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "constructs" +version = "10.3.0" +description = "A programming model for software-defined state" +optional = false +python-versions = "~=3.7" +files = [ + {file = "constructs-10.3.0-py3-none-any.whl", hash = "sha256:2972f514837565ff5b09171cfba50c0159dfa75ee86a42921ea8c86f2941b3d2"}, + {file = "constructs-10.3.0.tar.gz", hash = "sha256:518551135ec236f9cc6b86500f4fbbe83b803ccdc6c2cb7684e0b7c4d234e7b1"}, +] + +[package.dependencies] +jsii = ">=1.90.0,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "importlib-resources" +version = "6.1.1" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.1.1-py3-none-any.whl", hash = "sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6"}, + {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "jsii" +version = "1.92.0" +description = "Python client for jsii runtime" +optional = false +python-versions = "~=3.8" +files = [ + {file = "jsii-1.92.0-py3-none-any.whl", hash = "sha256:30deaea011e146e1d4c0dbb35bd7effd4d292cef676052e5672a825fc1aaaebf"}, + {file = "jsii-1.92.0.tar.gz", hash = "sha256:2b5205c0fec87e1a9a9f283f60577ad172d7124bb614b8cdadc963306e1ac75f"}, +] + +[package.dependencies] +attrs = ">=21.2,<24.0" +cattrs = ">=1.8,<23.2" +importlib-resources = ">=5.2.0" +publication = ">=0.0.3" +python-dateutil = "*" +typeguard = ">=2.13.3,<2.14.0" +typing-extensions = ">=3.8,<5.0" + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "4.0.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "publication" +version = "0.0.3" +description = "Publication helps you maintain public-api-friendly modules by preventing unintentional access to private implementation details via introspection." +optional = false +python-versions = "*" +files = [ + {file = "publication-0.0.3-py2.py3-none-any.whl", hash = "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6"}, + {file = "publication-0.0.3.tar.gz", hash = "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"}, +] + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "s3transfer" +version = "0.7.0" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "s3transfer-0.7.0-py3-none-any.whl", hash = "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a"}, + {file = "s3transfer-0.7.0.tar.gz", hash = "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e"}, +] + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "typeguard" +version = "2.13.3" +description = "Run-time type checker for Python" +optional = false +python-versions = ">=3.5.3" +files = [ + {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, + {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, +] + +[package.extras] +doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["mypy", "pytest", "typing-extensions"] + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[[package]] +name = "urllib3" +version = "2.0.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "ab9edfd82e012ad75d40393348fa28e61bdd91e3689bec9fe32b8a1989df0d37" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..de73b17 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "DKMS Customer API" +version = "0.1.0" +description = "Magic Labs DKMS customer encryption endpoint" +authors = ["Magic Labs "] +readme = "README.md" +license = "Apache-2.0" + +[tool.poetry.dependencies] +python = "^3.11" +aws-cdk-lib = "^2.92.0" +constructs = "^10.2.69" +pytest = "^7.4.2" +boto3 = "^1.28.82" +black = "^23.11.0" +requests = "^2.31.0" +aws-cdk-aws-apigatewayv2-alpha = "^2.110.0a0" +aws-cdk-aws-apigatewayv2-integrations-alpha = "^2.110.0a0" +aws-cdk-aws-lambda-python-alpha = "^2.110.0a0" + +[tool.poetry.group.dev.dependencies] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_dkms_api.py b/tests/test_dkms_api.py new file mode 100644 index 0000000..02e3a1c --- /dev/null +++ b/tests/test_dkms_api.py @@ -0,0 +1,63 @@ +import aws_cdk as cdk +import aws_cdk.assertions as assertions + +from deploy.dkms_api import DKMSCustomerAPIStack + + +test_public_key = """-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7Cg2FOFIhqeH3CCEaleN +9b1j1ZNVk+cfquTkI/BOheZd7mb85SaUNjRd1NPxqVFcmI7co/Aiw+Gb1aTwNLW6 +EiZKh94yJvpptB4uGrEABFWgev2yKFELOfcUXaddJMYBxDRNCeWZ9Mgl80xR0O6c +ziL7rLmfvFyCzE0moPOWwf5y8jmiuT8W/WAqqsKbacIX39rXV1KPB5uMTD6JSgDT +HLrGqyloOImBd99PEjkIHjOWaQ/+/srjBNCzUgleXPzazAH777YlrDGO8OLnAlSE +6dODdL2xNIqwFQdrnBPmyyVIstP3/Sqz/mJCa7/2tsZz36I1E/kIidEX4SoKY5x+ +cQIDAQAB +-----END PUBLIC KEY----- +""" + +test_jwks_url = "https://example.com/.well-known/jwks.json" + + +def test_dkms_api_stack(): + app = cdk.App() + env_name = "test" + stack = DKMSCustomerAPIStack( + app, + f"dkms-customer-api-{env_name}", + env_name=env_name, + jwks_url=test_jwks_url, + cors_allow_origins="*", + ) + template = assertions.Template.from_stack(stack) + template.resource_count_is("AWS::KMS::Key", 1) + template.has_resource_properties( + "AWS::Lambda::Function", + { + "Runtime": "python3.11", + "Handler": "index.handler", + "Timeout": 30, + "MemorySize": 128, + }, + ) + template.has_resource_properties( + "AWS::ApiGatewayV2::Api", + { + "Name": "dkms-customer-api-test", + "ProtocolType": "HTTP", + }, + ) + + +def test_dkms_api_stack_all_options(): + app = cdk.App() + env_name = "test" + stack = DKMSCustomerAPIStack( + app, + f"dkms-customer-api-{env_name}", + env_name=env_name, + jwks_url=test_jwks_url, + cors_allow_origins="*", + domain_name="example.com", + acm_cert_arn="arn:aws:acm:us-west-2:01234567890:certificate/f278cd4d-e846-4063-bb00-bd15c382bb41", + ) + template = assertions.Template.from_stack(stack)