├── source ├── shared │ ├── virtual-waiting-room-on-aws-common │ │ ├── __init__.py │ │ ├── vwr │ │ │ ├── __init__.py │ │ │ └── common │ │ │ │ ├── __init__.py │ │ │ │ ├── sanitize.py │ │ │ │ ├── validate.py │ │ │ │ ├── diag.py │ │ │ │ └── jwt.py │ │ ├── pyproject.toml │ │ ├── README.md │ │ ├── setup.py │ │ └── common_tests.py │ ├── shared_resources_tests.py │ └── custom_resources │ │ └── cfn_bucket_loader.py ├── core-api-authorizers-sample │ ├── chalice │ │ ├── requirements.txt │ │ ├── .chalice │ │ │ └── config.json │ │ ├── poetry.lock │ │ ├── pyproject.toml │ │ ├── sample_api_tests.py │ │ └── app.py │ ├── .gitignore │ └── custom_resources │ │ ├── requirements.txt │ │ └── pyproject.toml ├── token-authorizer │ ├── .gitignore │ └── chalice │ │ ├── .chalice │ │ └── config.json │ │ ├── requirements.txt │ │ ├── pyproject.toml │ │ └── token_authorizer_tests.py ├── openid-waitingroom │ ├── .gitignore │ ├── chalice │ │ ├── .chalice │ │ │ └── config.json │ │ ├── requirements.txt │ │ └── pyproject.toml │ ├── www │ │ ├── api_endpoints.js │ │ └── package.json │ └── custom_resources │ │ ├── requirements.txt │ │ ├── pyproject.toml │ │ ├── generate_redirect_uris_secret.py │ │ ├── generate_client_secret.py │ │ └── custom_resources_tests.py ├── control-panel │ ├── src │ │ ├── .jshintrc │ │ ├── components │ │ │ ├── Banner.vue │ │ │ ├── Controls.vue │ │ │ ├── Counters.vue │ │ │ ├── Event.vue │ │ │ ├── Credentials.vue │ │ │ ├── Endpoints.vue │ │ │ ├── WaitingRoomSize.vue │ │ │ ├── ExpiredTokens.vue │ │ │ ├── ResetWaitingRoom.vue │ │ │ ├── ServingCounter.vue │ │ │ ├── ActiveTokens.vue │ │ │ └── IncrementServingCounter.vue │ │ ├── main.js │ │ └── App.vue │ ├── babel.config.js │ ├── .gitignore │ ├── vue.config.js │ ├── README.md │ └── package.json ├── sample-waiting-room-site │ ├── src │ │ ├── .jshintrc │ │ ├── router │ │ │ └── index.js │ │ ├── components │ │ │ ├── PurchaseReceipt.vue │ │ │ ├── WaitingRoomSize.vue │ │ │ ├── TimeToExit.vue │ │ │ ├── ServingPosition.vue │ │ │ ├── CompletePurchase.vue │ │ │ └── AuthorizationToken.vue │ │ ├── App.vue │ │ ├── views │ │ │ ├── CheckOut.vue │ │ │ ├── Home.vue │ │ │ └── WaitingRoom.vue │ │ └── main.js │ ├── babel.config.js │ ├── .gitignore │ ├── vue.config.js │ ├── README.md │ └── package.json ├── sample-inlet-strategies │ ├── requirements.txt │ ├── pyproject.toml │ ├── inlet_strategy_tests.py │ ├── periodic_inlet.py │ └── max_size_inlet.py ├── core-api │ ├── custom_resources │ │ ├── requirements.txt │ │ ├── pyproject.toml │ │ ├── intersect_az.py │ │ ├── update_distribution.py │ │ ├── initialize_state.py │ │ └── generate_keys.py │ └── lambda_functions │ │ ├── counters.py │ │ ├── get_public_key.py │ │ ├── get_serving_num.py │ │ ├── get_waiting_num.py │ │ ├── get_list_expired_tokens.py │ │ ├── get_num_active_tokens.py │ │ ├── increment_serving_counter.py │ │ ├── get_queue_num.py │ │ ├── generate_token.py │ │ ├── assign_queue_num.py │ │ ├── auth_generate_token.py │ │ ├── generate_events.py │ │ └── update_session.py └── tools │ ├── regional_buckets.py │ ├── pylint.sh │ └── load_test_prepare.py ├── docs ├── use-cases.jpg ├── logical-view.jpg ├── physical-view.jpg ├── deployment-view.jpg ├── enter-waiting-room.jpg ├── get-entrance-result.jpg ├── get-entrance-token.jpg ├── get-serving-position.jpg ├── use-api-gateway-authorizer.jpg ├── logical-view.drawio ├── use-cases.drawio ├── behavioral-views.drawio └── sequence-diagrams.drawio ├── deployment ├── Dockerfile ├── docker_build.sh ├── pyproject.toml ├── end-workflow-notification.py ├── virtual-waiting-room-on-aws-api-gateway-cw-logs-role.json ├── deploy.sh └── run-unit-tests.sh ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── CODE_OF_CONDUCT.md ├── SECURITY.md ├── .gitignore ├── .eslintrc.js └── CONTRIBUTING.md /source/shared/virtual-waiting-room-on-aws-common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/core-api-authorizers-sample/chalice/requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /source/shared/virtual-waiting-room-on-aws-common/vwr/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/shared/virtual-waiting-room-on-aws-common/vwr/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/core-api-authorizers-sample/.gitignore: -------------------------------------------------------------------------------- 1 | chalice/.chalice/deployments/ 2 | chalice/.chalice/venv/ 3 | -------------------------------------------------------------------------------- /docs/use-cases.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/virtual-waiting-room-on-aws/main/docs/use-cases.jpg -------------------------------------------------------------------------------- /source/token-authorizer/.gitignore: -------------------------------------------------------------------------------- 1 | chalice/.chalice/deployments/ 2 | chalice/.chalice/venv/ 3 | chalice/vendor/ 4 | -------------------------------------------------------------------------------- /docs/logical-view.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/virtual-waiting-room-on-aws/main/docs/logical-view.jpg -------------------------------------------------------------------------------- /docs/physical-view.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/virtual-waiting-room-on-aws/main/docs/physical-view.jpg -------------------------------------------------------------------------------- /docs/deployment-view.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/virtual-waiting-room-on-aws/main/docs/deployment-view.jpg -------------------------------------------------------------------------------- /docs/enter-waiting-room.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/virtual-waiting-room-on-aws/main/docs/enter-waiting-room.jpg -------------------------------------------------------------------------------- /docs/get-entrance-result.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/virtual-waiting-room-on-aws/main/docs/get-entrance-result.jpg -------------------------------------------------------------------------------- /docs/get-entrance-token.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/virtual-waiting-room-on-aws/main/docs/get-entrance-token.jpg -------------------------------------------------------------------------------- /docs/get-serving-position.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/virtual-waiting-room-on-aws/main/docs/get-serving-position.jpg -------------------------------------------------------------------------------- /source/openid-waitingroom/.gitignore: -------------------------------------------------------------------------------- 1 | chalice/.chalice/deployments/ 2 | chalice/.chalice/venv/ 3 | chalice/vendor/ 4 | node_modules/ 5 | dist/ -------------------------------------------------------------------------------- /docs/use-api-gateway-authorizer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/virtual-waiting-room-on-aws/main/docs/use-api-gateway-authorizer.jpg -------------------------------------------------------------------------------- /deployment/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/python:3.12 2 | CMD bash 3 | WORKDIR /site-packages 4 | RUN pip install --upgrade pip 5 | RUN pip install -t . jwcrypto==1.5.6 6 | -------------------------------------------------------------------------------- /source/shared/virtual-waiting-room-on-aws-common/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=71", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | -------------------------------------------------------------------------------- /source/shared/virtual-waiting-room-on-aws-common/README.md: -------------------------------------------------------------------------------- 1 | # Virtual Waiting Room on AWS Common Package 2 | 3 | This package contains several Python modules that are shared across different resources. 4 | -------------------------------------------------------------------------------- /source/control-panel/src/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 11, 3 | "browser": true, 4 | "loopfunc": true, 5 | "globals": { 6 | "console": true, 7 | "define": true, 8 | "require": true 9 | } 10 | } -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 6 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 11, 3 | "browser": true, 4 | "loopfunc": true, 5 | "globals": { 6 | "console": true, 7 | "define": true, 8 | "require": true 9 | } 10 | } -------------------------------------------------------------------------------- /source/sample-waiting-room-site/babel.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | presets: [ 6 | '@vue/cli-plugin-babel/preset' 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /source/token-authorizer/chalice/.chalice/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "app_name": "token-authorizer", 4 | "stages": { 5 | "dev": { 6 | "api_gateway_stage": "api" 7 | } 8 | }, 9 | "lambda_memory_size": 1024 10 | } -------------------------------------------------------------------------------- /source/openid-waitingroom/chalice/.chalice/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "app_name": "openid-waitingroom", 4 | "stages": { 5 | "dev": { 6 | "api_gateway_stage": "oidc" 7 | } 8 | }, 9 | "lambda_memory_size": 1024 10 | } -------------------------------------------------------------------------------- /source/control-panel/babel.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /*jshint esversion: 6 */ 5 | 6 | module.exports = { 7 | presets: [ 8 | '@vue/cli-plugin-babel/preset' 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /source/core-api-authorizers-sample/chalice/.chalice/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "app_name": "core-api-authorizers-sample", 4 | "stages": { 5 | "dev": { 6 | "api_gateway_stage": "store" 7 | } 8 | }, 9 | "lambda_memory_size": 1024 10 | } -------------------------------------------------------------------------------- /source/token-authorizer/chalice/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2025.7.14 ; python_full_version > "3.9.0" 2 | charset-normalizer==3.4.2 ; python_full_version > "3.9.0" 3 | idna==3.10 ; python_full_version > "3.9.0" 4 | requests==2.32.4 ; python_full_version > "3.9.0" 5 | urllib3==2.5.0 ; python_full_version > "3.9.0" 6 | -------------------------------------------------------------------------------- /source/core-api-authorizers-sample/chalice/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | package = [] 3 | 4 | [metadata] 5 | lock-version = "2.0" 6 | python-versions = ">3.9" 7 | content-hash = "182d7c69ef4c3b7d1fd5f66583a6e04351c99d662d542a0ad606505b7bff8571" 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. 3 | -------------------------------------------------------------------------------- /source/openid-waitingroom/www/api_endpoints.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /*jshint esversion: 6 */ 5 | const public_api = "https://public-api.cloudfront.net"; 6 | const private_api = "https://private-api.execute-api.us-west-4.amazonaws.com/nope"; 7 | -------------------------------------------------------------------------------- /source/control-panel/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /source/sample-inlet-strategies/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-requests-auth==0.4.3 ; python_full_version > "3.9.0" 2 | certifi==2025.8.3 ; python_full_version > "3.9.0" 3 | charset-normalizer==3.4.3 ; python_full_version > "3.9.0" 4 | idna==3.10 ; python_full_version > "3.9.0" 5 | requests==2.32.4 ; python_full_version > "3.9.0" 6 | urllib3==2.5.0 ; python_full_version > "3.9.0" 7 | -------------------------------------------------------------------------------- /source/openid-waitingroom/chalice/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-requests-auth==0.4.3 ; python_full_version > "3.9.0" 2 | certifi==2025.7.14 ; python_full_version > "3.9.0" 3 | charset-normalizer==3.4.2 ; python_full_version > "3.9.0" 4 | idna==3.10 ; python_full_version > "3.9.0" 5 | requests==2.32.4 ; python_full_version > "3.9.0" 6 | urllib3==2.5.0 ; python_full_version > "3.9.0" 7 | -------------------------------------------------------------------------------- /source/openid-waitingroom/custom_resources/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2025.8.3 ; python_full_version > "3.9.0" 2 | charset-normalizer==3.4.3 ; python_full_version > "3.9.0" 3 | crhelper==2.0.11 ; python_full_version > "3.9.0" 4 | idna==3.10 ; python_full_version > "3.9.0" 5 | requests==2.32.4 ; python_full_version > "3.9.0" 6 | urllib3==2.5.0 ; python_full_version > "3.9.0" 7 | -------------------------------------------------------------------------------- /source/core-api-authorizers-sample/custom_resources/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2025.8.3 ; python_full_version > "3.9.0" 2 | charset-normalizer==3.4.3 ; python_full_version > "3.9.0" 3 | crhelper==2.0.11 ; python_full_version > "3.9.0" 4 | idna==3.10 ; python_full_version > "3.9.0" 5 | requests==2.32.4 ; python_full_version > "3.9.0" 6 | urllib3==2.5.0 ; python_full_version > "3.9.0" 7 | -------------------------------------------------------------------------------- /source/control-panel/vue.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /*jshint esversion: 6 */ 5 | 6 | module.exports = { 7 | publicPath: (process.env.NODE_ENV === 'production' ? '/control-panel/' : '/'), 8 | outputDir: 'dist/www/control-panel', 9 | transpileDependencies: true 10 | }; 11 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /source/core-api-authorizers-sample/chalice/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "chalice" 3 | version = "1.1.15" 4 | description = "Chalice Code" 5 | authors = ["Amazon Web Services"] 6 | package-mode = false 7 | 8 | 9 | [tool.poetry.dependencies] 10 | python = ">3.9" 11 | 12 | 13 | [build-system] 14 | requires = ["poetry-core"] 15 | build-backend = "poetry.core.masonry.api" 16 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/vue.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /*jshint esversion: 6 */ 5 | 6 | module.exports = { 7 | publicPath: (process.env.NODE_ENV === 'production' ? '/waiting-room-site/' : '/'), 8 | outputDir: 'dist/www/waiting-room-site', 9 | transpileDependencies: true 10 | }; 11 | -------------------------------------------------------------------------------- /source/core-api/custom_resources/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-requests-auth==0.4.3 ; python_full_version > "3.9.0" 2 | certifi==2025.8.3 ; python_full_version > "3.9.0" 3 | charset-normalizer==3.4.3 ; python_full_version > "3.9.0" 4 | crhelper==2.0.11 ; python_full_version > "3.9.0" 5 | idna==3.10 ; python_full_version > "3.9.0" 6 | requests==2.32.4 ; python_full_version > "3.9.0" 7 | urllib3==2.5.0 ; python_full_version > "3.9.0" 8 | -------------------------------------------------------------------------------- /source/sample-inlet-strategies/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "custom-resources" 3 | version = "1.1.15" 4 | description = "Virtual Waiting Room on AWS" 5 | authors = ["Amazon Web Services"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">3.9" 9 | requests = "2.32.4" 10 | aws-requests-auth = "0.4.3" 11 | 12 | 13 | [build-system] 14 | requires = ["poetry-core"] 15 | build-backend = "poetry.core.masonry.api" 16 | -------------------------------------------------------------------------------- /source/core-api-authorizers-sample/custom_resources/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "custom-resources" 3 | version = "1.1.15" 4 | description = "" 5 | authors = ["Amazon Web Services"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">3.9" 9 | crhelper = "2.0.11" 10 | requests = "2.32.4" 11 | urllib3 = "2.5.0" 12 | 13 | 14 | [build-system] 15 | requires = ["poetry-core"] 16 | build-backend = "poetry.core.masonry.api" 17 | -------------------------------------------------------------------------------- /source/token-authorizer/chalice/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "custom-resources" 3 | version = "1.1.15" 4 | description = "Token Authorizer for template of Virtual Waiting Room on AWS" 5 | authors = ["Amazon Web Services"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">3.9" 9 | requests="2.32.4" 10 | urllib3 = "2.5.0" 11 | 12 | 13 | [build-system] 14 | requires = ["poetry-core"] 15 | build-backend = "poetry.core.masonry.api" 16 | -------------------------------------------------------------------------------- /source/control-panel/README.md: -------------------------------------------------------------------------------- 1 | # control-panel 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/README.md: -------------------------------------------------------------------------------- 1 | # waiting-room-site 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /source/openid-waitingroom/custom_resources/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "custom-resources" 3 | version = "1.1.15" 4 | description = "Custom resources for open Id waiting room of Virtual Waiting Room on AWS" 5 | authors = ["Amazon Web Services"] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">3.9" 9 | crhelper = "2.0.11" 10 | requests = "2.32.4" 11 | urllib3 = "2.5.0" 12 | 13 | 14 | 15 | [build-system] 16 | requires = ["poetry-core"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /source/openid-waitingroom/chalice/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "custom-resources" 3 | version = "1.1.15" 4 | description = "Open Id waiting room template of Virtual Waiting Room on AWS" 5 | authors = ["Amazon Web Services"] 6 | package-mode = false 7 | 8 | [tool.poetry.dependencies] 9 | python = ">3.9" 10 | requests = "2.32.4" 11 | aws-requests-auth = "0.4.3" 12 | urllib3 = "2.5.0" 13 | 14 | 15 | [build-system] 16 | requires = ["poetry-core"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /source/control-panel/src/components/Banner.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /deployment/docker_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | target_dir="../source/python/lib/python3.12/" 4 | 5 | if [ ! -f Dockerfile ]; then 6 | echo "*** Dockerfile is not here, are you in the right place? ***" 7 | else 8 | # Build for x86_64/amd64 platform to match Lambda runtime 9 | docker build --platform linux/amd64 -t jwcrypto . 10 | docker run --platform linux/amd64 --name jwc -dit jwcrypto 11 | docker cp jwc:site-packages/ $target_dir 12 | docker container stop jwc 13 | docker container rm jwc 14 | fi 15 | -------------------------------------------------------------------------------- /source/core-api/custom_resources/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "custom-resources" 3 | version = "1.1.15" 4 | description = "Custom resources for core APIs of Virtual Waiting Room on AWS" 5 | authors = ["Amazon Web Services"] 6 | package-mode = false 7 | 8 | [tool.poetry.dependencies] 9 | python = ">3.9" 10 | crhelper = "2.0.11" 11 | requests = "2.32.4" 12 | aws-requests-auth = "0.4.3" 13 | urllib3 = "2.5.0" 14 | 15 | 16 | [build-system] 17 | requires = ["poetry-core"] 18 | build-backend = "poetry.core.masonry.api" 19 | -------------------------------------------------------------------------------- /source/shared/virtual-waiting-room-on-aws-common/vwr/common/sanitize.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | """ 4 | This module provides functions for sanitizing input from various sources. 5 | """ 6 | 7 | import bleach 8 | 9 | 10 | def deep_clean(text): 11 | """ 12 | Escape and clean all undesirable characters and tags in the input 13 | """ 14 | return bleach.clean(text, tags=[], attributes={}, styles=[], strip=True, strip_comments=True) 15 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | We take all security reports seriously. 4 | When we receive such reports, 5 | we will investigate and subsequently address 6 | any potential vulnerabilities as quickly as possible. 7 | If you discover a potential security issue in this project, 8 | please notify AWS/Amazon Security via our 9 | [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) 10 | or directly via email to [AWS Security](mailto:aws-security@amazon.com). 11 | Please do *not* create a public GitHub issue in this project. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this solution 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the feature you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /source/shared/virtual-waiting-room-on-aws-common/vwr/common/validate.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | """ 4 | This module provides functions for validating input from various sources. 5 | """ 6 | 7 | import re 8 | 9 | def is_valid_rid(text): 10 | """ 11 | Client supplied request IDs must conform to the pattern of IDs 12 | generated by API Gateway (e.g. 26a07142-098a-4bea-b67d-1fe5a098bf29) 13 | """ 14 | pattern = re.compile('\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12}') 15 | return pattern.match(text) 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .DS_Store 3 | .vscode/ 4 | *~ 5 | *tmp 6 | ~* 7 | error.txt 8 | package/ 9 | tmp/ 10 | global-s3-assets/ 11 | regional-s3-assets/ 12 | deployment/virtual-waiting-room-on-aws-authorizers.json 13 | deployment/virtual-waiting-room-on-aws-openid.json 14 | deployment/virtual-waiting-room-on-aws-sample.json 15 | *egg-info/ 16 | virtual_waiting_room_on_aws_common* 17 | deployment/pkg/ 18 | node_modules/ 19 | .env/ 20 | .venv/ 21 | coverage.lcov 22 | coverage.xml* 23 | .coverage 24 | .idea 25 | build 26 | docs/behavioral-views.drawio 27 | docs/deployment-view.drawio 28 | 29 | # cdxgen output 30 | bom.json -------------------------------------------------------------------------------- /source/shared/virtual-waiting-room-on-aws-common/vwr/common/diag.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | """ 4 | This module provides functions for printing and handling diagnostic output, like exceptions. 5 | """ 6 | 7 | import linecache 8 | import sys 9 | 10 | 11 | def print_exception(): 12 | """ 13 | Informative exception handler 14 | """ 15 | _, exc_obj, traceback = sys.exc_info() 16 | frame = traceback.tb_frame 17 | lineno = traceback.tb_lineno 18 | filename = frame.f_code.co_filename 19 | linecache.checkcache(filename) 20 | line = linecache.getline(filename, lineno, frame.f_globals) 21 | print(f'EXCEPTION IN ({filename}, LINE {lineno} "{line.strip()}"): {exc_obj}') 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:vue/essential" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": "latest", 12 | "sourceType": "module", 13 | }, 14 | "plugins": [ 15 | "vue" 16 | ], 17 | "rules": { 18 | }, 19 | "globals": { 20 | "_": true, 21 | "Cookies": true, 22 | "Fuse": true, 23 | "filterXSS": true, 24 | "machina": true, 25 | "moment": true, 26 | "objectHash": true, 27 | "renderjson": true, 28 | "showdown": true, 29 | "SVG": true, 30 | "Tabulator": true, 31 | "URI": true, 32 | "vis": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /source/shared/virtual-waiting-room-on-aws-common/vwr/common/jwt.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | """ 4 | This module provides functions for working with JSON Web Tokens. 5 | """ 6 | 7 | import base64 8 | import json 9 | 10 | 11 | def claim_dict(encoded_token): 12 | """ 13 | This function returns a dictionary of claims from a string JWT. 14 | Check the token's signature elsewhere. 15 | """ 16 | # split the three components 17 | _, payload, _ = encoded_token.split('.') 18 | # decode the payload in the dictionary of claims 19 | if len(payload) % 4: 20 | # not a multiple of 4, add padding: 21 | payload += '=' * (4 - len(payload) % 4) 22 | return json.loads(base64.b64decode(payload).decode('utf-8')) 23 | -------------------------------------------------------------------------------- /deployment/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "deployment" 3 | version = "1.1.15" 4 | description = "Deployment of Virtual Waiting Room on AWS" 5 | authors = ["Amazon Web Services"] 6 | package-mode=false 7 | 8 | [tool.poetry.dependencies] 9 | python = ">=3.10, <4" 10 | crhelper = "2.0.11" 11 | requests = "2.32.4" 12 | aws-requests-auth = "0.4.3" 13 | urllib3 = "2.5.0" 14 | awscli = "1.35.13" 15 | boto3 = "1.35.47" 16 | botocore = "1.35.47" 17 | build = "1.2.2" 18 | cfn-lint = "1.18.1" 19 | chalice = "1.31.2" 20 | coverage = "7.6.4" 21 | jwcrypto = "1.5.6" 22 | python-dateutil = "2.9.0" 23 | redis = "5.1.1" 24 | testresources = "2.0.1" 25 | setuptools = "78.1.1" 26 | cryptography = "44.0.1" 27 | virtual_waiting_room_on_aws_common = {path = "../source/shared/virtual-waiting-room-on-aws-common"} 28 | 29 | [build-system] 30 | requires = ["poetry-core"] 31 | build-backend = "poetry.core.masonry.api" 32 | -------------------------------------------------------------------------------- /source/control-panel/src/components/Controls.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 16 | 17 | 26 | -------------------------------------------------------------------------------- /source/tools/regional_buckets.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | """ 4 | This module creates S3 buckets with a region suffix. 5 | """ 6 | 7 | import time 8 | import boto3 9 | 10 | ROOT_BUCKET_NAME = "virtual-waiting-room-on-aws" 11 | 12 | 13 | regions = ['ap-northeast-1', 'ap-south-1', 'ap-southeast-1', 'ca-central-1', \ 14 | 'eu-central-1', 'eu-north-1', 'eu-west-1', 'sa-east-1', \ 15 | 'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2'] 16 | print(regions) 17 | 18 | for region in regions: 19 | try: 20 | s3 = boto3.client('s3', region_name=region) 21 | name = f"{ROOT_BUCKET_NAME}-{region}" 22 | response = s3.create_bucket( 23 | Bucket=name, 24 | CreateBucketConfiguration={'LocationConstraint': region}) 25 | print(response) 26 | except Exception as exception: 27 | print(exception) 28 | time.sleep(2) 29 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/router/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /*jshint esversion: 6 */ 5 | 6 | import { createRouter, createWebHistory } from 'vue-router'; 7 | import Home from '../views/Home.vue'; 8 | import WaitingRoom from '../views/WaitingRoom.vue'; 9 | import CheckOut from '../views/CheckOut.vue'; 10 | 11 | // routes determine which view component to show based on the address 12 | 13 | const routes = [ 14 | { 15 | path: '/', 16 | alias: "/index.html", 17 | name: 'Home', 18 | component: Home, 19 | }, 20 | { 21 | path: '/waitingroom', 22 | name: 'Waiting Room', 23 | component: WaitingRoom, 24 | }, 25 | { 26 | path: '/checkout', 27 | name: 'Check Out', 28 | component: CheckOut, 29 | } 30 | ]; 31 | 32 | const router = createRouter({ 33 | history: createWebHistory(process.env.BASE_URL), 34 | routes 35 | }); 36 | 37 | export default router; 38 | -------------------------------------------------------------------------------- /source/openid-waitingroom/www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openid-waitingroom", 3 | "version": "1.1.15", 4 | "description": "A Virtual Waiting Room on AWS OpenID Adaptor", 5 | "author": { 6 | "name": "Amazon Web Services", 7 | "url": "https://aws.amazon.com/solutions", 8 | "organization": true 9 | }, 10 | "license": "Apache-2.0", 11 | "private": true, 12 | "scripts": { 13 | "clean": "rm -rf node_modules/ dist/ coverage/", 14 | "postinstall": "mkdir -p dist && cp -f node_modules/bootstrap/dist/css/bootstrap.min.css node_modules/jquery/dist/jquery.min.js node_modules/bootstrap/dist/js/bootstrap.bundle.min.js node_modules/axios/dist/axios.min.js dist/" 15 | }, 16 | "dependencies": { 17 | "axios": "^1.12.1", 18 | "bootstrap": "^5.1.1", 19 | "jquery": "^3.6.0" 20 | }, 21 | "overrides": { 22 | "cross-spawn": "^7.0.6", 23 | "postcss": "^8.4.31", 24 | "semver": "^7.5.2", 25 | "word-wrap": "^1.2.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /source/control-panel/src/components/Counters.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /source/core-api/lambda_functions/counters.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module defines the various counters used by the core API. 6 | """ 7 | 8 | # counter for requests enqueued 9 | QUEUE_COUNTER = "queue_counter" 10 | 11 | # counter for number currently being served 12 | SERVING_COUNTER = "serving_counter" 13 | 14 | # counter for tokens generated 15 | # Bandit B105: not a hardcoded password 16 | TOKEN_COUNTER = "token_counter" # nosec 17 | 18 | # counter for number of expired queue positions 19 | EXPIRED_QUEUE_COUNTER = "expired_queue_counter" 20 | 21 | # counter for sessions completed (i.e. token was used to complete transaction) 22 | COMPLETED_SESSION_COUNTER = "completed_counter" 23 | 24 | # counter for sessions abandoned (i.e. token was never used to complete transaction) 25 | ABANDONED_SESSION_COUNTER = "abandoned_counter" 26 | 27 | # maximum expired queue position 28 | MAX_QUEUE_POSITION_EXPIRED = "max_queue_position_expired" 29 | 30 | # indicate if reset in progress 31 | RESET_IN_PROGRESS = "reset_in_progress" 32 | -------------------------------------------------------------------------------- /source/shared/virtual-waiting-room-on-aws-common/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module sets up the metadata for the waiting room common library. 6 | """ 7 | 8 | import setuptools 9 | 10 | with open("README.md", "r", encoding="utf-8") as fh: 11 | long_description = fh.read() 12 | 13 | setuptools.setup( 14 | name="virtual-waiting-room-on-aws-common", 15 | version="1.1.12", 16 | author="AWS Solutions PDX", 17 | author_email="aws-solutions-pdx@amazon.com", 18 | description="Common Python modules for Virtual Waiting Room on AWS", 19 | long_description=long_description, 20 | long_description_content_type="text/markdown", 21 | url="https://github.com/aws-solutions/virtual-waiting-room-on-aws", 22 | project_urls={ 23 | "Bug Tracker": 24 | "https://github.com/aws-solutions/virtual-waiting-room-on-aws/issues", 25 | }, 26 | classifiers=[ 27 | "Programming Language :: Python :: 3", 28 | "License :: OSI Approved :: Apache License", 29 | "Operating System :: OS Independent", 30 | ], 31 | packages=setuptools.find_packages(where="."), 32 | python_requires=">=3.10", 33 | install_requires=[ 34 | 'bleach~=4.1.0', 35 | ]) 36 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/components/PurchaseReceipt.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | 35 | 36 | 47 | -------------------------------------------------------------------------------- /source/control-panel/src/main.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /*jshint esversion: 6 */ 5 | 6 | import { createApp } from 'vue'; 7 | import { createStore } from 'vuex'; 8 | import App from './App.vue'; 9 | 10 | const store = createStore({ 11 | state() { 12 | return { 13 | servingCounter: undefined, 14 | waitingRoomSize: undefined, 15 | activeTokens: undefined, 16 | expiredTokens: undefined, 17 | }; 18 | }, 19 | mutations: { 20 | setServingCounter(state, value) { 21 | state.servingCounter = value; 22 | }, 23 | setWaitingRoomSize(state, value) { 24 | state.waitingRoomSize = value; 25 | }, 26 | setActiveTokens(state, value) { 27 | state.activeTokens = value; 28 | }, 29 | setExpiredTokens(state, value) { 30 | state.expiredTokens = value; 31 | }, 32 | }, 33 | getters: 34 | { 35 | getAvailableCounter(state) { 36 | if (state.servingCounter === undefined || state.waitingRoomSize === undefined || state.activeTokens === undefined || state.expiredTokens === undefined) { 37 | return undefined; 38 | } else { 39 | return state.servingCounter - (state.waitingRoomSize + state.activeTokens + state.expiredTokens); 40 | } 41 | }, 42 | } 43 | }); 44 | 45 | const app = createApp(App); 46 | app.use(store); 47 | app.mount('#app'); 48 | -------------------------------------------------------------------------------- /source/tools/pylint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # http://www.apache.org/licenses/LICENSE-2.0 4 | # 5 | # Unless required by applicable law or agreed to in writing, 6 | # software distributed under the License is distributed on an 7 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 8 | # KIND, either express or implied. See the License for the 9 | # specific language governing permissions and limitations 10 | # under the License. 11 | 12 | # IGNORED RULES 13 | # E0401 Unable to import module (packaged via Lambda layers instead) 14 | # C0103 Constant name 15 | # C0301 Line too long 16 | # C0302 Too many lines in module 17 | # C0303 Trailing whitespace 18 | # C0411 Order of import 19 | # C0412 Grouping of import 20 | # C0413 Wrong import position (in unit tests) 21 | # R0201 Method could be a function (in unit tests) 22 | # R0801 Similar lines in files 23 | # R1702 Too many nested blocks 24 | # R0912 Too many branches 25 | # R0913 Too many arguments 26 | # R0914 Too many local variables 27 | # R0915 Too many statements 28 | # W0105 No effect string (comment in test) 29 | # W0401 Wildcard import 30 | # W0603 Global statement 31 | # W0621 Redefining name 32 | # W0703 Catching too general exception 33 | # W0613 Unused argument (like passing 'event' or 'context' into lambda) 34 | # W0640 Cell variable 35 | 36 | set -euo pipefail 37 | 38 | find . -iname '*.py' | \ 39 | grep "/package/" --invert-match | \ 40 | grep "/chalice/vendor/" --invert-match | \ 41 | xargs pylint -d E0401,C0103,C0301,C0302,C0303,C0411,C0412,C0413,R0801,R1702,R0912,R0913,R0914,R0915,W0105,W0401,W0703,W0603,W0613,W0621,W0640,W0719 42 | -------------------------------------------------------------------------------- /source/control-panel/src/components/Event.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 24 | 25 | 55 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 47 | 48 | 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Please complete the following information about the solution:** 20 | - [ ] Version: [e.g. v1.0.0] 21 | 22 | To get the version of the solution, you can look at the description of the created CloudFormation stack. For example, "_(SO0021) - Video On Demand workflow with AWS Step Functions, MediaConvert, MediaPackage, S3, CloudFront and DynamoDB. Version **v5.0.0**_". If the description does not contain the version information, you can look at the mappings section of the template: 23 | 24 | ```yaml 25 | Mappings: 26 | SourceCode: 27 | General: 28 | S3Bucket: "solutions" 29 | KeyPrefix: "video-on-demand-on-aws/v5.0.0" 30 | ``` 31 | 32 | - [ ] Region: [e.g. us-east-1] 33 | - [ ] Was the solution modified from the version published on this repository? 34 | - [ ] If the answer to the previous question was yes, are the changes available on GitHub? 35 | - [ ] Have you checked your [service quotas](https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) for the sevices this solution uses? 36 | - [ ] Were there any errors in the CloudWatch Logs? 37 | 38 | **Screenshots** 39 | If applicable, add screenshots to help explain your problem (please **DO NOT include sensitive information**). 40 | 41 | **Additional context** 42 | Add any other context about the problem here. 43 | -------------------------------------------------------------------------------- /docs/logical-view.drawio: -------------------------------------------------------------------------------- 1 | 7Vtbc6IwFP41PnYGCKA+Vmu7OzvbcevudqZvEbLALBIbotX++g0SEBJdaMttii8OnBzC4cuX47nAAExXuzsC1+53bCN/oCn2bgBuBpo2HI/YbyTYxwJVN7RY4hDP5rKjYOG9Ii5UuHTj2SjMKVKMfeqt80ILBwGyaE4GCcEvebU/2M/fdQ0dJAkWFvRl6aNnU5dLVUU5DnxBnuPyW48MPrCCiTIXhC608UtGBGYDMCUY0/hotZsiPwIvwSW+7vbMaGoYQQEtc4H59Zf/+qzr85npPt0/P92//l5f8cXYQn/DH3i2jeaLLab7BAaXrnx2pA7ARL4vN2WLCEW7jIjbcYfwClGyZyp8FAw5JpwVY376kkM4lrkZcBNsIV9UJ535+NzsgD/6G2AAEgwP6HmDwqaBSGnfGhK6hMQUbwKKSM1IqCMBibaBMCQgHqFHvcBhwgeMVwPN9JkNkyVDxnSiox8bxFTrhcno2s4xJZgWiGxjmOY4ZIjhoGZMhnrHMBlKmFxbFgpDJvuJ/6K68RCdCtDbBmQkATIn3hZStluUb2hfMx6pU+2Mkx3LeGyWvmc1AofRNTSSm2Xg4K60KQ8ietUOYKJKmEgYIJtFqvwUE+piBwfQnx2lkwxKKLCvo3CYnQY4iAaRvzycRjB5LOBlopBCQkW1wL71IuNvlIMGYR5sin1MDkYAQzcmps5GCAsTbGRzvbOLEuINsVBxRMoscRAtDtkiEP67xNklTIJ2gnxIvW0+xD+1hny6OfaimDihiwnydNE1gQbxQ/KrsiG5MNGwaKIYBWmiA6XSZ/wAy+TwvycsM0qyTGuPZUAxcuRQRWdTlmXiRNqoHMvYEsF9Rm0dKYTnDTa0PJsTj3nOrgJ9dhBbUC3l5VTvQvmuOFagCpQ330t5YaI0J6ia8uIeFWswBfoaaILyck5/ofypJLaXXr4ylskFk56wDJRkWZJA9JJmb/asI+E+ehOeUq5mXTh8qrTVBQoDUBGFG8+65PpgT1hWNrfXW2SZGIK+11GmBgkNjYr9JFulvL1KE35SLuheGHyqwtsGgzVTSKs1450MLpqoIgqrYmzSSB1ArsFfKHyqafOpKJx6y6q98FA0uAEKJ/8o/aPwuPsU1kFBBaoshcWJmo5WNbkTtUBheGjKKQt6aOIKpKu5OZdknK0155J91r+NVzZNTCb8VDsPiISqqtQhvL/SSKlDk/sgj2gZbWmv9h0tvqByZbS+o0+89gbX0PJo7W9jdA+L3hZytdLurcferTIH1Ntaa+JsimnWYitWpIdaEc1S+5qiWX+LrWVp1mL30xxXlOgPpQS8XKJfGc16WxEtm4u32Dlq/T/zoxXRJGCuNyPobUV01H0Kq0UvNJVufgrf2YCa3ozSTIHC5ocozE6PX8rF6sfvDcHsHw== -------------------------------------------------------------------------------- /source/control-panel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "control-panel", 3 | "description": "This project is a sample control panel for managing an instance of Virtual Waiting Room on AWS", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions", 7 | "organization": true 8 | }, 9 | "version": "1.1.15", 10 | "private": true, 11 | "license": "Apache-2.0", 12 | "scripts": { 13 | "serve": "vue-cli-service serve", 14 | "build": "vue-cli-service build", 15 | "lint": "vue-cli-service lint" 16 | }, 17 | "dependencies": { 18 | "aws4fetch": "^1.0.13", 19 | "bootstrap": "^5.1.1", 20 | "core-js": "^3.8.3", 21 | "lodash": "^4.17.21", 22 | "vue": "^3.3.7", 23 | "vue-timers": "^2.0.4", 24 | "vuex": "^4.1.0" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.23.2", 28 | "@vue/babel-preset-app": "^5.0.8", 29 | "@vue/cli-plugin-babel": "^5.0.8", 30 | "@vue/cli-plugin-eslint": "^5.0.8", 31 | "@vue/cli-service": "^5.0.8", 32 | "@vue/compiler-sfc": "^3.3.7", 33 | "eslint": "^8.22.0", 34 | "eslint-plugin-vue": "^9.3.0", 35 | "naive-ui": "^2.19.3", 36 | "vfonts": "^0.1.0" 37 | }, 38 | "eslintConfig": { 39 | "root": true, 40 | "env": { 41 | "node": true 42 | }, 43 | "extends": [ 44 | "plugin:vue/base", 45 | "eslint:recommended" 46 | ], 47 | "parserOptions": {}, 48 | "rules": {} 49 | }, 50 | "overrides": { 51 | "cross-spawn": "^7.0.6", 52 | "postcss": "^8.4.31", 53 | "webpack-dev-server": "^5.2.1", 54 | "semver": "^7.5.2", 55 | "word-wrap": "^1.2.4", 56 | "send": "0.19.0" 57 | }, 58 | "browserslist": [ 59 | "> 1%", 60 | "last 2 versions", 61 | "not dead" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /deployment/end-workflow-notification.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module sends notification about the end of a workflow run 6 | and its associated artifacts. 7 | """ 8 | 9 | import os 10 | import requests 11 | from aws_requests_auth.boto_utils import BotoAWSRequestsAuth 12 | from urllib.parse import urlparse 13 | 14 | API_REGION = os.environ.get('AWS_DEFAULT_REGION') 15 | NOTIFICATION_ENDPOINT = os.environ.get('NOTIFICATION_ENDPOINT') 16 | 17 | SOLUTION_NAME = 'virtual-waiting-room-on-aws' 18 | 19 | PAYLOAD = {} 20 | PAYLOAD['solution_org'] = os.environ.get('GITHUB_REPOSITORY').split('/')[0] 21 | PAYLOAD['solution_name'] = os.environ.get('GITHUB_REPOSITORY').split('/')[1] 22 | PAYLOAD['branch'] = os.environ.get('BRANCH') 23 | PAYLOAD['workflow_name'] = os.environ.get('WORKFLOW_NAME') 24 | PAYLOAD['commit_id'] = os.environ.get('COMMIT_ID') 25 | PAYLOAD['workflow_run_id'] = os.environ.get('WORKFLOW_RUN_ID') 26 | PAYLOAD['version'] = os.environ.get('VERSION') 27 | PAYLOAD['pipeline_type'] = os.environ.get('PIPELINE_TYPE') 28 | PAYLOAD['notify_github_workflow_id'] = "post-pipeline-workflow.yml" 29 | 30 | def main(): 31 | """ 32 | Invokes notification endpoint to signal completion of workflow. 33 | """ 34 | 35 | parsed = urlparse(NOTIFICATION_ENDPOINT) 36 | auth = BotoAWSRequestsAuth(aws_host=parsed.netloc, 37 | aws_region=API_REGION, 38 | aws_service='execute-api') 39 | print(PAYLOAD) 40 | response = requests.post(NOTIFICATION_ENDPOINT, json=PAYLOAD, auth=auth, timeout=25) 41 | print(response.json()) 42 | if response.status_code != 200: 43 | return 1 44 | return 0 45 | if __name__ == "__main__": 46 | main() -------------------------------------------------------------------------------- /source/core-api/lambda_functions/get_public_key.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the get_public_key API handler. 6 | It retrieves the public key generated by a custom resource stored in Secrets Manager. 7 | """ 8 | 9 | import boto3 10 | import os 11 | import json 12 | from botocore import config 13 | from botocore.exceptions import ClientError 14 | from vwr.common.sanitize import deep_clean 15 | 16 | SECRET_NAME_PREFIX = os.environ["STACK_NAME"] 17 | SOLUTION_ID = os.environ['SOLUTION_ID'] 18 | EVENT_ID = os.environ["EVENT_ID"] 19 | 20 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 21 | user_config = config.Config(**user_agent_extra) 22 | client = boto3.client('secretsmanager', config=user_config) 23 | 24 | def lambda_handler(event, _): 25 | """ 26 | This function is the entry handler for Lambda. 27 | """ 28 | 29 | print(event) 30 | headers = { 31 | 'Content-Type': 'application/json', 32 | 'Access-Control-Allow-Origin': '*' 33 | } 34 | 35 | client_event_id = deep_clean(event['queryStringParameters']['event_id']) 36 | if client_event_id != EVENT_ID: 37 | return { 38 | "statusCode": 400, 39 | "headers": headers, 40 | "body": json.dumps({"error": "Invalid request ID"}) 41 | } 42 | 43 | response = {} 44 | try: 45 | get_secret_value_response = client.get_secret_value(SecretId=f"{SECRET_NAME_PREFIX}/jwk-public") 46 | response = { 47 | "statusCode": 200, 48 | "headers": headers, 49 | "body": get_secret_value_response['SecretString'] 50 | } 51 | except ClientError as e: 52 | print(e.response['Error']['Code']) 53 | raise e 54 | 55 | return response 56 | -------------------------------------------------------------------------------- /source/core-api/custom_resources/intersect_az.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | """ 4 | This custom resource is responsible for determining and returning the list 5 | of AZs that are common to all the service endpoints used in this solution. 6 | """ 7 | 8 | import os 9 | import json 10 | import boto3 11 | from crhelper import CfnResource 12 | from botocore import config 13 | 14 | helper = CfnResource() 15 | SOLUTION_ID = os.environ['SOLUTION_ID'] 16 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 17 | user_config = config.Config(**user_agent_extra) 18 | client = boto3.client('ec2', config=user_config) 19 | 20 | @helper.create 21 | @helper.update 22 | def create_update(event, _): 23 | """ 24 | This function is responsible for generating a sorted, intersection list of 25 | AZ names supported by all the specified VPC endpoint services. 26 | """ 27 | print(event) 28 | # these are the service names to process 29 | service_names = event["ResourceProperties"]["ServiceNames"] 30 | print(f"Service names: {json.dumps(service_names)}") 31 | # get the endpoint AZs for each service 32 | response = client.describe_vpc_endpoint_services( 33 | ServiceNames=service_names) 34 | print(response) 35 | # find the AZs common to all services 36 | intersect_az = set(response['ServiceDetails'][0]['AvailabilityZones']) 37 | for details in response['ServiceDetails'][1:]: 38 | intersect_az = intersect_az & set(details['AvailabilityZones']) 39 | # sort and return the intesecting AZ list 40 | sorted_az = sorted(intersect_az) 41 | print(f"Intersecting AZs: {json.dumps(sorted_az)}") 42 | helper.Data.update({"intersect_az": sorted_az}) 43 | 44 | 45 | def handler(event, context): 46 | """ 47 | This function is the entry point for the Lambda-backed custom resource. 48 | """ 49 | helper(event, context) 50 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/views/CheckOut.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 35 | 36 | 57 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waiting-room-site", 3 | "description": "This project is a sample waiting room page to be used as a reference or starting-point for custom pages", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions", 7 | "organization": true 8 | }, 9 | "license": "Apache-2.0", 10 | "version": "1.1.15", 11 | "private": true, 12 | "scripts": { 13 | "serve": "vue-cli-service serve", 14 | "build": "vue-cli-service build", 15 | "lint": "vue-cli-service lint" 16 | }, 17 | "dependencies": { 18 | "axios": "^1.12.1", 19 | "axios-retry": "^3.3.1", 20 | "bootstrap": "^5.1.1", 21 | "core-js": "^3.8.3", 22 | "lodash": "^4.17.21", 23 | "moment": "^2.29.1", 24 | "simple-statistics": "^7.7.0", 25 | "vue": "^3.3.7", 26 | "vue-router": "^4.2.5", 27 | "vue-timers": "^2.0.4", 28 | "vuex": "^4.1.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.23.2", 32 | "@vue/babel-preset-app": "^5.0.8", 33 | "@vue/cli-plugin-babel": "^5.0.8", 34 | "@vue/cli-plugin-eslint": "^5.0.8", 35 | "@vue/cli-plugin-router": "^5.0.8", 36 | "@vue/cli-service": "^5.0.8", 37 | "@vue/compiler-sfc": "^3.3.7", 38 | "@vue/component-compiler-utils": "^3.3.0", 39 | "eslint": "^8.20.0", 40 | "eslint-plugin-vue": "^9.3.0", 41 | "naive-ui": "^2.19.3", 42 | "postcss": "^8.4.31", 43 | "vfonts": "^0.1.0" 44 | }, 45 | "eslintConfig": { 46 | "root": true, 47 | "env": { 48 | "node": true 49 | }, 50 | "extends": [ 51 | "plugin:vue/base", 52 | "eslint:recommended" 53 | ], 54 | "parserOptions": {}, 55 | "rules": {} 56 | }, 57 | "overrides": { 58 | "cross-spawn": "^7.0.6", 59 | "postcss": "^8.4.31", 60 | "webpack-dev-server": "^5.2.1", 61 | "semver": "^7.5.2", 62 | "word-wrap": "^1.2.4" 63 | }, 64 | "browserslist": [ 65 | "> 1%", 66 | "last 2 versions", 67 | "not dead" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 38 | 39 | 66 | -------------------------------------------------------------------------------- /deployment/virtual-waiting-room-on-aws-api-gateway-cw-logs-role.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "(SO0166) - Virtual Waiting Room on AWS API Gateway CW Logs Role %%VERSION%%", 4 | "Resources": { 5 | "CloudWatchRole": { 6 | "Type": "AWS::IAM::Role", 7 | "Properties": { 8 | "AssumeRolePolicyDocument": { 9 | "Version": "2012-10-17", 10 | "Statement": [ 11 | { 12 | "Effect": "Allow", 13 | "Principal": { 14 | "Service": [ 15 | "apigateway.amazonaws.com" 16 | ] 17 | }, 18 | "Action": "sts:AssumeRole" 19 | } 20 | ] 21 | }, 22 | "Policies": [ 23 | { 24 | "PolicyDocument": { 25 | "Version": "2012-10-17", 26 | "Statement": [ 27 | { 28 | "Effect": "Allow", 29 | "Resource": {"Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:*"} , 30 | "Action": [ 31 | "logs:CreateLogGroup", 32 | "logs:CreateLogStream", 33 | "logs:DescribeLogGroups", 34 | "logs:DescribeLogStreams", 35 | "logs:PutLogEvents", 36 | "logs:GetLogEvents", 37 | "logs:FilterLogEvents" 38 | ] 39 | } 40 | ] 41 | }, 42 | "PolicyName": "CW-Logging-Policy" 43 | } 44 | ] 45 | }, 46 | "Metadata": { 47 | "guard": { 48 | "SuppressedRules": [ 49 | "IAM_NO_INLINE_POLICY_CHECK" 50 | ] 51 | } 52 | } 53 | }, 54 | "Account": { 55 | "Type": "AWS::ApiGateway::Account", 56 | "Properties": { 57 | "CloudWatchRoleArn": { 58 | "Fn::GetAtt": [ 59 | "CloudWatchRole", 60 | "Arn" 61 | ] 62 | } 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /source/control-panel/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 23 | 24 | 61 | 62 | 66 | -------------------------------------------------------------------------------- /source/core-api-authorizers-sample/chalice/sample_api_tests.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is provides unit tests for the inlet strategies module. 6 | """ 7 | 8 | # pylint: disable=C0415,W0201,E1101 9 | 10 | import unittest 11 | from unittest.mock import patch, MagicMock 12 | 13 | 14 | class SampleAPITestException(Exception): 15 | """ 16 | This class is an exception subclass for unit testing errors 17 | """ 18 | 19 | def __init__(self, *args: object) -> None: 20 | super().__init__(*args) 21 | 22 | 23 | def chalice_mock(app_name=None): 24 | """ 25 | This function handles the mock for the Chalice object 26 | """ 27 | mock = MagicMock() 28 | mock.current_request = MagicMock() 29 | mock.current_request.query_params = MagicMock() 30 | mock.current_request.query_params.get = MagicMock() 31 | mock.current_request.query_params.get.return_value = "" 32 | return mock 33 | 34 | 35 | @patch('boto3.resource') 36 | @patch('boto3.client') 37 | @patch('requests.post') 38 | @patch('requests.get') 39 | @patch('chalice.Chalice', new=chalice_mock) 40 | class TestSampleAPI(unittest.TestCase): 41 | """ 42 | This class extends TestCase with testing functions 43 | """ 44 | 45 | def test_checkout(self, patched_resource, patched_client, patched_post, 46 | patched_get): 47 | """ 48 | Test the checkout function 49 | """ 50 | import app 51 | app.checkout() 52 | 53 | def test_search(self, patched_resource, patched_client, patched_post, 54 | patched_get): 55 | """ 56 | Test the search function 57 | """ 58 | import app 59 | app.search() 60 | 61 | def test_layaway(self, patched_resource, patched_client, patched_post, 62 | patched_get): 63 | """ 64 | Test the layaway function 65 | """ 66 | import app 67 | app.layaway() 68 | 69 | 70 | if __name__ == '__main__': 71 | unittest.main() 72 | -------------------------------------------------------------------------------- /source/core-api/lambda_functions/get_serving_num.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the get_serving_num API handler. 6 | It retrieves the number currently being served by the waiting room. 7 | """ 8 | 9 | import redis 10 | import json 11 | import os 12 | import boto3 13 | from botocore import config 14 | from counters import SERVING_COUNTER 15 | from vwr.common.sanitize import deep_clean 16 | 17 | # connection info and other globals 18 | REDIS_HOST = os.environ["REDIS_HOST"] 19 | REDIS_PORT = os.environ["REDIS_PORT"] 20 | EVENT_ID = os.environ["EVENT_ID"] 21 | SECRET_NAME_PREFIX = os.environ["STACK_NAME"] 22 | SOLUTION_ID = os.environ['SOLUTION_ID'] 23 | 24 | boto_session = boto3.session.Session() 25 | region = boto_session.region_name 26 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 27 | user_config = config.Config(**user_agent_extra) 28 | secrets_client = boto3.client('secretsmanager', config=user_config, endpoint_url=f"https://secretsmanager.{region}.amazonaws.com") 29 | 30 | response = secrets_client.get_secret_value(SecretId=f"{SECRET_NAME_PREFIX}/redis-auth") 31 | redis_auth = response.get("SecretString") 32 | rc = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, ssl=True, decode_responses=True, password=redis_auth) 33 | 34 | def lambda_handler(event, _): 35 | """ 36 | This function is the entry handler for Lambda. 37 | """ 38 | 39 | print(event) 40 | client_event_id = deep_clean(event['queryStringParameters']['event_id']) 41 | headers = { 42 | 'Content-Type': 'application/json', 43 | 'Access-Control-Allow-Origin': '*' 44 | } 45 | 46 | if client_event_id != EVENT_ID: 47 | return { 48 | "statusCode": 400, 49 | "headers": headers, 50 | "body": json.dumps({"error": "Invalid event ID"}) 51 | } 52 | 53 | cur_serving = rc.get(SERVING_COUNTER) 54 | print(cur_serving) 55 | 56 | return { 57 | "statusCode": 200, 58 | "headers": headers, 59 | "body": json.dumps({"serving_counter": cur_serving}) 60 | } 61 | -------------------------------------------------------------------------------- /docs/use-cases.drawio: -------------------------------------------------------------------------------- 1 | 7VvLcqM4FP2aLJMCJMAsHcfJpCbpcrW7Jz2zI6CAKhh5hPBjvn6EeRgk7DhuDLjibIIuQqCjc3Vf8hUYzVYP1J77z8RFwZWmuKsrcHel8T8D8H+JZJ1KBtBKBR7FbipSt4Ip/g9lQiWTxthFUaUjIyRgeF4VOiQMkcMqMptSsqx2eyNB9a1z20OSYOrYgSx9wS7zM6mqKNsbfyDs+dmrB3p2Y2bnnTNB5NsuWZZEYHwFRpQQll7NViMUJODluKTP3e+4W3wYRSE75IGxce88YMu/W6rPdKVP/0Th+3U2ysIO4mzCo4DELhcN3RkOccSozQjNZsDWOSx8MvPkMp4FQyfpAG4XiDLMgXuyX1EwIRFmmIS8yythjMxKHYYB9pIbjMy51GezgDdUfinPKP88/iRalUTZDB8QmSFG17xLdtfIwM7Ypmp62l5u1w5kXfzSquWP2RlbvGLgLaD8IsP0E/hq9fi+2MzxuXzq+MiNA3Q+AEOrirBmKt0iDCWEh5NHLhgFOJnlueKq6x3jasnMpchmiMt+8A04IS+znXcJYD4035t543bpY4amc9tJ7iy5eWgctGJvXVcBKmGmwhrQzFOBllutEmo/527PUVO1zmGTzdAdClC/YStclO5gk62LjFDoDhM3iLecwI4i7FSBQSvMfvFr5UbPWn9XWnfJ7JW8sS41JohiPg9uuzJZyOf0q9wojZQ0t0NtWvlYO5cmIjF10MfOC7Oph9jHWxlyK66evNClhdRr1jGXURTYDC+qDmLd4mZvmBCcGKJipwdVHkFxC0/nnT1VdujEgQbCQKowUAqMNNCGa8W0f4N+4EK/Q+iXL8uFfw3zT3b9LvzbY10v/GuYf4bEv1tKbNexo+QrX2weSIQev/pOeBihKc/8fdiJOnViTFVwYgw5QG7XiQFyoDGJXwOuqJqSRnLf0b8xiuRQrkvcQE3Y2y5u+cvKuFG8SGON/gJndO41QznY+LzZOHKzb87cHG824IFmA/TLbQa7EiSfNhtQGEhp12zAJqK2L8A/2C+3+Rz59+SgVRiTh0HwzSKzt1toPP5Tk/Kf4o3R+BmdUR76WsjGwBYz/bWwylo9DhN1kR3BDq2xWB8BoEX3rxY2OYfwF+aM4qKUlR2CVdT4Coq16LrUgqV/Gcuxb+cqW459nOqJ4bhWNXHD14+zHNemQEhdGOjElkOOd8eLTY0t09UzrRnrQohiyWreqiUxJZhHJHzDXkwTjAXIH7mR8TjeCW59ivPaLMXVojjYi+JjGKANiglXkbfuFXYFUJ2BJ+dkvpShMQ40NGavDA0Uc3tiYHFwhCKeohFDnVNHKHJq60K/3VvchX4N06+JBGEPEi2aTKO9dYye8EhX9erya0e6y6Z4YEMc6NQ8aqA+efwmAg7cRLR+LX5lxSA4culVMVlntLz0cqQ+ocSjKIq49IdPSewl5z97nBoq1LAzJ7Smvtqi/tTsnnuXup8KJPL+bBRIDoJbXPt8Tc8s0wQU7QYapgo0oENdNcwqFxTzZqABzQKGquqmNTCOowaA5g00B9DSoKkoUNqyzRvVMqGpDyB/ianAdnlz+GkCZcnf8I0w/Iad/uVPiqpQZ9uvJkdh4xXjSldzgmWria8Bcd43UVX+Yx6+jW+a9zh5/8anbUZJLVlJGytkq0ZDQZGQsgXHVg0N8MFAJ1YsTQ6Kdh436bdmmZ1n13Jb1lfNyjuepER/Ks3SYUOaJQ10as2SS6BV3VGecMTZcUbFeVWRTn11XZ1v4qxwkVLcZhEPTComrQbroDWWby+1euKfipmd4gDv72Z2jJYLoVqjhfiO2VS3258Fm8TIwzqWTYIBMBqLdXlz+/vutPv2V/Jg/D8= -------------------------------------------------------------------------------- /source/openid-waitingroom/custom_resources/generate_redirect_uris_secret.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is a Lambda function custom resource for CloudFormation. It is 6 | responsible for creating and deleting the allowed redirect URIs secret for 7 | the Open ID adapter for the waiting room. 8 | """ 9 | 10 | import os 11 | import json 12 | import boto3 13 | from crhelper import CfnResource 14 | from botocore import config 15 | 16 | helper = CfnResource() 17 | 18 | PLACEHOLDER_URIS = [ 19 | "https://DNS/oauth2/idpresponse", "https://CNAME/oauth2/idpresponse" 20 | ] 21 | 22 | SOLUTION_ID = os.environ['SOLUTION_ID'] 23 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 24 | user_config = config.Config(**user_agent_extra) 25 | 26 | @helper.create 27 | def create(event, _): 28 | """ 29 | This function is responsible for creating a new redirect URIs secret 30 | """ 31 | print(event) 32 | # create the secret value 33 | secrets_manager = boto3.client('secretsmanager', config=user_config) 34 | secret_value = json.dumps(PLACEHOLDER_URIS, indent=4) 35 | # store the value in a new secret 36 | secret_id = f'{event["ResourceProperties"]["SecretPrefix"]}/redirect_uris' 37 | secrets_manager.create_secret( 38 | Name=secret_id, 39 | Description="Open ID allowed redirect URIs after authentication", 40 | SecretString=secret_value 41 | ) 42 | 43 | 44 | @helper.delete 45 | def delete(event, _): 46 | """ 47 | This function is responsible for deleting the redirect URIs secret 48 | """ 49 | print(event) 50 | secrets_manager = boto3.client('secretsmanager', config=user_config) 51 | # delete the secret value 52 | secret_id = f'{event["ResourceProperties"]["SecretPrefix"]}/redirect_uris' 53 | secrets_manager.delete_secret( 54 | SecretId=secret_id, 55 | ForceDeleteWithoutRecovery=True 56 | ) 57 | 58 | 59 | def handler(event, context): 60 | """ 61 | This is the entry point for the custom resource Lambda function. 62 | """ 63 | helper(event, context) 64 | -------------------------------------------------------------------------------- /source/openid-waitingroom/custom_resources/generate_client_secret.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is a Lambda function custom resource for CloudFormation. It is 6 | responsible for creating and deleting the client secret for the Open ID 7 | adapter for the waiting room. 8 | """ 9 | 10 | import os 11 | import boto3 12 | from crhelper import CfnResource 13 | from botocore import config 14 | 15 | helper = CfnResource() 16 | 17 | PASSWORD_LENGTH = 50 18 | 19 | SOLUTION_ID = os.environ['SOLUTION_ID'] 20 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 21 | user_config = config.Config(**user_agent_extra) 22 | 23 | @helper.create 24 | def create(event, _): 25 | """ 26 | This function is responsible for creating a new client secret 27 | """ 28 | print(event) 29 | # create the secret value 30 | secrets_manager = boto3.client('secretsmanager', config=user_config) 31 | secret_value = secrets_manager.get_random_password( \ 32 | PasswordLength=PASSWORD_LENGTH, ExcludePunctuation=True).get('RandomPassword') 33 | # store the value in a new secret named /client_secret 34 | secret_id = f'{event["ResourceProperties"]["SecretPrefix"]}/client_secret' 35 | secrets_manager.create_secret( 36 | Name=secret_id, 37 | Description="Open ID client secret used for token requests", 38 | SecretString=secret_value 39 | ) 40 | 41 | 42 | @helper.delete 43 | def delete(event, _): 44 | """ 45 | This function is responsible for deleting the client secret 46 | """ 47 | print(event) 48 | # delete the secret value 49 | secrets_manager = boto3.client('secretsmanager', config=user_config) 50 | secret_id = f'{event["ResourceProperties"]["SecretPrefix"]}/client_secret' 51 | secrets_manager.delete_secret( 52 | SecretId=secret_id, 53 | ForceDeleteWithoutRecovery=True 54 | ) 55 | 56 | 57 | def handler(event, context): 58 | """ 59 | This is the entry point for the custom resource Lambda function. 60 | """ 61 | helper(event, context) 62 | -------------------------------------------------------------------------------- /source/core-api-authorizers-sample/chalice/app.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | """ 4 | This module represents a client's API that uses the waiting room API Gateway authorizers 5 | to perform store operations after the user has received a token to pass through 6 | the waiting room. 7 | """ 8 | 9 | from chalice import Chalice, CustomAuthorizer, CORSConfig, Response 10 | 11 | app = Chalice(app_name='core-api-authorizers-sample') 12 | 13 | WAITING_ROOM_AUTH = CustomAuthorizer('WaitingRoomAuthorizer', 14 | header='Authorization', 15 | authorizer_uri='PLACEHOLDER') 16 | 17 | CORS_CONFIG = CORSConfig(allow_origin='*', 18 | allow_headers=['*'], 19 | max_age=600, 20 | expose_headers=['*'], 21 | allow_credentials=True) 22 | 23 | RESPONSE_HEADERS = { 24 | 'Content-Type': 'application/json', 25 | 'Access-Control-Allow-Origin': '*' 26 | } 27 | 28 | 29 | @app.route('/checkout', 30 | methods=['GET'], 31 | authorizer=WAITING_ROOM_AUTH, 32 | cors=CORS_CONFIG) 33 | def checkout(): 34 | """ 35 | This function represents a /checkout API endpoint. 36 | """ 37 | return Response(status_code=200, 38 | body={"result": "Success"}, 39 | headers=RESPONSE_HEADERS) 40 | 41 | 42 | @app.route('/search', 43 | methods=['GET'], 44 | authorizer=WAITING_ROOM_AUTH, 45 | cors=CORS_CONFIG) 46 | def search(): 47 | """ 48 | This function represents a /search API endpoint. 49 | """ 50 | return Response(status_code=200, 51 | body={"result": "Success"}, 52 | headers=RESPONSE_HEADERS) 53 | 54 | 55 | @app.route('/layaway', 56 | methods=['GET'], 57 | authorizer=WAITING_ROOM_AUTH, 58 | cors=CORS_CONFIG) 59 | def layaway(): 60 | """ 61 | This function represents a /layaway API endpoint. 62 | """ 63 | return Response(status_code=200, 64 | body={"result": "Success"}, 65 | headers=RESPONSE_HEADERS) 66 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/main.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /*jshint esversion: 6 */ 5 | 6 | import { createApp } from 'vue'; 7 | import { createStore } from 'vuex'; 8 | import App from './App.vue'; 9 | import router from './router'; 10 | 11 | const store = createStore({ 12 | state() { 13 | return { 14 | publicApiUrl: "", 15 | eventId: "", 16 | requestId: null, 17 | myPosition: 0, 18 | queuePosition: 0, 19 | token: null, 20 | receipt: null, 21 | commerceApiUrl: "", 22 | launchQueryParameters: "", 23 | passThru: false 24 | }; 25 | }, 26 | mutations: { 27 | setRequestId(state, id) { 28 | state.requestId = id; 29 | }, 30 | setMyPosition(state, position) { 31 | state.myPosition = position; 32 | }, 33 | setQueuePosition(state, position) { 34 | state.queuePosition = position; 35 | }, 36 | setToken(state, token) { 37 | state.token = token; 38 | }, 39 | setPublicApiUrl(state, url) { 40 | state.publicApiUrl = url; 41 | }, 42 | setEventId(state, id) { 43 | state.eventId = id; 44 | }, 45 | setReceipt(state, receipt) { 46 | state.receipt = receipt; 47 | }, 48 | setCommerceApiUrl(state, url) { 49 | state.commerceApiUrl = url; 50 | }, 51 | setLaunchQueryParameters(state, query) { 52 | state.launchQueryParameters = query; 53 | }, 54 | setPassThru(state, value) { 55 | state.passThru = value; 56 | } 57 | }, 58 | getters: 59 | { 60 | hasRequestId(state) { 61 | return state.requestId !== null; 62 | }, 63 | hasQueuePosition(state) { 64 | return state.myPosition > 0; 65 | }, 66 | hasToken(state) { 67 | return state.token !== null; 68 | }, 69 | hasReceipt(state) { 70 | return state.receipt !== null; 71 | } 72 | } 73 | }); 74 | 75 | const app = createApp(App); 76 | app.use(router); 77 | app.use(store); 78 | app.mount('#app'); 79 | -------------------------------------------------------------------------------- /source/core-api/lambda_functions/get_waiting_num.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the get_waiting_num API handler. 6 | It retrieves the number currently queued in the waiting room and have not been issued a token yet. 7 | """ 8 | 9 | 10 | import redis 11 | import json 12 | import os 13 | import boto3 14 | from botocore import config 15 | from counters import QUEUE_COUNTER, TOKEN_COUNTER, EXPIRED_QUEUE_COUNTER 16 | from vwr.common.sanitize import deep_clean 17 | 18 | # connection info and other globals 19 | REDIS_HOST = os.environ["REDIS_HOST"] 20 | REDIS_PORT = os.environ["REDIS_PORT"] 21 | EVENT_ID = os.environ["EVENT_ID"] 22 | SECRET_NAME_PREFIX = os.environ["STACK_NAME"] 23 | SOLUTION_ID = os.environ['SOLUTION_ID'] 24 | 25 | boto_session = boto3.session.Session() 26 | region = boto_session.region_name 27 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 28 | user_config = config.Config(**user_agent_extra) 29 | secrets_client = boto3.client('secretsmanager', config=user_config, endpoint_url=f"https://secretsmanager.{region}.amazonaws.com") 30 | 31 | response = secrets_client.get_secret_value(SecretId=f"{SECRET_NAME_PREFIX}/redis-auth") 32 | redis_auth = response.get("SecretString") 33 | rc = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, ssl=True, decode_responses=True, password=redis_auth) 34 | 35 | def lambda_handler(event, _): 36 | """ 37 | This function is the entry handler for Lambda. 38 | """ 39 | 40 | print(event) 41 | client_event_id = deep_clean(event['queryStringParameters']['event_id']) 42 | headers = { 43 | 'Content-Type': 'application/json', 44 | 'Access-Control-Allow-Origin': '*' 45 | } 46 | 47 | if client_event_id != EVENT_ID: 48 | return { 49 | "statusCode": 400, 50 | "headers": headers, 51 | "body": json.dumps({"error": "Invalid event ID"}) 52 | } 53 | 54 | queue_count = int(rc.get(QUEUE_COUNTER)) 55 | token_count = int(rc.get(TOKEN_COUNTER)) 56 | expired_queue_count = int(rc.get(EXPIRED_QUEUE_COUNTER)) if rc.get(EXPIRED_QUEUE_COUNTER) else 0 57 | waiting_num = queue_count - token_count - expired_queue_count 58 | 59 | return { 60 | "statusCode": 200, 61 | "headers": headers, 62 | "body": json.dumps({"waiting_num": waiting_num}) 63 | } 64 | -------------------------------------------------------------------------------- /source/control-panel/src/components/Credentials.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 44 | 45 | 77 | -------------------------------------------------------------------------------- /source/core-api/custom_resources/update_distribution.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the custom resource used to update the CloudFront distribution used by the public API. 6 | Initial creation of the distribution uses API ID for the API key header. 7 | This update sets the header to use to actual API key of the API. 8 | """ 9 | 10 | import os 11 | from crhelper import CfnResource 12 | import boto3 13 | from botocore import config 14 | 15 | DISTRIBUTION_ID = os.environ.get("DISTRIBUTION_ID") 16 | API_KEY_ID = os.environ.get("API_KEY_ID") 17 | SOLUTION_ID = os.environ['SOLUTION_ID'] 18 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 19 | user_config = config.Config(**user_agent_extra) 20 | 21 | # connection info and other globals 22 | helper = CfnResource() 23 | cf_client = boto3.client('cloudfront', config=user_config) 24 | api_client = boto3.client('apigateway', config=user_config) 25 | 26 | @helper.create 27 | @helper.update 28 | def create_update(event, _): 29 | """ 30 | This function is responsible for updating the CloudFront distribution's x-api-key header 31 | with actual API key value. The API key is regenerated during stack updates so this needs 32 | to run not only during create, but update as well. 33 | """ 34 | print(event) 35 | response = cf_client.get_distribution_config(Id=DISTRIBUTION_ID) 36 | # get config and ETag 37 | distribution_config = response['DistributionConfig'] 38 | etag = response["ETag"] 39 | api_key = api_client.get_api_key( 40 | apiKey=API_KEY_ID, 41 | includeValue=True 42 | ) 43 | # update header with actual API key value 44 | distribution_config["Origins"]["Items"][0]["CustomHeaders"]["Items"][0]["HeaderValue"] = api_key["value"] 45 | # update distribution with new header value 46 | response = cf_client.update_distribution( 47 | Id=DISTRIBUTION_ID, 48 | DistributionConfig=distribution_config, 49 | IfMatch=etag 50 | ) 51 | print(response) 52 | 53 | 54 | @helper.delete 55 | def delete(event, _): 56 | """ 57 | Distribution is not deleted as it is created natively in the API core template. 58 | CloudFormation will handle its deletion. 59 | """ 60 | print(event) 61 | print("Not deleting distribution. CloudFormation will handle this.") 62 | 63 | 64 | def handler(event, context): 65 | """ 66 | This function is the entry point for the Lambda-backed custom resource. 67 | """ 68 | helper(event, context) 69 | -------------------------------------------------------------------------------- /source/tools/load_test_prepare.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is responsible for preparing the waiting room for a load test. 6 | 1. Reset the waiting room via API 7 | 2. Update the inlet handler Lambda function environment variables 8 | with desired rate and duration 9 | 10 | AWS credentials from the environment are required. 11 | """ 12 | 13 | import json 14 | import os 15 | import time 16 | from urllib.parse import urlparse 17 | 18 | import boto3 19 | import requests 20 | from aws_requests_auth.boto_utils import BotoAWSRequestsAuth 21 | 22 | PRIVATE_API_ENDPOINT = os.environ["PRIVATE_API_ENDPOINT"] 23 | PRIVATE_API_REGION = os.environ["PRIVATE_API_REGION"] 24 | INLET_LAMBDA_NAME = os.environ["INLET_LAMBDA_NAME"] 25 | EVENT_ID = os.environ["EVENT_ID"] 26 | 27 | INCREMENT = 1000 28 | HOLD_OFF = 30 29 | DURATION = 7200 30 | 31 | 32 | def reset_waiting_room(): 33 | """ 34 | This function is responsible for calling the reset_initial_state API 35 | """ 36 | api = f"{PRIVATE_API_ENDPOINT}/reset_initial_state" 37 | body = {"event_id": EVENT_ID} 38 | parsed = urlparse(PRIVATE_API_ENDPOINT) 39 | auth = BotoAWSRequestsAuth(aws_host=parsed.netloc, 40 | aws_region=PRIVATE_API_REGION, 41 | aws_service='execute-api') 42 | response = requests.post(api, json=body, auth=auth, timeout=25) 43 | print(f"/reset_initial_state {response.status_code}") 44 | 45 | 46 | def update_inlet_run_window(): 47 | """ 48 | This function is responsible for updating the time and increment on 49 | the periodic inlet Lambda function. 50 | """ 51 | client = boto3.client("lambda") 52 | response = client.get_function_configuration( 53 | FunctionName=INLET_LAMBDA_NAME) 54 | environment = response["Environment"]["Variables"] 55 | # start in 1 minute from now 56 | start_ingest = int(time.time()) + HOLD_OFF 57 | # stop after 60 minutes 58 | # stop_ingest = start_ingest + DURATION 59 | # update the Lambda 60 | environment["START_TIME"] = f"{start_ingest}" 61 | # environment["END_TIME"] = f"{stop_ingest}" 62 | environment["END_TIME"] = "0" 63 | environment["INCREMENT_BY"] = f"{INCREMENT}" 64 | response = client.update_function_configuration( 65 | FunctionName=INLET_LAMBDA_NAME, Environment={"Variables": environment}) 66 | print(json.dumps(response, indent=4)) 67 | 68 | 69 | if __name__ == "__main__": 70 | reset_waiting_room() 71 | update_inlet_run_window() 72 | -------------------------------------------------------------------------------- /source/control-panel/src/components/Endpoints.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | 46 | 47 | 81 | -------------------------------------------------------------------------------- /source/core-api/lambda_functions/get_list_expired_tokens.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the get_list_expired_tokens API handler. 6 | It queries DynamoDB for requests that were issued tokens which have since expired. 7 | """ 8 | 9 | import json 10 | import boto3 11 | import os 12 | import time 13 | from botocore import config 14 | from boto3.dynamodb.conditions import Key 15 | from vwr.common.sanitize import deep_clean 16 | 17 | DDB_TOKEN_TABLE_NAME = os.environ["TOKEN_TABLE"] 18 | EVENT_ID = os.environ["EVENT_ID"] 19 | SOLUTION_ID = os.environ['SOLUTION_ID'] 20 | 21 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 22 | user_config = config.Config(**user_agent_extra) 23 | boto_session = boto3.session.Session() 24 | region = boto_session.region_name 25 | ddb_resource = boto3.resource('dynamodb', config=user_config) 26 | ddb_table = ddb_resource.Table(DDB_TOKEN_TABLE_NAME) 27 | 28 | def lambda_handler(event, _): 29 | """ 30 | This function is the entry handler for Lambda. 31 | """ 32 | 33 | print(event) 34 | client_event_id = deep_clean(event['queryStringParameters']['event_id']) 35 | headers = { 36 | 'Content-Type': 'application/json', 37 | 'Access-Control-Allow-Origin': '*' 38 | } 39 | if client_event_id == EVENT_ID: 40 | try: 41 | current_time = int(time.time()) 42 | response = ddb_table.query( 43 | IndexName="EventExpiresIndex", 44 | ProjectionExpression="request_id", 45 | KeyConditionExpression=Key('event_id').eq(EVENT_ID) & Key('expires').lt(current_time)) 46 | items = [item['request_id'] for item in response['Items']] 47 | while "LastEvaluatedKey" in response: 48 | response = ddb_table.query( 49 | IndexName="EventExpiresIndex", 50 | ProjectionExpression="request_id", 51 | KeyConditionExpression=Key('event_id').eq(EVENT_ID) & Key('expires').lt(current_time), 52 | ExclusiveStartKey=response["LastEvaluatedKey"]) 53 | items.extend([item['request_id'] for item in response['Items']]) 54 | response = { 55 | "statusCode": 200, 56 | "headers": headers, 57 | "body": json.dumps(items) 58 | } 59 | except Exception as e: 60 | print(e) 61 | raise e 62 | else: 63 | response = { 64 | "statusCode": 400, 65 | "headers": headers, 66 | "body": json.dumps({"error": "Invalid event ID"}) 67 | } 68 | print(response) 69 | return response 70 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/views/WaitingRoom.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 37 | 38 | 89 | -------------------------------------------------------------------------------- /source/control-panel/src/components/WaitingRoomSize.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | 21 | 22 | 80 | -------------------------------------------------------------------------------- /source/shared/shared_resources_tests.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | """ 4 | This module is the unit test for the shared functions. 5 | """ 6 | 7 | import os 8 | import unittest 9 | from unittest.mock import patch 10 | from unittest.mock import MagicMock 11 | 12 | os.environ["SOLUTION_ID"] = "SO12345" 13 | 14 | from custom_resources import cfn_bucket_loader 15 | 16 | 17 | class CustomResourcesSharedTest(unittest.TestCase): 18 | """ 19 | This class is reponsible for tests against the waiting room core custom resources functions 20 | """ 21 | 22 | def setUp(self): 23 | """ 24 | This function is responsible for setting up the overall environment before each test 25 | """ 26 | 27 | def tearDown(self): 28 | """ 29 | This function is responsible for cleaning up the test environment 30 | """ 31 | print( 32 | "----------------------------------------------------------------") 33 | 34 | @patch('boto3.client') 35 | def test_cfn_bucket_loader_update_web_content(self, mock_client): 36 | """ 37 | Test for update web contents 38 | """ 39 | mock_event = { 40 | "ResourceProperties": { 41 | "BucketName": "bucket_123", 42 | "APIs": { 43 | "url1": "http://url1/something", 44 | "url2": "http://url2/something" 45 | } 46 | } 47 | } 48 | 49 | bkt_loader = cfn_bucket_loader 50 | bkt_loader.delete_bucket_contents = MagicMock() 51 | bkt_loader.put_web_contents = MagicMock() 52 | bkt_loader.put_api_endpoints_js = MagicMock() 53 | 54 | cfn_bucket_loader.update_web_content(mock_event, None) 55 | bkt_loader.delete_bucket_contents.assert_called_with("bucket_123") 56 | bkt_loader.put_web_contents.assert_called_with("bucket_123") 57 | bkt_loader.put_api_endpoints_js.assert_called_with( 58 | "bucket_123", { 59 | "url1": "http://url1/something", 60 | "url2": "http://url2/something" 61 | }) 62 | 63 | @patch('boto3.client') 64 | def test_cfn_bucket_loader_delete_web_content(self, mock_client): 65 | """ 66 | Test for delete bucket contents 67 | """ 68 | mock_event = { 69 | "ResourceProperties": { 70 | "BucketName": "bucket_123", 71 | } 72 | } 73 | 74 | bkt_loader = cfn_bucket_loader 75 | bkt_loader.delete_bucket_contents = MagicMock() 76 | 77 | cfn_bucket_loader.delete_web_content(mock_event, None) 78 | bkt_loader.delete_bucket_contents.assert_called_with("bucket_123") 79 | 80 | 81 | if __name__ == "__main": 82 | unittest.main() 83 | -------------------------------------------------------------------------------- /source/shared/virtual-waiting-room-on-aws-common/common_tests.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the unit test for the shared functions. 6 | """ 7 | 8 | import os 9 | import unittest 10 | 11 | os.environ["SOLUTION_ID"] = "SO12345" 12 | 13 | from vwr.common import diag, jwt, sanitize, validate 14 | 15 | class CommonTest(unittest.TestCase): 16 | """ 17 | This class is reponsible for tests against the waiting room core custom resources functions 18 | """ 19 | 20 | def setUp(self): 21 | """ 22 | This function is responsible for setting up the overall environment before each test 23 | """ 24 | 25 | def tearDown(self): 26 | """ 27 | This function is responsible for cleaning up the test environment 28 | """ 29 | print("----------------------------------------------------------------") 30 | 31 | 32 | def test_diag(self): 33 | """ 34 | Tests valid rid 35 | """ 36 | try: 37 | raise RuntimeError() 38 | except RuntimeError: 39 | diag.print_exception() 40 | 41 | 42 | 43 | def test_is_valid_rid(self): 44 | """ 45 | Tests valid rid 46 | """ 47 | request_id = "26a07142-098a-4bea-b67d-1fe5a098bf29" 48 | self.assertTrue(validate.is_valid_rid(request_id)) 49 | 50 | request_id = "1-26a07142-098a-4bea-b67d1fe5a098bf29" 51 | self.assertFalse(validate.is_valid_rid(request_id)) 52 | 53 | request_id = "" 54 | self.assertFalse(validate.is_valid_rid(request_id)) 55 | 56 | 57 | def test_sanitize(self): 58 | """ 59 | Tests sanitize 60 | """ 61 | sanitize.deep_clean("") 62 | 63 | 64 | 65 | def test_jwt_payload(self): 66 | """ 67 | Function to test JWT payload 68 | """ 69 | text = 'A.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.B' 70 | 71 | payload = jwt.claim_dict(text) 72 | expected = {'sub': '1234567890', 'name': 'John Doe', 'iat': 1516239022 } 73 | self.assertEqual(payload, expected) 74 | 75 | text = 'A.eyJzdWIiOiIyMTM0NTY3ODk0IiwibmFtZSI6Ik1hcnkgSmFuZSIsImlhdCI6MTUxNjIzOTAyNX0.B' 76 | payload = jwt.claim_dict(text) 77 | expected = {'sub': '2134567894', 'name': 'Mary Jane', 'iat': 1516239025 } 78 | self.assertEqual(payload, expected) 79 | 80 | text = 'A.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhlbGxvIHdvcmxkIiwiaWF0IjoxNTE2MjM5MDIyfQ.B' 81 | payload = jwt.claim_dict(text) 82 | expected = {'sub': '1234567890', 'name': 'Mary Jane', 'iat': 1516239022 } 83 | self.assertNotEqual(payload, expected) 84 | 85 | 86 | if __name__ == "__main": 87 | unittest.main() 88 | -------------------------------------------------------------------------------- /source/sample-inlet-strategies/inlet_strategy_tests.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is provides unit tests for the inlet strategies module. 6 | """ 7 | 8 | # pylint: disable=C0415,W0201 9 | 10 | import json 11 | import time 12 | import unittest 13 | from unittest.mock import patch 14 | 15 | SNS_EVENT = { 16 | "Records": [{ 17 | "Sns": { 18 | "Message": 19 | json.dumps({ 20 | "completed": ["FC4FBD08-D930-42CD-8F0E-D7333B5979A5"], 21 | "abandoned": ["5188734F-7FFC-4AA0-B485-24660723F8AC"], 22 | "exited": "10" 23 | }), 24 | "Subject": 25 | "TestInvoke" 26 | } 27 | }] 28 | } 29 | 30 | 31 | class InletStrategyUnitTestException(Exception): 32 | """ 33 | This class is an exception subclass for unit testing errors 34 | """ 35 | 36 | def __init__(self, *args: object) -> None: 37 | super().__init__(*args) 38 | 39 | 40 | def environ_get_mock(key, default_value=None): 41 | """ 42 | This function is the mocked (replaced) function for returning environment variables 43 | """ 44 | result = "" 45 | if key == "EVENT_ID": 46 | result = "641EE9DD-57BE-437E-B157-BAD15F3D6408" 47 | elif key == "CORE_API_ENDPOINT": 48 | result = "https://www.example.com" 49 | elif key == "CORE_API_REGION": 50 | result = "us-east-1" 51 | elif key == "MAX_SIZE": 52 | result = "100" 53 | elif key == "INCREMENT_BY": 54 | result = "100" 55 | elif key == "START_TIME": 56 | result = str(int(time.time())) 57 | elif key == "END_TIME": 58 | result = str(int(time.time())) 59 | elif key == "CLOUDWATCH_ALARM": 60 | result = "NonExistantAlarmName" 61 | elif default_value is not None: 62 | result = default_value 63 | return result 64 | 65 | 66 | @patch('os.environ.get', new=environ_get_mock) 67 | @patch('boto3.resource') 68 | @patch('boto3.client') 69 | @patch('requests.post') 70 | @patch('requests.get') 71 | class TestInletStrategies(unittest.TestCase): 72 | """ 73 | This class extends TestCase with testing functions 74 | """ 75 | 76 | def test_max_size_inlet(self, patched_resource, patched_client, 77 | patched_post, patched_get): 78 | """ 79 | Test the max size inlet strategy 80 | """ 81 | import max_size_inlet 82 | max_size_inlet.lambda_handler(SNS_EVENT, {}) 83 | 84 | def test_periodic_inlet(self, patched_resource, patched_client, 85 | patched_post, patched_get): 86 | """ 87 | Test the periodic inlet strategy 88 | """ 89 | import periodic_inlet 90 | periodic_inlet.lambda_handler({}, {}) 91 | 92 | 93 | if __name__ == '__main__': 94 | unittest.main() 95 | -------------------------------------------------------------------------------- /source/core-api/lambda_functions/get_num_active_tokens.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the get_num_active_tokens API handler. 6 | It queries DynamoDB for number of requests that have yet to expire, have not been completed, 7 | and have not been deemed abandoned. 8 | """ 9 | 10 | import json 11 | import os 12 | import time 13 | import boto3 14 | from botocore import config 15 | from boto3.dynamodb.conditions import Key, Attr 16 | from vwr.common.sanitize import deep_clean 17 | 18 | DDB_TOKEN_TABLE_NAME = os.environ["TOKEN_TABLE"] 19 | EVENT_ID = os.environ["EVENT_ID"] 20 | SOLUTION_ID = os.environ['SOLUTION_ID'] 21 | 22 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 23 | user_config = config.Config(**user_agent_extra) 24 | boto_session = boto3.session.Session() 25 | region = boto_session.region_name 26 | ddb_resource = boto3.resource('dynamodb', config=user_config) 27 | ddb_table = ddb_resource.Table(DDB_TOKEN_TABLE_NAME) 28 | 29 | def lambda_handler(event, _): 30 | """ 31 | This function is the entry handler for Lambda. 32 | """ 33 | 34 | print(event) 35 | client_event_id = deep_clean(event['queryStringParameters']['event_id']) 36 | headers = { 37 | 'Content-Type': 'application/json', 38 | 'Access-Control-Allow-Origin': '*' 39 | } 40 | if client_event_id == EVENT_ID: 41 | try: 42 | current_time = int(time.time()) 43 | response = ddb_table.query( 44 | IndexName="EventExpiresIndex", 45 | ProjectionExpression="request_id", 46 | KeyConditionExpression=Key('event_id').eq(EVENT_ID) & Key('expires').gte(current_time), 47 | FilterExpression=Attr('session_status').eq(0)) 48 | items = response.get("Items", []) 49 | while "LastEvaluatedKey" in response: 50 | response = ddb_table.query( 51 | IndexName="EventExpiresIndex", 52 | ProjectionExpression="request_id", 53 | KeyConditionExpression=Key('event_id').eq(EVENT_ID) & Key('expires').gte(current_time), 54 | FilterExpression=Attr('session_status').eq(0), 55 | ExclusiveStartKey=response["LastEvaluatedKey"]) 56 | items.extend(response.get("Items", [])) 57 | response = { 58 | "statusCode": 200, 59 | "headers": headers, 60 | "body": json.dumps({"active_tokens": len(items)}) 61 | } 62 | except Exception as e: 63 | print(e) 64 | raise e 65 | else: 66 | response = { 67 | "statusCode": 400, 68 | "headers": headers, 69 | "body": json.dumps({"error": "Invalid event ID"}) 70 | } 71 | print(response) 72 | return response 73 | -------------------------------------------------------------------------------- /deployment/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | set -euo pipefail 7 | 8 | # local folders 9 | template_dir="$PWD" 10 | template_dist_dir="$template_dir/global-s3-assets" 11 | build_dist_dir="$template_dir/regional-s3-assets" 12 | 13 | # AWS default settings 14 | BUCKET="virtual-waiting-room-on-aws" 15 | REGIONS="ap-northeast-1 ap-south-1 ap-southeast-1 ca-central-1 eu-central-1 eu-north-1 eu-west-1 sa-east-1 us-east-1 us-east-2 us-west-1 us-west-2" 16 | ACL="public-read" 17 | DEPLOY_TYPE="dev" 18 | SOLUTION_NAME="virtual-waiting-room-on-aws" 19 | VERSION="v1.1.0" 20 | 21 | while getopts 'a:b:p:r:s:t:v:h' OPTION; do 22 | case "$OPTION" in 23 | b) 24 | BUCKET="$OPTARG" 25 | ;; 26 | r) 27 | REGIONS="$OPTARG" 28 | ;; 29 | a) 30 | ACL="$OPTARG" 31 | ;; 32 | t) 33 | DEPLOY_TYPE="$OPTARG" 34 | ;; 35 | s) 36 | SOLUTION_NAME="$OPTARG" 37 | ;; 38 | v) 39 | VERSION="$OPTARG" 40 | ;; 41 | h) 42 | echo 43 | echo "script usage: $(basename $0) [-b BucketBasename] [-s SolutionName] [-v VersionString] [-r RegionsForDeploy] [-a ACLSettings(public-read|none)] [-t DeployType(dev|release)]" >&2 44 | echo "example usage: ./$(basename $0) -b swr -s virtual-waiting-room-on-aws -v v1.1.0 -r \"us-west-2 us-east-1\" -a public-read -t dev" >&2 45 | echo 46 | exit 1 47 | ;; 48 | ?) 49 | echo "script usage: $(basename $0) [-b BucketBasename] [-s SolutionName] [-v VersionString] [-r RegionsForDeploy] [-a ACLSettings(public-read|none)] [-t DeployType(dev|release)]" >&2 50 | exit 1 51 | ;; 52 | esac 53 | done 54 | shift "$(($OPTIND -1))" 55 | 56 | echo Bucket Basename = $BUCKET 57 | echo Regions = $REGIONS 58 | echo ACL Setting = $ACL 59 | echo Deploy Type = $DEPLOY_TYPE 60 | 61 | root_template="virtual-waiting-room-on-aws.template" 62 | 63 | for R in $REGIONS; do 64 | if [ "$ACL" = "public-read" ]; then 65 | aws s3 sync $template_dist_dir/ s3://$BUCKET-$R/$SOLUTION_NAME/$VERSION --acl public-read --storage-class INTELLIGENT_TIERING 66 | aws s3 sync $build_dist_dir/ s3://$BUCKET-$R/$SOLUTION_NAME/$VERSION --acl public-read --storage-class INTELLIGENT_TIERING 67 | else 68 | aws s3 sync $template_dist_dir/ s3://$BUCKET-$R/$SOLUTION_NAME/$VERSION --storage-class INTELLIGENT_TIERING 69 | aws s3 sync $build_dist_dir/ s3://$BUCKET-$R/$SOLUTION_NAME/$VERSION --storage-class INTELLIGENT_TIERING 70 | fi 71 | 72 | if [ "$DEPLOY_TYPE" = "release" ]; then 73 | aws s3 sync $template_dist_dir/ s3://$BUCKET-$R/$SOLUTION_NAME/latest --acl public-read --storage-class INTELLIGENT_TIERING 74 | echo 75 | echo "ROOT TEMPLATE WEB LOCATION: https://$BUCKET-$R.s3-$R.amazonaws.com/$SOLUTION_NAME/latest/$root_template" 76 | else 77 | echo "ROOT TEMPLATE WEB LOCATION: https://$BUCKET-$R.s3-$R.amazonaws.com/$SOLUTION_NAME/$VERSION/$root_template" 78 | fi 79 | done 80 | -------------------------------------------------------------------------------- /source/core-api/lambda_functions/increment_serving_counter.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the increment_serving_counter API handler. 6 | As the name implies, it increments the waiting room's serving counter by the given increment_by value. 7 | Authorization is required to invoke this API. 8 | """ 9 | 10 | import redis 11 | import os 12 | import json 13 | import boto3 14 | from time import time 15 | from botocore import config 16 | from counters import SERVING_COUNTER 17 | from vwr.common.sanitize import deep_clean 18 | 19 | # connection info and other globals 20 | REDIS_HOST = os.environ["REDIS_HOST"] 21 | REDIS_PORT = os.environ["REDIS_PORT"] 22 | EVENT_ID = os.environ["EVENT_ID"] 23 | SECRET_NAME_PREFIX = os.environ["STACK_NAME"] 24 | SOLUTION_ID = os.environ["SOLUTION_ID"] 25 | SERVING_COUNTER_ISSUEDAT_TABLE = os.environ["SERVING_COUNTER_ISSUEDAT_TABLE"] 26 | ENABLE_QUEUE_POSITION_EXPIRY = os.environ["ENABLE_QUEUE_POSITION_EXPIRY"] 27 | 28 | boto_session = boto3.session.Session() 29 | region = boto_session.region_name 30 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 31 | user_config = config.Config(**user_agent_extra) 32 | secrets_client = boto3.client('secretsmanager', config=user_config, endpoint_url=f'https://secretsmanager.{region}.amazonaws.com') 33 | response = secrets_client.get_secret_value(SecretId=f"{SECRET_NAME_PREFIX}/redis-auth") 34 | redis_auth = response.get("SecretString") 35 | rc = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, ssl=True, decode_responses=True, password=redis_auth) 36 | ddb_resource = boto3.resource('dynamodb', endpoint_url=f'https://dynamodb.{region}.amazonaws.com', config=user_config) 37 | ddb_table = ddb_resource.Table(SERVING_COUNTER_ISSUEDAT_TABLE) 38 | 39 | 40 | def lambda_handler(event, _): 41 | """ 42 | This function is the entry handler for Lambda. 43 | """ 44 | 45 | headers = { 46 | 'Content-Type': 'application/json', 47 | 'Access-Control-Allow-Origin': '*' 48 | } 49 | body = json.loads(event['body']) 50 | increment_by = body['increment_by'] 51 | client_event_id = deep_clean(body['event_id']) 52 | 53 | if client_event_id != EVENT_ID: 54 | return { 55 | "statusCode": 400, 56 | "headers": headers, 57 | "body": json.dumps({"error": "Invalid event ID"}) 58 | } 59 | 60 | cur_serving = rc.incrby(SERVING_COUNTER, increment_by) 61 | 62 | if ENABLE_QUEUE_POSITION_EXPIRY == 'true': 63 | item = { 64 | 'event_id': EVENT_ID, 65 | 'serving_counter': int(cur_serving), 66 | 'issue_time': int(time()), 67 | 'queue_positions_served': 0 68 | } 69 | ddb_table.put_item(Item=item) 70 | print(f'Item: {item}') 71 | 72 | print(f"cur_serving: {cur_serving}") 73 | 74 | return { 75 | "statusCode": 200, 76 | "headers": headers, 77 | "body": json.dumps({"serving_num": cur_serving}) 78 | } 79 | -------------------------------------------------------------------------------- /source/core-api/custom_resources/initialize_state.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the custom resource used to intialize the various counters used by the core API. 6 | It also redeploys the public and private APIs whenever there's an update. 7 | """ 8 | 9 | import os 10 | import time 11 | from urllib.parse import urlparse 12 | from crhelper import CfnResource 13 | import requests 14 | from aws_requests_auth.boto_utils import BotoAWSRequestsAuth 15 | import boto3 16 | from botocore import config 17 | 18 | # connection info and other globals 19 | helper = CfnResource() 20 | EVENT_ID = os.environ.get("EVENT_ID") 21 | CORE_API_ENDPOINT = os.environ.get("CORE_API_ENDPOINT") 22 | PUBLIC_API_ID = os.environ.get("PUBLIC_API_ID") 23 | PRIVATE_API_ID = os.environ.get("PRIVATE_API_ID") 24 | API_STAGE = os.environ.get("API_STAGE") 25 | boto_session = boto3.session.Session() 26 | region = boto_session.region_name 27 | SOLUTION_ID = os.environ['SOLUTION_ID'] 28 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 29 | user_config = config.Config(**user_agent_extra) 30 | api_client = boto3.client("apigateway", config=user_config) 31 | 32 | @helper.create 33 | def create(event, _): 34 | """ 35 | This function makes an authenticated call to the private API endpoint 36 | to intialize the various counters used by the core API. 37 | """ 38 | print(event) 39 | core_api = f'{CORE_API_ENDPOINT}/reset_initial_state' 40 | body = { 41 | "event_id": EVENT_ID 42 | } 43 | parsed = urlparse(CORE_API_ENDPOINT) 44 | # create an authentication signer for AWS 45 | auth = BotoAWSRequestsAuth(aws_host=parsed.netloc, 46 | aws_region=region, 47 | aws_service='execute-api') 48 | response = requests.post(core_api, json=body, auth=auth, timeout=600) 49 | print(response.status_code) 50 | 51 | @helper.update 52 | def update(event, _): 53 | """ 54 | Counters and DynamoDB table are not reset during update. 55 | Both public and private APIs are redeployed since that doesn't happen automatically. 56 | """ 57 | print(event) 58 | print("Not resetting counters on update.") 59 | print("Redeploying APIs.") 60 | api_client.create_deployment( 61 | restApiId=PUBLIC_API_ID, 62 | stageName=API_STAGE, 63 | description="Automated deployment through waiting room core API update.") 64 | # avoid throttling 65 | time.sleep(5) 66 | api_client.create_deployment( 67 | restApiId=PRIVATE_API_ID, 68 | stageName=API_STAGE, 69 | description="Automated deployment through waiting room core API update.") 70 | 71 | @helper.delete 72 | def delete(event, _): 73 | """ 74 | Counters and DynamoDB table are untouched during delete. 75 | """ 76 | print(event) 77 | print("Not deleting counters on delete.") 78 | 79 | def handler(event, context): 80 | """ 81 | This function is the entry point for the Lambda-backed custom resource. 82 | """ 83 | helper(event, context) 84 | -------------------------------------------------------------------------------- /source/control-panel/src/components/ExpiredTokens.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 21 | 22 | 88 | -------------------------------------------------------------------------------- /source/sample-inlet-strategies/periodic_inlet.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is one of the sample inlet strategies. 6 | It increments the serving counter every minute based on an increment_by value provided, 7 | but only if the current time is greater than the set start time, 8 | and the end time has not been reached or is set to 0 (indefinite). 9 | User can optionally attach an alarm to this inlet, and will increment counter 10 | if the alarm is in an OK state. 11 | """ 12 | 13 | import json 14 | import os 15 | from urllib.parse import urlparse 16 | import time 17 | import boto3 18 | import requests 19 | from aws_requests_auth.boto_utils import BotoAWSRequestsAuth 20 | 21 | EVENT_ID = os.environ.get("EVENT_ID") 22 | CORE_API_ENDPOINT = os.environ.get("CORE_API_ENDPOINT") 23 | INCREMENT_BY = int(os.environ.get("INCREMENT_BY")) 24 | CORE_API_REGION = os.environ.get("CORE_API_REGION") 25 | START_TIME = int(os.environ.get("START_TIME")) 26 | END_TIME = int(os.environ.get("END_TIME")) 27 | CLOUDWATCH_ALARM = os.environ.get("CLOUDWATCH_ALARM") 28 | cw_client = boto3.client('cloudwatch') 29 | TIMEOUT = 60 30 | 31 | def lambda_handler(event, _): 32 | """ 33 | This function is responsible for incrementing the serving counter on a periodic basis 34 | """ 35 | 36 | print(event) 37 | current_time = int(time.time()) 38 | # alarm could be OK, INSUFFICIENT_DATA or ALARM 39 | alarm_status = "OK" 40 | if CLOUDWATCH_ALARM: 41 | response = cw_client.describe_alarms( AlarmNames=[CLOUDWATCH_ALARM]) 42 | print(response) 43 | if "MetricAlarms" in response and response["MetricAlarms"]: 44 | alarm_status = response["MetricAlarms"][0]["StateValue"] 45 | elif "CompositeAlarms" in response and response["CompositeAlarms"]: 46 | alarm_status = response["CompositeAlarms"][0]["StateValue"] 47 | else: 48 | print("Unable to find alarm.") 49 | if (current_time > START_TIME and (END_TIME == 0 or current_time < END_TIME) and alarm_status != "ALARM"): 50 | core_api = f'{CORE_API_ENDPOINT}/increment_serving_counter' 51 | body = { 52 | "event_id": EVENT_ID, 53 | "increment_by": INCREMENT_BY 54 | } 55 | parsed = urlparse(CORE_API_ENDPOINT) 56 | #print(parsed) 57 | # create an authentication signer for AWS 58 | auth = BotoAWSRequestsAuth(aws_host=parsed.netloc, 59 | aws_region=CORE_API_REGION, 60 | aws_service='execute-api') 61 | response = requests.post(core_api, json=body, auth=auth, timeout=TIMEOUT) 62 | print(response.status_code) 63 | print(response.content.decode()) 64 | result = { 65 | "statusCode": response.status_code, 66 | "headers": {'Content-Type': 'text/plain'}, 67 | "body": json.loads(response.text) 68 | } 69 | else: 70 | result = { 71 | "statusCode": "400", 72 | "headers": {'Content-Type': 'text/plain'}, 73 | "body": json.dumps({"error": "Request is outside of valid time period."}) 74 | } 75 | return result 76 | -------------------------------------------------------------------------------- /source/control-panel/src/components/ResetWaitingRoom.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | 39 | 40 | 90 | -------------------------------------------------------------------------------- /source/control-panel/src/components/ServingCounter.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | 26 | 27 | 88 | -------------------------------------------------------------------------------- /source/control-panel/src/components/ActiveTokens.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 26 | 27 | 94 | -------------------------------------------------------------------------------- /source/core-api/lambda_functions/get_queue_num.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the get_queue_num API handler. 6 | It retrieves the queue number assigned to a request from redis. 7 | """ 8 | 9 | import redis 10 | import json 11 | import os 12 | import boto3 13 | from botocore import config 14 | from vwr.common.sanitize import deep_clean 15 | from vwr.common.validate import is_valid_rid 16 | 17 | # connection info 18 | REDIS_HOST = os.environ["REDIS_HOST"] 19 | REDIS_PORT = os.environ["REDIS_PORT"] 20 | EVENT_ID = os.environ["EVENT_ID"] 21 | SECRET_NAME_PREFIX = os.environ["STACK_NAME"] 22 | SOLUTION_ID = os.environ["SOLUTION_ID"] 23 | QUEUE_POSITION_ENTRYTIME_TABLE = os.environ["QUEUE_POSITION_ENTRYTIME_TABLE"] 24 | 25 | boto_session = boto3.session.Session() 26 | region = boto_session.region_name 27 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 28 | user_config = config.Config(**user_agent_extra) 29 | secrets_client = boto3.client('secretsmanager', config=user_config, endpoint_url=f"https://secretsmanager.{region}.amazonaws.com") 30 | response = secrets_client.get_secret_value(SecretId=f"{SECRET_NAME_PREFIX}/redis-auth") 31 | redis_auth = response.get("SecretString") 32 | rc = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, ssl=True, decode_responses=True, password=redis_auth) 33 | ddb_resource = boto3.resource('dynamodb', endpoint_url=f'https://dynamodb.{region}.amazonaws.com', config=user_config) 34 | ddb_table_queue_position_entry_time = ddb_resource.Table(QUEUE_POSITION_ENTRYTIME_TABLE) 35 | 36 | 37 | def lambda_handler(event, _): 38 | """ 39 | This function is the entry handler for Lambda. 40 | """ 41 | 42 | print(event) 43 | request_id = deep_clean(event['queryStringParameters']['request_id']) 44 | client_event_id = deep_clean(event['queryStringParameters']['event_id']) 45 | headers = { 46 | 'Content-Type': 'application/json', 47 | 'Access-Control-Allow-Origin': '*' 48 | } 49 | 50 | if client_event_id == EVENT_ID and is_valid_rid(request_id): 51 | queue_position_item = ddb_table_queue_position_entry_time.get_item(Key={"request_id": request_id}) 52 | queue_number = int(queue_position_item['Item']['queue_position']) if 'Item' in queue_position_item else None 53 | 54 | if queue_number: 55 | print(queue_number) 56 | response = { 57 | "statusCode": 200, 58 | "headers": headers, 59 | "body": json.dumps( 60 | { 61 | 'queue_number': queue_number, 62 | 'entry_time': int(queue_position_item['Item']['entry_time']), 63 | 'event_id': queue_position_item['Item']['event_id'], 64 | 'status': int(queue_position_item['Item']['status']) 65 | } 66 | ) 67 | } 68 | else: 69 | # request wasn't found in dynamodb table but event_id is valid 70 | response = { 71 | "statusCode": 202, 72 | "headers": headers, 73 | "body": json.dumps({"error": "Request ID not found"}) 74 | } 75 | else: 76 | response = { 77 | "statusCode": 400, 78 | "headers": headers, 79 | "body": json.dumps({"error": "Invalid event or request ID"}) 80 | } 81 | 82 | return response 83 | -------------------------------------------------------------------------------- /source/core-api/custom_resources/generate_keys.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the custom resource responsible for generating public and private keys 6 | that will be used by the waiting room for a given event. 7 | """ 8 | 9 | import os 10 | import json 11 | import uuid 12 | import boto3 13 | from jwcrypto import jwk 14 | from crhelper import CfnResource 15 | from botocore import config 16 | 17 | helper = CfnResource() 18 | boto_session = boto3.session.Session() 19 | region = boto_session.region_name 20 | SOLUTION_ID = os.environ['SOLUTION_ID'] 21 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 22 | user_config = config.Config(**user_agent_extra) 23 | 24 | secrets_client = boto3.client('secretsmanager', config=user_config) 25 | SECRET_NAME_PREFIX = os.environ["STACK_NAME"] 26 | 27 | 28 | @helper.create 29 | def create(event, _): 30 | """ 31 | This function is responsible for generating public and private keys. 32 | Keys generated are stored in Secrets Manager. 33 | """ 34 | print(event) 35 | 36 | # key id 37 | kid = uuid.uuid4().hex 38 | 39 | # create JWK format keys 40 | keypair = jwk.JWK.generate(kid=kid, alg='RS256', kty='RSA', size=2048) 41 | 42 | # get the private and public JWK from the pair 43 | private_jwk = keypair.export_private(as_dict=True) 44 | print("Private key generated.") 45 | 46 | public_jwk = keypair.export_public(as_dict=True) 47 | print("Public key generated") 48 | print(f"{json.dumps(public_jwk, indent=4)}") 49 | 50 | # store pub/private keys in secrets manager 51 | try: 52 | response = secrets_client.create_secret( 53 | Name=f"{SECRET_NAME_PREFIX}/jwk-private", 54 | Description="Private JWK", 55 | SecretString=json.dumps(private_jwk)) 56 | if response["ResponseMetadata"]["HTTPStatusCode"] == 200: 57 | print("Private key saved in secrets manager.") 58 | response = secrets_client.create_secret( 59 | Name=f"{SECRET_NAME_PREFIX}/jwk-public", 60 | Description="Public JWK", 61 | SecretString=json.dumps(public_jwk)) 62 | if response["ResponseMetadata"]["HTTPStatusCode"] == 200: 63 | print("Public key saved in secrets manager.") 64 | 65 | except Exception as exception: 66 | print(exception) 67 | raise exception 68 | 69 | 70 | @helper.update 71 | def update(event, _): 72 | """ 73 | Keys will not be regenerated on resource updates. 74 | """ 75 | print(event) 76 | print("Not going to update keys on update.") 77 | 78 | 79 | @helper.delete 80 | def delete(event, _): 81 | """ 82 | This function is responsible for deleting the public and private keys from Secrets Manager. 83 | """ 84 | # remove saved keys 85 | print(event) 86 | response = secrets_client.delete_secret( 87 | SecretId=f"{SECRET_NAME_PREFIX}/jwk-private", 88 | ForceDeleteWithoutRecovery=True) 89 | print(response) 90 | response = secrets_client.delete_secret( 91 | SecretId=f"{SECRET_NAME_PREFIX}/jwk-public", 92 | ForceDeleteWithoutRecovery=True) 93 | print(response) 94 | 95 | 96 | def handler(event, context): 97 | """ 98 | This function is the entry point for the Lambda-backed custom resource. 99 | """ 100 | helper(event, context) 101 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/components/WaitingRoomSize.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | 27 | 28 | 102 | -------------------------------------------------------------------------------- /source/control-panel/src/components/IncrementServingCounter.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | 42 | 43 | 96 | -------------------------------------------------------------------------------- /source/core-api/lambda_functions/generate_token.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the generate_token API handler. 6 | It generates a token for a valid request that has been allowed to complete its transaction. 7 | """ 8 | 9 | import os 10 | import json 11 | import redis 12 | import boto3 13 | from http import HTTPStatus 14 | from botocore import config 15 | from vwr.common.sanitize import deep_clean 16 | from vwr.common.validate import is_valid_rid 17 | from generate_token_base import generate_token_base_method 18 | 19 | # connection info and other globals 20 | REDIS_HOST = os.environ["REDIS_HOST"] 21 | REDIS_PORT = os.environ["REDIS_PORT"] 22 | DDB_TOKEN_TABLE = os.environ["TOKEN_TABLE"] 23 | SECRET_NAME_PREFIX = os.environ["STACK_NAME"] 24 | VALIDITY_PERIOD = int(os.environ["VALIDITY_PERIOD"]) 25 | EVENT_ID = os.environ["EVENT_ID"] 26 | EVENT_BUS_NAME = os.environ["EVENT_BUS_NAME"] 27 | SOLUTION_ID = os.environ["SOLUTION_ID"] 28 | QUEUE_POSITION_ENTRYTIME_TABLE = os.environ["QUEUE_POSITION_ENTRYTIME_TABLE"] 29 | QUEUE_POSITION_EXPIRY_PERIOD = os.environ["QUEUE_POSITION_EXPIRY_PERIOD"] 30 | SERVING_COUNTER_ISSUEDAT_TABLE = os.environ["SERVING_COUNTER_ISSUEDAT_TABLE"] 31 | ENABLE_QUEUE_POSITION_EXPIRY = os.environ["ENABLE_QUEUE_POSITION_EXPIRY"] 32 | 33 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 34 | user_config = config.Config(**user_agent_extra) 35 | boto_session = boto3.session.Session() 36 | region = boto_session.region_name 37 | ddb_resource = boto3.resource('dynamodb', endpoint_url=f'https://dynamodb.{region}.amazonaws.com', config=user_config) 38 | ddb_table_tokens = ddb_resource.Table(DDB_TOKEN_TABLE) 39 | ddb_table_queue_position_entry_time = ddb_resource.Table(QUEUE_POSITION_ENTRYTIME_TABLE) 40 | ddb_table_serving_counter_issued_at = ddb_resource.Table(SERVING_COUNTER_ISSUEDAT_TABLE) 41 | events_client = boto3.client('events', endpoint_url=f'https://events.{region}.amazonaws.com', config=user_config) 42 | 43 | secrets_client = boto3.client('secretsmanager', endpoint_url=f'https://secretsmanager.{region}.amazonaws.com', config=user_config) 44 | response = secrets_client.get_secret_value(SecretId=f"{SECRET_NAME_PREFIX}/redis-auth") 45 | redis_auth = response.get("SecretString") 46 | rc = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, ssl=True, decode_responses=True, password=redis_auth) 47 | 48 | 49 | def lambda_handler(event, _): 50 | """ 51 | This function is the entry handler for Lambda. 52 | """ 53 | 54 | print(event) 55 | body = json.loads(event['body']) 56 | request_id = deep_clean(body['request_id']) 57 | client_event_id = deep_clean(body['event_id']) 58 | host = event['requestContext']['domainName'] 59 | stage = event['requestContext']['stage'] 60 | issuer = f"https://{host}/{stage}" 61 | headers = { 62 | 'Content-Type': 'application/json', 63 | 'Access-Control-Allow-Origin': '*' 64 | } 65 | 66 | if client_event_id != EVENT_ID or not is_valid_rid(request_id): 67 | return { 68 | "statusCode": HTTPStatus.BAD_REQUEST.value, 69 | "headers": headers, 70 | "body": json.dumps({"error": "Invalid event or request ID"}) 71 | } 72 | 73 | is_key_id_in_header = True 74 | return generate_token_base_method( 75 | EVENT_ID, request_id, headers, rc, ENABLE_QUEUE_POSITION_EXPIRY, QUEUE_POSITION_EXPIRY_PERIOD, 76 | secrets_client, SECRET_NAME_PREFIX, VALIDITY_PERIOD, issuer, events_client, EVENT_BUS_NAME, is_key_id_in_header, 77 | ddb_table_tokens, ddb_table_queue_position_entry_time, ddb_table_serving_counter_issued_at 78 | ) 79 | -------------------------------------------------------------------------------- /source/core-api/lambda_functions/assign_queue_num.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the assign_queue_num API handler. 6 | It processes the requests sent to the SQS queue and 7 | assigns a queue number to each of the request (message). 8 | """ 9 | 10 | # pylint: disable=E0401,C0301,W0703 11 | 12 | import os 13 | import json 14 | import boto3 15 | import redis 16 | from time import time 17 | from botocore import config 18 | from vwr.common.sanitize import deep_clean 19 | from counters import QUEUE_COUNTER 20 | 21 | # connection info and other globals 22 | SOLUTION_ID = os.environ['SOLUTION_ID'] 23 | REDIS_HOST = os.environ["REDIS_HOST"] 24 | REDIS_PORT = os.environ["REDIS_PORT"] 25 | QUEUE_URL = os.environ["QUEUE_URL"] 26 | EVENT_ID = os.environ["EVENT_ID"] 27 | SECRET_NAME_PREFIX = os.environ["STACK_NAME"] 28 | QUEUE_POSITION_ENTRYTIME_TABLE = os.environ["QUEUE_POSITION_ENTRYTIME_TABLE"] 29 | ENABLE_QUEUE_POSITION_EXPIRY = os.environ["ENABLE_QUEUE_POSITION_EXPIRY"] 30 | 31 | boto_session = boto3.session.Session() 32 | region = boto_session.region_name 33 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 34 | user_config = config.Config(**user_agent_extra) 35 | sqs_client = boto3.client('sqs', config=user_config, endpoint_url=f"https://sqs.{region}.amazonaws.com") 36 | secrets_client = boto3.client('secretsmanager', config=user_config, endpoint_url=f"https://secretsmanager.{region}.amazonaws.com") 37 | secrets_response = secrets_client.get_secret_value(SecretId=f"{SECRET_NAME_PREFIX}/redis-auth") 38 | redis_auth = secrets_response.get("SecretString") 39 | rc = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, ssl=True, decode_responses=True, password=redis_auth) 40 | ddb_resource = boto3.resource('dynamodb', endpoint_url=f'https://dynamodb.{region}.amazonaws.com', config=user_config) 41 | ddb_table = ddb_resource.Table(QUEUE_POSITION_ENTRYTIME_TABLE) 42 | 43 | 44 | def lambda_handler(event, _): 45 | """ 46 | This function is the entry handler for Lambda. 47 | """ 48 | print(event) 49 | num_msg = len(event['Records']) 50 | cur_count = rc.incr(QUEUE_COUNTER, num_msg) 51 | print(cur_count) 52 | q_start_num = cur_count - (num_msg-1) 53 | 54 | # iterate over msgs 55 | return_with_exception = False 56 | for msg in event['Records']: 57 | try: 58 | body = json.loads(msg['body']) 59 | request_id = msg['messageAttributes']['apig_request_id']['stringValue'] 60 | client_event_id = deep_clean(body['event_id']) 61 | 62 | # if valid, assign number and del msg 63 | # if the event ID is invalid, don't process it at all 64 | if client_event_id == EVENT_ID: 65 | item = { 66 | 'event_id': EVENT_ID, 67 | 'queue_position': int(q_start_num), 68 | 'entry_time': int(time()), 69 | 'request_id': request_id, 70 | 'status': 1 71 | } 72 | ddb_table.put_item(Item=item) 73 | print(f"Item: {item}") 74 | 75 | # sqs has a vpc endpoint 76 | response = sqs_client.delete_message( 77 | QueueUrl=QUEUE_URL, 78 | ReceiptHandle=msg["receiptHandle"] 79 | ) 80 | print(response) 81 | q_start_num += 1 82 | except Exception as exception: # NOSONAR 83 | print(exception) 84 | return_with_exception = True 85 | if return_with_exception: 86 | raise Exception("one or more messages failed processing") # NOSONAR 87 | 88 | # return the current count based on this batch process 89 | return cur_count 90 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-solutions/virtual-waiting-room-on-aws/issues), or [recently closed](https://github.com/aws-solutions/virtual-waiting-room-on-aws/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['issues'](https://github.com/aws-solutions/virtual-waiting-room-on-aws/issues) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-solutions/%%SOLUTION_NAME%%/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](https://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | 63 | -------------------------------------------------------------------------------- /docs/behavioral-views.drawio: -------------------------------------------------------------------------------- 1 | 7V1bk9o2FP41O9M+pGNb+MLjXtgkM9l2W5KmfdRiAZ4YizFmL/31lbBsQMfG9oKEgCQzu1gY4z3fuX5Hkq/Q7ez1Y4rn0wcakvjKscLXK3R35Ti207PYLz7yJkbcfpCPTNIoFGPrgWH0HxGD4oOTZRSSxdaJGaVxFs23B0c0Scgo2xrDaUpftk8b03j7W+d4QsDAcIRjOPo9CrOpGLUta/3GJxJNpuKrA1e8McPFyWJgMcUhfdkYQoMrdJtSmuWvZq+3JObSK+SSf+6+5t3yxlKSZG0+EP49QckT+evpU2/6n//JDv+xFx/6Tn6ZZxwvxV/8nTyxga9MyOzXDbu9RcawFX9D9lYIZhzF8S2Nabo6RGOX/79CN+xs+oNsvOOt/rF3QryYEn4zNjt4JmkWMTFfx9EkYWMZnbPRMU2yofgOSxxvXCr/x8bFTbNrkNdacdilkJl6EjojWfrGThEfCAKBi9DMD3ZfDLyscfaLsekGxJ4nBrHQrUl58bX42QuBQAc0UA+g8ZEkJMUrKPDox4dBErKXg2f+Rzr31zFOZ+z4d5pFYybNLKLJ2QHV28bJdSBMXlABk6sMJnsXTNePn9nPz8kzE+65Y9GrgKJXAUXPUgYFQOKG+3ySAtEz9zvnL5ez+Es0JnGUsKObOUkjdieEyyoWw4/rsZuXaZSR4RyP+EdfuB9EN9NsFgtAWNTJMPtIWh7HMZ4voqcSjJSMlukieiZ/kUUe3PgoXWb8m27LoGUdyKlt4xNAfMrAtYkPclThA8NLbh/Mj81pxJ3YJcIk29HxcXIBTgCZDXmuoFvdgntz5d5JANE0m9IJTXC8CVErsdXrUL1LcrdEWRHE7QpJ2upieI3Gf8JJGF+qXyoDgDEKHxis8KiLwtuW207jbVWihNnQ3VuCZ/TuhtcRmOucY7FshP3IptwCLBafn6MRTI8uwhZc45y/DbMoc4yh18kYUDv3r8z7e0CSHwdf2ZUev61+/jHkv+4GXwZfB1eOF7PbuXliFuFN+KuXiN1zUUH8IG+rm8ZhRdDYQEOuCZ5oltEZe4Mk4TUnQPhYTEc/VkPsb/xHqPLq4N8tvSahRIZkOJ2QXei4NdrP7pYu0xFpSt0hsCmJWR37vH0fVTiJjz7maWShEGUVXxJOEtT5fT2Wyecm2uVtvF8BfKAAKcmWKSvLrRBneF8k6ZwkclXI3ha8WcCPXqOshJi9XiH8W9+tB7kRqXqQG/VDD8jMoLcvkd+XMpD7AORI1PvWLyTnZtiFOSfJ3F32q7nmG7zffGuy8j2RRZ5kvn295lsEi7Oy33qUGxVED8qOp9d+bUhL/Lkk/I7vhyOc8Ei9hNSEMVZr1+WcLXShprbYE1DPlgCViw3VZgur7tM32x0wN6qIJpgDzXYLaarr70M2MCwKSutzsshwcqm1pcT/li2QptrSVUfQw1xplzHqrS1zfTo0tegrI9NhcsKUPeNqP5riZMJfhEwoo4w5KVnI3P8UXaK1HAfr0U29xsIXxmTMC9MFM4AomXxZHd051XEupcskXHnHvWJbvxN2bdxoX4kv9LeVQwa9JuAxoeG3jdOEvtdXOhJxWhzftz3f3Tqf94xXd3BYtwxZj9uYLsPvOBtNL9MRO55xntgxuceTq1BbT1xaW4MrRsqiGqR5HobXD1fl1Azrlikg0ygKubuL0H+54WOC/kMvZZD++530v7JnoDUVMTqt2y1Md1s1kd1OmD1VskQwreNZXDrjnoHlUMKjZG9zWNVozupKLsM+FJfhtM73hMttJiZrQsm++Z5U+rYkJjsnfE7199QmfE61NqtN+GD3XG75Li4z7rl94+IeMrm7m2tS27iHWuZ9cs/vcGHPMUOWu5UQWVJtWjFPU6/YID07Xz7F0WK6jm/1Ujwu2V4g/K4Wdw2/tGcociQa1pdTE8VsuwNn/BRs+34oHoVm3wFwM1ulB+DA1suzO7C8xYu3ZMSGwoiljVW0jjkG6+1hsGpyRzD9UbPBIhi2lvMwZ44LBjmrztqMgRXVhcE2sNYUhAduh/Vlvkk1rPVdzxP0wzsAbvbDegBmmbteR+y1yDcnrEQXy/MqskaxLhI/FadbNVKuzSb70nxVr2IBVuBVpZPqZllCxmS9AOs+ZTWhWCj3Ow25b/s7WixX7w2Fs/smvJ8kSvOXZ+3QEgjg8QBCkCAo12VZX1mtkwLZnwk90Amg1qvqNFMGPRhYdoUUzasjOlEGlfwLlKU6chcS5WLV7n3RKsqd0U+L2Gyf2saZBKx5DTKJmikO1SbRtnmqbL1JD4bvy1gV+h5TQH3jTAGyA+aYQi6eA0/pUkaM9iA3L/ZQ+WkAZUvFNc0AXOi/DDKAhokZ0oK5ljMJlFmAa7Qz6SRL1DLXlDvHh/MmkC24i/AkxXyPmAec4MnPLHMTPx8Z51lMLrx6DVWsNEeppTUEyqwBzncsmhhR9e48xnDdRbHRUsHbkKs1RfO+CzHl5Xqa1/30IM/EJbvuauTz4g1uaxTJ9AGhVjPfHUlpmK252dGDRMoJNzs6w97cH9MDu6O5A+LCCqlsXSaUbw3JI8xq34STsflCpMbbvCuv9ysmFmmyeRcmtCds851hP5bNA9gD6RKqbR5yrQnf4ZL/aePVRkLPRUNvWd3IM8fU0YmYuifRjEiucVSbOszWT9nUu8J+LFMHsPuaw7vJi3Ka6ANpCShqu0BY3S6OdeVQkSkp9Z7v9pEHL4HqmIp9l4A4Ml/UzkdCq2u6kGqrO14t9W6PevCCSZGSuM62V+i19KhQSeS+h7zZhuKI7EEu8gFHUEXOhEBumAQnWeyHcl+5RrJYGcHpwTRZ7NpfTHM7c85/N2S2BXb4NwAzk9tdXqe9NltOI+mpkyUsGZgSjaPJMhVPQrhwA3DQNmImGEChNWYagJL9gNTJ0uRuYYMsXWkeQsWkcq07evgwmOalUz4zTUyjr3q+yskvwfdbkxVtt1yqw/6wWy7ZLcuvrkvwg+qvqcvT5dO3H8OkZgG+B2mT4tlblx3ykIE5XwBbWAa56U4bL5WscFPMUzZfzIMU14J5OIMXkHsVzEVb1sNTs48nLI1k5VfNLZxVM38Hws3aoQvh3vYlFDOMPvR5hptpUZi8y0xrCud9QbRlEOUFe6ofZuCck5nuQLjZTDUhrHmjB9+QPSGbiAzQnG6VhSjb/c6H6XdeLD7xZ3SS1dLjBckyVtrBaUinXy/u8dAXRZWhjaQdNlrOQ+28OxtyKr+ndnc2+XykoTj0Icd8GcvrGryIi8yrDX2Tn0Pnd9uUtGVtqGypdWDyEt1usjz6nqSByX2qjrJsucRNmSx9WOwWixRoSnbmCuZUR3s8KcpXFPNdKeZr3lSrUKszKY72eEiULoBtzdVRAMtfw0mMoC4ras1iKUjNpe1tdC8cCs5ql7QdCB+NxJAR1r1GKKicO55G5HljlZCxJlux1rO1yarZrpI5XqmnqZl3DCC/csomW49ws8lqQthBmk0W1r0bJruebWGy4e7T19MGq2bDPau+3g6EjTFcpDnW9mHVswvbY3UMbInrQy1bBspIgD5siMZ4mfCNtK1vn+Gz4Ed0NqcJEWz0eTUQCh0yqUqx654KdPjnu1R/0Y4HvFSr8jt7COwwpTTbPD3F82k+fQwN/gc= -------------------------------------------------------------------------------- /source/openid-waitingroom/custom_resources/custom_resources_tests.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the unit test for custom resources. 6 | """ 7 | 8 | import os 9 | import unittest 10 | import json 11 | from unittest.mock import patch 12 | 13 | os.environ['SOLUTION_ID'] = "SO0166" 14 | 15 | import generate_client_secret 16 | import generate_redirect_uris_secret 17 | 18 | 19 | class CustomResourcesTest(unittest.TestCase): 20 | """ 21 | This class is reponsible for tests against the waiting room core custom resources functions 22 | """ 23 | 24 | def setUp(self): 25 | """ 26 | This function is responsible for setting up the overall environment before each test 27 | """ 28 | self.PLACEHOLDER_URIS = [ 29 | "https://DNS/oauth2/idpresponse", "https://CNAME/oauth2/idpresponse" 30 | ] 31 | self.mock_event = { 32 | "ResourceProperties" : { 33 | "SecretPrefix": "somesecret_prefix" 34 | } 35 | } 36 | self.sercret_prefix = self.mock_event["ResourceProperties"]["SecretPrefix"] 37 | 38 | 39 | def tearDown(self): 40 | """ 41 | This function is responsible for cleaning up the test environment 42 | """ 43 | print("----------------------------------------------------------------") 44 | 45 | 46 | @patch('boto3.client') 47 | def test_generate_client_secret_create(self, mock_client): 48 | """ 49 | Test generation of client secret create 50 | """ 51 | 52 | generate_client_secret.create(self.mock_event, None) 53 | mock_client.assert_called_once() 54 | self.assertEqual(mock_client.call_args[0][0], 'secretsmanager') 55 | self.assertEqual(mock_client.mock_calls[3][0], "().create_secret") 56 | self.assertEqual(mock_client.mock_calls[3][2]["Name"], f'{self.sercret_prefix}/client_secret') 57 | 58 | 59 | @patch('boto3.client') 60 | def test_generate_client_secret_delete(self, mock_client): 61 | """ 62 | Test generation of client secret delete 63 | """ 64 | generate_client_secret.delete(self.mock_event, None) 65 | mock_client.assert_called_once() 66 | self.assertEqual(mock_client.call_args[0][0], 'secretsmanager') 67 | self.assertEqual(mock_client.mock_calls[1][0], "().delete_secret") 68 | self.assertEqual(mock_client.mock_calls[1][2]["SecretId"], f'{self.sercret_prefix}/client_secret') 69 | 70 | @patch('boto3.client') 71 | def test_generate_redirecturis_secret_create(self, mock_client): 72 | """ 73 | Test for generate redirection uris create 74 | """ 75 | 76 | generate_redirect_uris_secret.create(self.mock_event, None) 77 | mock_client.assert_called_once() 78 | self.assertEqual(mock_client.call_args[0][0], 'secretsmanager') 79 | self.assertEqual(mock_client.mock_calls[1][0], "().create_secret") 80 | self.assertEqual(mock_client.mock_calls[1][2]["Name"], f'{self.sercret_prefix}/redirect_uris') 81 | self.assertEqual(mock_client.mock_calls[1][2]["SecretString"], json.dumps(self.PLACEHOLDER_URIS, indent=4)) 82 | 83 | 84 | @patch('boto3.client') 85 | def test_generate_redirecturis_delete(self, mock_client): 86 | """ 87 | Test for generate redirection uris delete 88 | """ 89 | generate_redirect_uris_secret.delete(self.mock_event, None) 90 | mock_client.assert_called_once() 91 | self.assertEqual(mock_client.call_args[0][0], 'secretsmanager') 92 | self.assertEqual(mock_client.mock_calls[1][0], "().delete_secret") 93 | self.assertEqual(mock_client.mock_calls[1][2]["SecretId"], f'{self.sercret_prefix}/redirect_uris') 94 | 95 | 96 | if __name__ == "__main": 97 | unittest.main() 98 | -------------------------------------------------------------------------------- /source/core-api/lambda_functions/auth_generate_token.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the auth_generate_token API handler. 6 | It generates a token for a valid request that has been allowed to complete its transaction. 7 | Authorization is required to invoke this API. 8 | """ 9 | 10 | import os 11 | import json 12 | import redis 13 | import boto3 14 | from http import HTTPStatus 15 | from botocore import config 16 | from vwr.common.sanitize import deep_clean 17 | from vwr.common.validate import is_valid_rid 18 | from generate_token_base import generate_token_base_method 19 | 20 | # connection info and other globals 21 | REDIS_HOST = os.environ["REDIS_HOST"] 22 | REDIS_PORT = os.environ["REDIS_PORT"] 23 | DDB_TOKEN_TABLE = os.environ["TOKEN_TABLE"] 24 | SECRET_NAME_PREFIX = os.environ["STACK_NAME"] 25 | VALIDITY_PERIOD = int(os.environ["VALIDITY_PERIOD"]) 26 | EVENT_ID = os.environ["EVENT_ID"] 27 | EVENT_BUS_NAME = os.environ["EVENT_BUS_NAME"] 28 | SOLUTION_ID = os.environ["SOLUTION_ID"] 29 | QUEUE_POSITION_ENTRYTIME_TABLE = os.environ["QUEUE_POSITION_ENTRYTIME_TABLE"] 30 | QUEUE_POSITION_EXPIRY_PERIOD = os.environ["QUEUE_POSITION_EXPIRY_PERIOD"] 31 | SERVING_COUNTER_ISSUEDAT_TABLE = os.environ["SERVING_COUNTER_ISSUEDAT_TABLE"] 32 | ENABLE_QUEUE_POSITION_EXPIRY = os.environ["ENABLE_QUEUE_POSITION_EXPIRY"] 33 | 34 | boto_session = boto3.session.Session() 35 | region = boto_session.region_name 36 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 37 | user_config = config.Config(**user_agent_extra) 38 | ddb_resource = boto3.resource('dynamodb', endpoint_url=f'https://dynamodb.{region}.amazonaws.com', config=user_config) 39 | ddb_table_tokens = ddb_resource.Table(DDB_TOKEN_TABLE) 40 | ddb_table_queue_position_entry_time = ddb_resource.Table(QUEUE_POSITION_ENTRYTIME_TABLE) 41 | ddb_table_serving_counter_issued_at = ddb_resource.Table(SERVING_COUNTER_ISSUEDAT_TABLE) 42 | events_client = boto3.client('events', endpoint_url=f'https://events.{region}.amazonaws.com', config=user_config) 43 | 44 | secrets_client = boto3.client('secretsmanager', endpoint_url=f'https://secretsmanager.{region}.amazonaws.com', config=user_config) 45 | response = secrets_client.get_secret_value(SecretId=f"{SECRET_NAME_PREFIX}/redis-auth") 46 | redis_auth = response.get("SecretString") 47 | rc = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, ssl=True, decode_responses=True, password=redis_auth) 48 | 49 | def lambda_handler(event, _): 50 | """ 51 | This function is the entry handler for Lambda. 52 | """ 53 | 54 | global VALIDITY_PERIOD 55 | print(event) 56 | body = json.loads(event['body']) 57 | request_id = deep_clean(body['request_id']) 58 | client_event_id = deep_clean(body['event_id']) 59 | host = event['requestContext']['domainName'] 60 | stage = event['requestContext']['stage'] 61 | issuer = f"https://{host}/{stage}" 62 | 63 | if "issuer" in body: 64 | issuer = body['issuer'] 65 | if "validity_period" in body: 66 | VALIDITY_PERIOD = int(body['validity_period']) 67 | headers = { 68 | 'Content-Type': 'application/json', 69 | 'Access-Control-Allow-Origin': '*' 70 | } 71 | 72 | if client_event_id != EVENT_ID or not is_valid_rid(request_id): 73 | return { 74 | "statusCode": HTTPStatus.BAD_REQUEST.value, 75 | "headers": headers, 76 | "body": json.dumps({"error": "Invalid event or request ID"}) 77 | } 78 | 79 | is_key_id_in_header = False 80 | return generate_token_base_method( 81 | EVENT_ID, request_id, headers, rc, ENABLE_QUEUE_POSITION_EXPIRY, QUEUE_POSITION_EXPIRY_PERIOD, 82 | secrets_client, SECRET_NAME_PREFIX, VALIDITY_PERIOD, issuer, events_client, EVENT_BUS_NAME, is_key_id_in_header, 83 | ddb_table_tokens, ddb_table_queue_position_entry_time, ddb_table_serving_counter_issued_at 84 | ) 85 | -------------------------------------------------------------------------------- /source/core-api/lambda_functions/generate_events.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module runs only if enabled during core API deployment. 6 | It writes various waiting room metrics to the waiting room's event bus. 7 | User can subscribe to the event bus to process the published data and act upon it, if desired. 8 | """ 9 | 10 | import redis 11 | import json 12 | import boto3 13 | import os 14 | from botocore import config 15 | from counters import QUEUE_COUNTER, SERVING_COUNTER, TOKEN_COUNTER, ABANDONED_SESSION_COUNTER, COMPLETED_SESSION_COUNTER 16 | 17 | # connection info and other globals 18 | REDIS_HOST = os.environ["REDIS_HOST"] 19 | REDIS_PORT = os.environ["REDIS_PORT"] 20 | EVENT_ID = os.environ["EVENT_ID"] 21 | EVENT_BUS_NAME = os.environ["EVENT_BUS_NAME"] 22 | GET_NUM_ACTIVE_TOKENS_FN = os.environ["ACTIVE_TOKENS_FN"] 23 | SOLUTION_ID = os.environ["SOLUTION_ID"] 24 | SECRET_NAME_PREFIX = os.environ["STACK_NAME"] 25 | 26 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 27 | user_config = config.Config(**user_agent_extra) 28 | boto_session = boto3.session.Session() 29 | region = boto_session.region_name 30 | events_client = boto3.client('events', endpoint_url=f"https://events.{region}.amazonaws.com", config=user_config) 31 | lambda_client = boto3.client('lambda', endpoint_url=f"https://lambda.{region}.amazonaws.com", config=user_config) 32 | secrets_client = boto3.client('secretsmanager', endpoint_url=f"https://secretsmanager.{region}.amazonaws.com", config=user_config) 33 | response = secrets_client.get_secret_value(SecretId=f"{SECRET_NAME_PREFIX}/redis-auth") 34 | redis_auth = response.get("SecretString") 35 | rc = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, ssl=True, decode_responses=True, password=redis_auth) 36 | 37 | # put events for number of valid tokens, current queue_counter value, current serving_num value, 38 | # total items(tokens) in db, and items(tokens) marked session_completed 39 | def lambda_handler(event, _): 40 | """ 41 | This function is the entry handler for Lambda. 42 | """ 43 | print(event) 44 | input_params = {"queryStringParameters": {"event_id": EVENT_ID}} 45 | 46 | response = lambda_client.invoke( 47 | FunctionName = GET_NUM_ACTIVE_TOKENS_FN, 48 | InvocationType = 'RequestResponse', 49 | Payload = json.dumps(input_params) 50 | ) 51 | result = response['Payload'].read() 52 | out = json.loads(result) 53 | body = json.loads(out["body"]) 54 | num_active_tokens = body["active_tokens"] 55 | 56 | # get current queue_counter value 57 | queue_counter_value = rc.get(QUEUE_COUNTER) 58 | 59 | # get current serving_num value 60 | serving_number_value = rc.get(SERVING_COUNTER) 61 | 62 | # get total tokens generated 63 | total_tokens = rc.get(TOKEN_COUNTER) 64 | 65 | # get tokens marked completed 66 | completed_sessions = rc.get(COMPLETED_SESSION_COUNTER) 67 | 68 | # get tokens marked abandoned 69 | abandoned_sessions = rc.get(ABANDONED_SESSION_COUNTER) 70 | 71 | # write to event bus 72 | try: 73 | response = events_client.put_events( 74 | Entries=[ 75 | { 76 | 'Source': 'custom.waitingroom', 77 | 'DetailType': 'waiting_room_metrics', 78 | 'Detail': json.dumps({"event_id": EVENT_ID, 79 | "num_active_tokens": num_active_tokens, 80 | "total_num_tokens": total_tokens, 81 | "queue_counter": queue_counter_value, 82 | "serving_number": serving_number_value, 83 | "completed_sessions": completed_sessions, 84 | "abandoned_sessions": abandoned_sessions}), 85 | 'EventBusName': EVENT_BUS_NAME 86 | } 87 | ] 88 | ) 89 | except Exception as exception: 90 | print(exception) 91 | raise exception 92 | 93 | return response 94 | -------------------------------------------------------------------------------- /source/shared/custom_resources/cfn_bucket_loader.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | """ 4 | This module is the custom resource used by CloudFormation to install 5 | web pages into a bucket for CloudFront. It also adds a JavaScript file containing 6 | endpoints to access specified APIs. 7 | """ 8 | 9 | import os 10 | 11 | from crhelper import CfnResource 12 | import boto3 13 | from botocore import config 14 | 15 | helper = CfnResource() 16 | 17 | # this is where the web contents are packaged 18 | CONTENTS_LOCAL = "www" 19 | 20 | SOLUTION_ID = os.environ['SOLUTION_ID'] 21 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 22 | user_config = config.Config(**user_agent_extra) 23 | 24 | 25 | @helper.create 26 | @helper.update 27 | def update_web_content(event, _): 28 | """ 29 | This function is responsible for creating or updating web content 30 | """ 31 | bucket_name = event["ResourceProperties"]["BucketName"] 32 | apis = event["ResourceProperties"]["APIs"] 33 | delete_bucket_contents(bucket_name) 34 | put_web_contents(bucket_name) 35 | put_api_endpoints_js(bucket_name, apis) 36 | 37 | 38 | @helper.delete 39 | def delete_web_content(event, _): 40 | """ 41 | This function is responsible for deleting web content 42 | """ 43 | bucket_name = event["ResourceProperties"]["BucketName"] 44 | delete_bucket_contents(bucket_name) 45 | 46 | 47 | def handler(event, context): 48 | """ 49 | Lambda entry point. 50 | """ 51 | helper(event, context) 52 | 53 | 54 | def put_web_contents(bucket_name): 55 | """ 56 | This function is responsible for removing any existing contents 57 | from the specified bucket, and adding contents to the bucket 58 | from the packaged contents. 59 | """ 60 | client = boto3.client("s3", config=user_config) 61 | 62 | # upload each local file to the bucket, preserve folders 63 | for dirpath, _, filenames in os.walk(CONTENTS_LOCAL): 64 | for name in filenames: 65 | local = f"{dirpath}/{name}" 66 | remote = local.replace(f"{CONTENTS_LOCAL}/", "") 67 | print(f'put {local}') 68 | content_type = None 69 | if remote.endswith(".js"): 70 | content_type = "application/javascript" 71 | elif remote.endswith(".html"): 72 | content_type = "text/html" 73 | elif remote.endswith(".css"): 74 | content_type = "text/css" 75 | else: 76 | content_type = "binary/octet-stream" 77 | with open(local, 'rb') as data: 78 | client.put_object(Bucket=bucket_name, 79 | Key=remote, 80 | Body=data, 81 | ContentType=content_type 82 | ) 83 | 84 | 85 | def put_api_endpoints_js(bucket_name, apis): 86 | """ 87 | This function is responsible for creating a file 88 | containing the URL of the Core API endpoint. 89 | """ 90 | client = boto3.client("s3", config=user_config) 91 | key = "api_endpoints.js" 92 | content_type = "application/javascript" 93 | contents = "" 94 | for name, url in apis.items(): 95 | contents = f'{contents}const {name} = \"{url}\";\n' 96 | client.put_object(Bucket=bucket_name, 97 | Key=key, 98 | Body=contents, 99 | ContentType=content_type 100 | ) 101 | 102 | 103 | def delete_bucket_contents(bucket_name): 104 | """ 105 | This function is responsible for removing all contents from the specified bucket. 106 | """ 107 | client = boto3.client("s3", config=user_config) 108 | response = client.list_objects_v2(Bucket=bucket_name) 109 | for item in response.get("Contents", []): 110 | print(f'delete {item["Key"]}') 111 | client.delete_object(Bucket=bucket_name, Key=item["Key"]) 112 | while response.get("NextContinuationToken", False): 113 | response = client.list_objects_v2( 114 | Bucket=bucket_name, 115 | ContinuationToken=response.get("NextContinuationToken")) 116 | for item in response.get("Contents", []): 117 | print(f'delete {item["Key"]}') 118 | client.delete_object(Bucket=bucket_name, Key=item["Key"]) 119 | -------------------------------------------------------------------------------- /deployment/run-unit-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This assumes all of the OS-level configuration has been completed and git repo has already been cloned 4 | # 5 | # This script should be run from the repo's deployment directory 6 | # cd deployment 7 | # ./run-unit-tests.sh 8 | # 9 | 10 | 11 | # configure the environment 12 | VENV=$(mktemp -d) && echo "$VENV" 13 | python3 -m venv "$VENV" 14 | source "$VENV"/bin/activate 15 | 16 | cd ./../deployment 17 | 18 | poetry install 19 | # Get reference for all important folders 20 | template_dir="$PWD" 21 | echo "template_dir" $template_dir 22 | source_dir="$template_dir/../source" 23 | echo "source_dir" $source_dir 24 | 25 | 26 | echo "------------------------------------------------------------------------------" 27 | echo " Test core API lambda functions" 28 | echo "------------------------------------------------------------------------------" 29 | cd $source_dir/core-api/lambda_functions 30 | coverage run lambda_functions_tests.py 31 | coverage xml 32 | sed -i -- 's/filename\=\"/filename\=\"source\/core-api\/lambda_functions\//g' coverage.xml 33 | 34 | echo "------------------------------------------------------------------------------" 35 | echo " Test core API custom resources functions" 36 | echo "------------------------------------------------------------------------------" 37 | cd $source_dir/core-api/custom_resources 38 | coverage run custom_resource_tests.py 39 | coverage xml 40 | sed -i -- 's/filename\=\"/filename\=\"source\/core-api\/custom_resources\//g' coverage.xml 41 | 42 | echo "------------------------------------------------------------------------------" 43 | echo " Test shared custom resources functions" 44 | echo "------------------------------------------------------------------------------" 45 | cd $source_dir/shared/ 46 | coverage run shared_resources_tests.py 47 | coverage xml 48 | sed -i -- 's/filename\=\"/filename\=\"source\/shared\//g' coverage.xml 49 | 50 | echo "------------------------------------------------------------------------------" 51 | echo " Test shared common functions" 52 | echo "------------------------------------------------------------------------------" 53 | cd $source_dir/shared/virtual-waiting-room-on-aws-common 54 | coverage run common_tests.py 55 | coverage xml 56 | sed -i -- 's/filename\=\"/filename\=\"source\/shared\/virtual-waiting-room-on-aws-common\//g' coverage.xml 57 | 58 | echo "------------------------------------------------------------------------------" 59 | echo " Test open-id API functions" 60 | echo "------------------------------------------------------------------------------" 61 | cd $source_dir/openid-waitingroom/chalice 62 | coverage run openid_waitingroom_tests.py 63 | coverage xml 64 | sed -i -- 's/filename\=\"/filename\=\"source\/openid-waitingroom\/chalice\//g' coverage.xml 65 | 66 | echo "------------------------------------------------------------------------------" 67 | echo " Test open-id waitingroom custom resources functions" 68 | echo "------------------------------------------------------------------------------" 69 | cd $source_dir/openid-waitingroom/custom_resources 70 | coverage run custom_resources_tests.py 71 | coverage xml 72 | sed -i -- 's/filename\=\"/filename\=\"source\/openid-waitingroom\/custom_resources\//g' coverage.xml 73 | 74 | echo " Test inlet strategy Lambda functions" 75 | echo "------------------------------------------------------------------------------" 76 | cd $source_dir/sample-inlet-strategies 77 | coverage run inlet_strategy_tests.py 78 | coverage xml 79 | sed -i -- 's/filename\=\"/filename\=\"source\/sample-inlet-strategies\//g' coverage.xml 80 | 81 | echo "------------------------------------------------------------------------------" 82 | echo " Test token authorizer Lambda functions" 83 | echo "------------------------------------------------------------------------------" 84 | cd $source_dir/token-authorizer/chalice 85 | coverage run token_authorizer_tests.py 86 | coverage xml 87 | sed -i -- 's/filename\=\"/filename\=\"source\/token-authorizer\/chalice\//g' coverage.xml 88 | 89 | echo "------------------------------------------------------------------------------" 90 | echo " Test sample API functions" 91 | echo "------------------------------------------------------------------------------" 92 | cd $source_dir/core-api-authorizers-sample/chalice 93 | coverage run sample_api_tests.py 94 | coverage xml 95 | sed -i -- 's/filename\=\"/filename\=\"source\/core-api-authorizers-sample\/chalice\//g' coverage.xml 96 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/components/TimeToExit.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | 44 | 45 | 124 | -------------------------------------------------------------------------------- /docs/sequence-diagrams.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/components/ServingPosition.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | 38 | 39 | 130 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/components/CompletePurchase.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | 47 | 48 | 128 | -------------------------------------------------------------------------------- /source/sample-inlet-strategies/max_size_inlet.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is one of the sample inlet strategies. 6 | It increments the serving counter based on the maximum size of transactions that the downstream site can handle. 7 | This module expects to receive a message through an SNS topic that can include: 8 | - exited : number of transactions that have completed 9 | - list of request IDs to be marked completed 10 | - list of request IDs to be marked abandoned 11 | Above data is used to determine how much to increment the serving counter. 12 | """ 13 | 14 | import json 15 | import os 16 | from urllib.parse import urlparse 17 | 18 | import requests 19 | from aws_requests_auth.boto_utils import BotoAWSRequestsAuth 20 | 21 | EVENT_ID = os.environ.get("EVENT_ID") 22 | CORE_API_ENDPOINT = os.environ.get("CORE_API_ENDPOINT") 23 | CORE_API_REGION = os.environ.get("CORE_API_REGION") 24 | MAX_SIZE = int(os.environ.get("MAX_SIZE")) 25 | TIMEOUT = 60 26 | 27 | increment_by_api = f'{CORE_API_ENDPOINT}/increment_serving_counter' 28 | update_status_api = f'{CORE_API_ENDPOINT}/update_session' 29 | active_tokens_api = f'{CORE_API_ENDPOINT}/num_active_tokens' 30 | num_updated_tokens = 0 31 | 32 | def lambda_handler(event, _): 33 | """ 34 | This function is responsible for processing an SNS message to update serving counter 35 | """ 36 | 37 | print(event) 38 | msg = json.loads(event["Records"][0]["Sns"]["Message"]) 39 | result = json.dumps({"message": "Nothing to process."}) 40 | 41 | if msg: 42 | exited = None 43 | increment_by = 0 44 | parsed = urlparse(CORE_API_ENDPOINT) 45 | # create an authentication signer for AWS 46 | auth = BotoAWSRequestsAuth(aws_host=parsed.netloc, 47 | aws_region=CORE_API_REGION, 48 | aws_service='execute-api') 49 | 50 | if "exited" in msg: 51 | exited = int(msg["exited"]) 52 | 53 | # Update tokens' status 54 | if "completed" in msg: 55 | status = 1 56 | update_tokens(status, msg["completed"], auth) 57 | 58 | if "abandoned" in msg: 59 | status = -1 60 | update_tokens(status, msg["abandoned"], auth) 61 | 62 | # Call num_active_tokens and subtract result from max allowed users 63 | payload = {"event_id": EVENT_ID} 64 | response = requests.get(active_tokens_api, params=payload, auth=auth, timeout=TIMEOUT) 65 | active_tokens = response.json()["active_tokens"] 66 | capacity = MAX_SIZE - int(active_tokens) 67 | 68 | # Always use the value provided with "exited" for increment_serving_counter when provided 69 | if exited is not None and exited < capacity: 70 | increment_by = exited 71 | # otherwise the sum of items in "completed" and "abandoned" lists 72 | elif num_updated_tokens < capacity: 73 | increment_by = num_updated_tokens 74 | # but if capacity is less than exited value and num_updated_tokens, 75 | # then use that value to increment serving counter 76 | else: 77 | increment_by = capacity 78 | body = { 79 | "event_id": EVENT_ID, 80 | "increment_by": increment_by 81 | } 82 | print(f"exited: {exited}, num_updated_tokens: {num_updated_tokens}, capacity: {capacity}, increment_by: {increment_by}") 83 | # only increment counter if exited information was present or tokens were actually updated 84 | if exited or num_updated_tokens > 0: 85 | response = requests.post(increment_by_api, json=body, auth=auth, timeout=TIMEOUT) 86 | result = response.json() 87 | print(result) 88 | return result 89 | 90 | def update_tokens(status, request_ids, auth): 91 | """ 92 | This function is responsible for updating the status of a token via the API 93 | """ 94 | 95 | global num_updated_tokens 96 | print(f"number of status {status}: {len(request_ids)}") 97 | 98 | body = { 99 | "event_id": EVENT_ID, 100 | "status": status 101 | } 102 | for request_id in request_ids: 103 | body["request_id"] = request_id 104 | response = requests.post(update_status_api, json=body, auth=auth, timeout=TIMEOUT) 105 | if response.status_code == 200: 106 | num_updated_tokens += 1 107 | print(response.content.decode()) 108 | -------------------------------------------------------------------------------- /source/core-api/lambda_functions/update_session.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is the update_session API handler. 6 | It updates the status of a session (token) stored in DynamoDB. 7 | Session status is denoted by an integer. Sessions set to a status of 1 indicates completed, and -1 indicates abandoned. 8 | Authorization is required to invoke this API. 9 | """ 10 | 11 | import redis 12 | import json 13 | import boto3 14 | import os 15 | from botocore.exceptions import ClientError 16 | from botocore import config 17 | from boto3.dynamodb.conditions import Attr 18 | from counters import COMPLETED_SESSION_COUNTER, ABANDONED_SESSION_COUNTER 19 | from vwr.common.sanitize import deep_clean 20 | from vwr.common.validate import is_valid_rid 21 | 22 | # connection info and other globals 23 | REDIS_HOST = os.environ["REDIS_HOST"] 24 | REDIS_PORT = os.environ["REDIS_PORT"] 25 | EVENT_ID = os.environ["EVENT_ID"] 26 | EVENT_BUS_NAME = os.environ["EVENT_BUS_NAME"] 27 | DDB_TOKEN_TABLE_NAME = os.environ["TOKEN_TABLE"] 28 | SOLUTION_ID = os.environ['SOLUTION_ID'] 29 | SECRET_NAME_PREFIX = os.environ["STACK_NAME"] 30 | 31 | user_agent_extra = {"user_agent_extra": SOLUTION_ID} 32 | user_config = config.Config(**user_agent_extra) 33 | boto_session = boto3.session.Session() 34 | region = boto_session.region_name 35 | ddb_resource = boto3.resource('dynamodb', endpoint_url=f"https://dynamodb.{region}.amazonaws.com", config=user_config) 36 | ddb_table = ddb_resource.Table(DDB_TOKEN_TABLE_NAME) 37 | events_client = boto3.client('events', endpoint_url=f"https://events.{region}.amazonaws.com", config=user_config) 38 | status_codes = {1: "completed", -1: "abandoned"} 39 | secrets_client = boto3.client('secretsmanager', config=user_config, endpoint_url=f"https://secretsmanager.{region}.amazonaws.com") 40 | response = secrets_client.get_secret_value(SecretId=f"{SECRET_NAME_PREFIX}/redis-auth") 41 | redis_auth = response.get("SecretString") 42 | rc = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, ssl=True, decode_responses=True, password=redis_auth) 43 | 44 | def lambda_handler(event, _): 45 | """ 46 | This function is the entry handler for Lambda. 47 | """ 48 | 49 | print(event) 50 | body = json.loads(event['body']) 51 | request_id = deep_clean(body['request_id']) 52 | client_event_id = deep_clean(body['event_id']) 53 | status = int(body['status']) 54 | headers = { 55 | 'Content-Type': 'application/json', 56 | 'Access-Control-Allow-Origin': '*' 57 | } 58 | 59 | if client_event_id == EVENT_ID and is_valid_rid(request_id): 60 | try: 61 | result = ddb_table.update_item( 62 | UpdateExpression='SET session_status = :status', 63 | ConditionExpression=(Attr('request_id').eq(request_id) and Attr('session_status').eq(0)), 64 | Key={'request_id': request_id}, 65 | ExpressionAttributeValues={':status': status} 66 | ) 67 | response = { 68 | "statusCode": 200, 69 | "headers": headers, 70 | "body": json.dumps(result) 71 | } 72 | # write to event bus 73 | events_client.put_events( 74 | Entries=[ 75 | { 76 | 'Source': 'custom.waitingroom', 77 | 'DetailType': 'session_updated', 78 | 'Detail': json.dumps({"event_id": EVENT_ID, 79 | "request_id": request_id, 80 | "status": status_codes[status]}), 81 | 'EventBusName': EVENT_BUS_NAME 82 | } 83 | ] 84 | ) 85 | # increment counter tracking sessions completed 86 | if status == -1: 87 | rc.incr(ABANDONED_SESSION_COUNTER, 1) 88 | 89 | elif status == 1: 90 | rc.incr(COMPLETED_SESSION_COUNTER, 1) 91 | except ClientError as e: 92 | if e.response['Error']['Code'] != 'ConditionalCheckFailedException': 93 | raise e 94 | print(e) 95 | response = { 96 | "statusCode": 404, 97 | "headers": headers, 98 | "body": json.dumps({"error": "Request ID doesn't exist or status already set."}) 99 | } 100 | 101 | except Exception as e: 102 | print(e) 103 | raise e 104 | else: 105 | response = { 106 | "statusCode": 400, 107 | "headers": headers, 108 | "body": json.dumps({"error": "Invalid event or request ID"}) 109 | } 110 | print(response) 111 | return response 112 | -------------------------------------------------------------------------------- /source/sample-waiting-room-site/src/components/AuthorizationToken.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 9 | 36 | 37 | 140 | -------------------------------------------------------------------------------- /source/token-authorizer/chalice/token_authorizer_tests.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """ 5 | This module is provides unit tests for the inlet strategies module. 6 | """ 7 | 8 | # pylint: disable=C0415,W0201 9 | 10 | import json 11 | import unittest 12 | from unittest.mock import patch 13 | 14 | TESTING_PUBLIC_KEY = """ 15 | {"kty": "RSA", 16 | "alg": "RS256", 17 | "kid": "303694c8a3394c59aec8260e34787f9a", 18 | "n": "q_00x09ISc2elL-QKBCBOHSP0ZenaRwomIkuln0oWrQ6aHCzjKrkuW4v7isz_ifX6DT5ECzTSsTokb2BEkRDLKYmXfhLxFYgXusO2kPv17tbIMyoQPejFlJOIn5K4sPgP-AoE9GGR2boqtqMBEGPJdDYNvFNBgUBhdGwwus3DrcTtb9lsuFKcx5OdX99k8vEDwwHy2bZGBPyS6EXW1Xu1_-nrrMSn7LhbhyZWwGFWuvwG4D9iegZm51UdoT1xZqH8vVUAkt3ZvHztB4LMEK2U3DRvGE7I-6OWs1ohm6sYxnVeI4podcQ2YuD6YgNTbJqdrpCZVgOSnRvZNHhXFRgrw", 19 | "e": "AQAB"} 20 | """ 21 | 22 | TESTING_TOKEN = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjMwMzY5NGM4YTMzOTRjNTlhZWM4MjYwZTM0Nzg3ZjlhIiwidHlwIjoiSldUIn0.eyJhdWQiOiJTYW1wbGUiLCJleHAiOjE2NjM5Nzc5NjIsImlhdCI6MTY2Mzk3NzY2MiwiaXNzIjoiaHR0cHM6Ly9tYTgwY2VvZmFrLmV4ZWN1dGUtYXBpLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tL2FwaSIsIm5iZiI6MTY2Mzk3NzY2MiwicXVldWVfcG9zaXRpb24iOjI1ODIsInN1YiI6ImUzN2EyZTk2LWZlOTgtNGQ1Zi04NWM5LWIwNDczMzFmOWVmYiIsInRva2VuX3VzZSI6ImFjY2VzcyJ9.UkYE9edglO6kPN5_r3wy9OeP15w7iB3M7tDCDzPr3wxGehfBJaZv1J-K4-VnJ4q04BLXtExnOlFG2TVHak1zdOClt47ioUmBJ-eyva2YtWWTOhIgBR_pC2dDXR3MCJ1sHyD5_EpfzgBDjD_BwbyOesi_h72CSTTcusGv-wiIiJR85C3rLn3eVjoziFoWl0X2O0SSDkxkxWCNRX6tWf9z983OUX7OL02rh0q2M6iKtokIvzDcL4imgvrho9M9eIKV66kF3VN7GqE2sIifv5ClrOVvQA4x0OY1K6Z5TOVQ0936-81BYBABASrRqUlMPfSYcZDbj2uW5HI1TgcEfBwJug" #nosec B105 23 | 24 | 25 | class TokenAuthorizerUnitTestException(Exception): 26 | """ 27 | This class is an exception subclass for unit testing errors 28 | """ 29 | 30 | def __init__(self, *args: object) -> None: 31 | super().__init__(*args) 32 | 33 | 34 | def environ_get_mock(key, default_value=None): 35 | """ 36 | This function is the mocked (replaced) function for returning environment variables 37 | """ 38 | result = "" 39 | if key == "WAITING_ROOM_EVENT_ID": 40 | result = "641EE9DD-57BE-437E-B157-BAD15F3D6408" 41 | elif key in ["PUBLIC_API_ENDPOINT", "ISSUER"]: 42 | result = "https://www.example.com" 43 | elif default_value is not None: 44 | result = default_value 45 | return result 46 | 47 | 48 | def get_public_key(): 49 | """ 50 | This function for returning a mock public key used with JWT 51 | """ 52 | return json.loads(TESTING_PUBLIC_KEY) 53 | 54 | 55 | @patch('os.environ.get', new=environ_get_mock) 56 | @patch('boto3.resource') 57 | @patch('boto3.client') 58 | @patch('requests.post') 59 | @patch('requests.get') 60 | class TestInletStrategies(unittest.TestCase): 61 | """ 62 | This class extends TestCase with testing functions 63 | """ 64 | 65 | def test_get_public_key(self, patched_resource, patched_client, 66 | patched_post, patched_get): 67 | """ 68 | Test the get_public_key function 69 | """ 70 | import app 71 | app.get_public_key() 72 | 73 | @patch('app.get_public_key', new=get_public_key) 74 | def test_verify_token_sig(self, patched_resource, patched_client, 75 | patched_post, patched_get): 76 | """ 77 | Test the verify_token_sig function 78 | """ 79 | import app 80 | app.verify_token_sig(TESTING_TOKEN) 81 | 82 | @patch('app.get_public_key', new=get_public_key) 83 | def test_verify_token(self, patched_resource, patched_client, patched_post, 84 | patched_get): 85 | """ 86 | Test the verify_token function 87 | """ 88 | import app 89 | app.verify_token(TESTING_TOKEN) 90 | 91 | @patch('app.get_public_key', new=get_public_key) 92 | def test_check_authorizer_token(self, patched_resource, patched_client, 93 | patched_post, patched_get): 94 | """ 95 | Test the check_authorizer_token function 96 | """ 97 | import app 98 | app.check_authorizer_token(TESTING_TOKEN, "/") 99 | 100 | @patch('app.get_public_key', new=get_public_key) 101 | def test_api_gateway_authorizer(self, patched_resource, patched_client, 102 | patched_post, patched_get): 103 | """ 104 | Test the api_gateway_authorizer function 105 | """ 106 | import app 107 | app.api_gateway_authorizer( 108 | { 109 | 'authorizationToken': 110 | TESTING_TOKEN, 111 | 'methodArn': 112 | "arn:aws:execute-api:us-east-1:0123456789012:pvb6r6th3e/*/GET/expired_tokens" 113 | }, {}) 114 | 115 | 116 | if __name__ == '__main__': 117 | unittest.main() 118 | --------------------------------------------------------------------------------