diff --git a/.github/workflows/check-webroot.yml b/.github/workflows/check-webroot.yml index 452a3586d..fe2d2bcd1 100644 --- a/.github/workflows/check-webroot.yml +++ b/.github/workflows/check-webroot.yml @@ -34,11 +34,18 @@ jobs: VUE_APP_API_STATE_ROOT: https://api.test.jcc.iaapi.io VUE_APP_API_LICENSE_ROOT: https://api.test.jcc.iaapi.io VUE_APP_API_SEARCH_ROOT: https://search.test.jcc.iaapi.io + VUE_APP_API_USER_ROOT: https://api.test.jcc.iaapi.io + VUE_APP_API_STATE_ROOT_COSMO: https://api.test.cosmetology.jcc.iaapi.io + VUE_APP_API_LICENSE_ROOT_COSMO: https://api.test.cosmetology.jcc.iaapi.io + VUE_APP_API_SEARCH_ROOT_COSMO: https://search.test.cosmetology.jcc.iaapi.io + VUE_APP_API_USER_ROOT_COSMO: https://api.test.cosmetology.jcc.iaapi.io VUE_APP_COGNITO_REGION: us-east-1 VUE_APP_COGNITO_AUTH_DOMAIN_STAFF: https://ia-cc-staff-test.auth.us-east-1.amazoncognito.com VUE_APP_COGNITO_CLIENT_ID_STAFF: ${{ secrets.DEV_WEBROOT_COGNITO_CLIENT_ID_STAFF }} VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE: https://ia-cc-provider-test.auth.us-east-1.amazoncognito.com VUE_APP_COGNITO_CLIENT_ID_LICENSEE: ${{ secrets.DEV_WEBROOT_COGNITO_CLIENT_ID_LICENSEE }} + VUE_APP_COGNITO_AUTH_DOMAIN_STAFF_COSMO: https://staff-auth.test.cosmetology.jcc.iaapi.io + VUE_APP_COGNITO_CLIENT_ID_STAFF_COSMO: ${{ secrets.DEV_WEBROOT_COGNITO_CLIENT_ID_STAFF_COSMO }} VUE_APP_RECAPTCHA_KEY: 6Le-3bgqAAAAAILDVUKkRnAF9SSzb8o9uv5lY7Ih steps: @@ -90,11 +97,18 @@ jobs: VUE_APP_API_STATE_ROOT: ${{ env.VUE_APP_API_STATE_ROOT }} VUE_APP_API_LICENSE_ROOT: ${{ env.VUE_APP_API_LICENSE_ROOT }} VUE_APP_API_SEARCH_ROOT: ${{ env.VUE_APP_API_SEARCH_ROOT }} + VUE_APP_API_USER_ROOT: ${{ env.VUE_APP_API_USER_ROOT }} + VUE_APP_API_STATE_ROOT_COSMO: ${{ env.VUE_APP_API_STATE_ROOT_COSMO }} + VUE_APP_API_LICENSE_ROOT_COSMO: ${{ env.VUE_APP_API_LICENSE_ROOT_COSMO }} + VUE_APP_API_SEARCH_ROOT_COSMO: ${{ env.VUE_APP_API_SEARCH_ROOT_COSMO }} + VUE_APP_API_USER_ROOT_COSMO: ${{ env.VUE_APP_API_USER_ROOT_COSMO }} VUE_APP_COGNITO_REGION: ${{ env.VUE_APP_COGNITO_REGION }} VUE_APP_COGNITO_AUTH_DOMAIN_STAFF: ${{ env.VUE_APP_COGNITO_AUTH_DOMAIN_STAFF }} VUE_APP_COGNITO_CLIENT_ID_STAFF: ${{ env.VUE_APP_COGNITO_CLIENT_ID_STAFF }} VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE: ${{ env.VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE }} VUE_APP_COGNITO_CLIENT_ID_LICENSEE: ${{ env.VUE_APP_COGNITO_CLIENT_ID_LICENSEE }} + VUE_APP_COGNITO_AUTH_DOMAIN_STAFF_COSMO: ${{ env.VUE_APP_COGNITO_AUTH_DOMAIN_STAFF_COSMO }} + VUE_APP_COGNITO_CLIENT_ID_STAFF_COSMO: ${{ env.VUE_APP_COGNITO_CLIENT_ID_STAFF_COSMO }} VUE_APP_RECAPTCHA_KEY: ${{ env.VUE_APP_RECAPTCHA_KEY }} VUE_APP_MOCK_API: true run: yarn build diff --git a/backend/compact-connect-ui-app/lambdas/nodejs/cloudfront-csp/index.js b/backend/compact-connect-ui-app/lambdas/nodejs/cloudfront-csp/index.js index 9068048ba..eef5e425f 100644 --- a/backend/compact-connect-ui-app/lambdas/nodejs/cloudfront-csp/index.js +++ b/backend/compact-connect-ui-app/lambdas/nodejs/cloudfront-csp/index.js @@ -13,17 +13,23 @@ * * These values are injected into the lambda function at build time. See the * `generate_csp_lambda_code` function in - * backend/compact-connect/stacks/frontend_deployment_stack/distribution.py + * backend/compact-connect-ui-app/stacks/frontend_deployment_stack/distribution.py * @type {object} */ const environmentValues = { webFrontend: `##WEB_FRONTEND##`, + // JCC dataApi: `##DATA_API##`, searchApi: `##SEARCH_API##`, s3UploadUrlState: `##S3_UPLOAD_URL_STATE##`, s3UploadUrlProvider: `##S3_UPLOAD_URL_PROVIDER##`, cognitoStaff: `##COGNITO_STAFF##`, cognitoProvider: `##COGNITO_PROVIDER##`, + // COSMETOLOGY + dataApiCosmo: `##DATA_API_COSMO##`, + searchApiCosmo: `##SEARCH_API_COSMO##`, + s3UploadUrlStateCosmo: `##S3_UPLOAD_URL_STATE_COSMO##`, + cognitoStaffCosmo: `##COGNITO_STAFF_COSMO##`, }; // ============================================================================ @@ -62,12 +68,18 @@ const getFullyQualified = (domain) => { const getEnvironmentUrls = () => { const environmentUrls = {}; + // JCC environmentUrls.dataApi = getFullyQualified(environmentValues.dataApi); environmentUrls.searchApi = getFullyQualified(environmentValues.searchApi); environmentUrls.s3UploadUrlState = getFullyQualified(environmentValues.s3UploadUrlState); environmentUrls.s3UploadUrlProvider = getFullyQualified(environmentValues.s3UploadUrlProvider); environmentUrls.cognitoStaff = getFullyQualified(environmentValues.cognitoStaff); environmentUrls.cognitoProvider = getFullyQualified(environmentValues.cognitoProvider); + // COSMETOLOGY + environmentUrls.dataApiCosmo = getFullyQualified(environmentValues.dataApiCosmo); + environmentUrls.searchApiCosmo = getFullyQualified(environmentValues.searchApiCosmo); + environmentUrls.s3UploadUrlStateCosmo = getFullyQualified(environmentValues.s3UploadUrlStateCosmo); + environmentUrls.cognitoStaffCosmo = getFullyQualified(environmentValues.cognitoStaffCosmo); return environmentUrls; }; @@ -214,11 +226,13 @@ const setCspHeader = (headers = {}) => { 'self', 'data:', domains.dataApi, + domains.dataApiCosmo, 'https://www.gstatic.com/recaptcha/', ]), buildSrcString('media-src', [ 'self', domains.dataApi, + domains.dataApiCosmo, ]), buildSrcString('frame-src', [ 'self', @@ -238,14 +252,20 @@ const setCspHeader = (headers = {}) => { ]), buildSrcString('connect-src', [ 'self', + cognitoIdpUrl, + 'https://www.google.com/recaptcha/', + // JCC domains.dataApi, domains.searchApi, domains.s3UploadUrlState, domains.s3UploadUrlProvider, domains.cognitoStaff, domains.cognitoProvider, - cognitoIdpUrl, - 'https://www.google.com/recaptcha/', + // COSMETOLOGY + domains.dataApiCosmo, + domains.searchApiCosmo, + domains.s3UploadUrlStateCosmo, + domains.cognitoStaffCosmo, // Begin Statsig domains 'https://api.statsig.com/', 'https://featuregates.org/', diff --git a/backend/compact-connect-ui-app/lambdas/nodejs/cloudfront-csp/test/index.test.js b/backend/compact-connect-ui-app/lambdas/nodejs/cloudfront-csp/test/index.test.js index 3d7e73b8e..824e3a69d 100644 --- a/backend/compact-connect-ui-app/lambdas/nodejs/cloudfront-csp/test/index.test.js +++ b/backend/compact-connect-ui-app/lambdas/nodejs/cloudfront-csp/test/index.test.js @@ -19,12 +19,18 @@ const { // ================================================================================================ const environmentValues = { webFrontend: 'app.compactconnect.org', + // JCC dataApi: 'api.compactconnect.org', searchApi: 'search.compactconnect.org', s3UploadUrlState: 'prod-persistentstack-bulkuploadsbucketda4bdcd0-zq5o0q8uqq5i.s3.amazonaws.com', s3UploadUrlProvider: 'prod-persistentstack-providerusersbucket5c7b202b-ffpgh4fyozwk.s3.amazonaws.com', cognitoStaff: 'staff-auth.compactconnect.org', cognitoProvider: 'licensee-auth.compactconnect.org', + // COSMETOLOGY + dataApiCosmo: 'api.cosmetology.compactconnect.org', + searchApiCosmo: 'search.cosmetology.compactconnect.org', + s3UploadUrlStateCosmo: 'prod-persistentstack-bulkuploadsbucketda4bdcd0-zq5o0q8uqq5j.s3.amazonaws.com', + cognitoStaffCosmo: 'staff-auth.cosmetology.compactconnect.org', }; /** @@ -46,12 +52,18 @@ const prepareLambdaForTest = () => { // Replace placeholders with test values const replacements = { '##WEB_FRONTEND##': environmentValues.webFrontend, + // JCC '##DATA_API##': environmentValues.dataApi, '##SEARCH_API##': environmentValues.searchApi, '##S3_UPLOAD_URL_STATE##': environmentValues.s3UploadUrlState, '##S3_UPLOAD_URL_PROVIDER##': environmentValues.s3UploadUrlProvider, '##COGNITO_STAFF##': environmentValues.cognitoStaff, '##COGNITO_PROVIDER##': environmentValues.cognitoProvider, + // COSMETOLOGY + '##DATA_API_COSMO##': environmentValues.dataApiCosmo, + '##SEARCH_API_COSMO##': environmentValues.searchApiCosmo, + '##S3_UPLOAD_URL_STATE_COSMO##': environmentValues.s3UploadUrlStateCosmo, + '##COGNITO_STAFF_COSMO##': environmentValues.cognitoStaffCosmo, }; // Apply all replacements to the Lambda code @@ -70,13 +82,19 @@ const prepareLambdaForTest = () => { }; const buildCspHeaders = (environment) => { + const cognitoIdpUrl = 'https://cognito-idp.us-east-1.amazonaws.com'; + // JCC const dataApiUrl = (environment?.dataApi) ? `https://${environment.dataApi}` : ''; const searchApiUrl = (environment?.searchApi) ? `https://${environment.searchApi}` : ''; const s3UploadUrlState = (environment?.s3UploadUrlState) ? `https://${environment.s3UploadUrlState}` : ''; const s3UploadUrlProvider = (environment?.s3UploadUrlProvider) ? `https://${environment.s3UploadUrlProvider}` : ''; const cognitoStaffUrl = (environment?.cognitoStaff) ? `https://${environment.cognitoStaff}` : ''; const cognitoProviderUrl = (environment?.cognitoProvider) ? `https://${environment.cognitoProvider}` : ''; - const cognitoIdpUrl = 'https://cognito-idp.us-east-1.amazonaws.com'; + // COSMETOLOGY + const dataApiUrlCosmo = (environment?.dataApiCosmo) ? `https://${environment.dataApiCosmo}` : ''; + const searchApiUrlCosmo = (environment?.searchApiCosmo) ? `https://${environment.searchApiCosmo}` : ''; + const s3UploadUrlStateCosmo = (environment?.s3UploadUrlStateCosmo) ? `https://${environment.s3UploadUrlStateCosmo}` : ''; + const cognitoStaffUrlCosmo = (environment?.cognitoStaffCosmo) ? `https://${environment.cognitoStaffCosmo}` : ''; // src configs are maintained here as arrays for ease of maintenance; // defining them as static strings could lead to long lines of code. const cspDefaultSrc = [ @@ -128,11 +146,13 @@ const buildCspHeaders = (environment) => { '\'self\'', 'data:', dataApiUrl, + dataApiUrlCosmo, 'https://www.gstatic.com/recaptcha/', ].join(' '); const cspMediaSrc = [ '\'self\'', dataApiUrl, + dataApiUrlCosmo, ].join(' '); const cspFrameSrc = [ '\'self\'', @@ -152,14 +172,20 @@ const buildCspHeaders = (environment) => { ].join(' '); const cspConnectSrc = [ '\'self\'', + cognitoIdpUrl, + 'https://www.google.com/recaptcha/', + // JCC dataApiUrl, searchApiUrl, s3UploadUrlState, s3UploadUrlProvider, cognitoStaffUrl, cognitoProviderUrl, - cognitoIdpUrl, - 'https://www.google.com/recaptcha/', + // COSMETOLOGY + dataApiUrlCosmo, + searchApiUrlCosmo, + s3UploadUrlStateCosmo, + cognitoStaffUrlCosmo, // Begin Statsig domains 'https://api.statsig.com/', 'https://featuregates.org/', diff --git a/backend/compact-connect-ui-app/lambdas/nodejs/package.json b/backend/compact-connect-ui-app/lambdas/nodejs/package.json index 8ffb19d76..ccc9dbfdd 100644 --- a/backend/compact-connect-ui-app/lambdas/nodejs/package.json +++ b/backend/compact-connect-ui-app/lambdas/nodejs/package.json @@ -30,5 +30,8 @@ "@aws-sdk/client-s3": "^3.682.0", "@aws-sdk/util-dynamodb": "^3.682.0", "zod": "^3.23.8" + }, + "resolutions": { + "fast-xml-parser": "5.3.6" } } diff --git a/backend/compact-connect-ui-app/lambdas/nodejs/yarn.lock b/backend/compact-connect-ui-app/lambdas/nodejs/yarn.lock index ba6e6914d..2f1410018 100644 --- a/backend/compact-connect-ui-app/lambdas/nodejs/yarn.lock +++ b/backend/compact-connect-ui-app/lambdas/nodejs/yarn.lock @@ -2143,12 +2143,12 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-xml-parser@4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f" - integrity sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw== +fast-xml-parser@4.4.1, fast-xml-parser@5.3.6: + version "5.3.6" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz#85a69117ca156b1b3c52e426495b6de266cb6a4b" + integrity sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA== dependencies: - strnum "^1.0.5" + strnum "^2.1.2" fecha@^4.2.0: version "4.2.3" @@ -2917,10 +2917,10 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strnum@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" - integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== +strnum@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.1.2.tgz#a5e00ba66ab25f9cafa3726b567ce7a49170937a" + integrity sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ== supports-color@^7, supports-color@^7.1.0: version "7.2.0" diff --git a/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/__init__.py b/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/__init__.py index f21ba5170..0b4b3217b 100644 --- a/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/__init__.py +++ b/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/__init__.py @@ -3,6 +3,7 @@ from common_constructs.access_logs_bucket import AccessLogsBucket from common_constructs.bucket import Bucket from common_constructs.frontend_app_config_utility import ( + AppId, PersistentStackFrontendAppConfigValues, ProviderUsersStackFrontendAppConfigValues, ) @@ -39,6 +40,11 @@ def __init__( persistent_stack_frontend_app_config_values = ( PersistentStackFrontendAppConfigValues.load_persistent_stack_values_from_ssm_parameter(self) ) + persistent_stack_frontend_app_config_values_cosmetology = ( + PersistentStackFrontendAppConfigValues.load_persistent_stack_values_from_ssm_parameter( + self, app_id=AppId.COSMETOLOGY + ) + ) provider_users_stack_frontend_app_config_values = ( ProviderUsersStackFrontendAppConfigValues.load_provider_users_stack_values_from_ssm_parameter(self) ) @@ -51,6 +57,12 @@ def __init__( 'Persistent Stack App Configuration not found in SSM. ' 'Make sure Persistent Stack resources have been deployed.' ) + if persistent_stack_frontend_app_config_values_cosmetology is None: + raise ValueError( + 'Persistent Stack App Configuration (cosmetology) not found in SSM. ' + 'Make sure Cosmetology Persistent Stack resources have been deployed and the parameter ' + 'has been copied to this account.' + ) if provider_users_stack_frontend_app_config_values is None: raise ValueError( 'Provider Users Stack App Configuration not found in SSM. ' @@ -100,6 +112,7 @@ def __init__( ui_bucket=self.ui_bucket, environment_context=environment_context, persistent_stack_app_config_values=persistent_stack_frontend_app_config_values, + persistent_stack_app_config_values_cosmetology=persistent_stack_frontend_app_config_values_cosmetology, provider_users_stack_app_config_values=provider_users_stack_frontend_app_config_values, ) @@ -110,5 +123,6 @@ def __init__( security_profile=security_profile, access_logs_bucket=self.frontend_access_logs_bucket, persistent_stack_frontend_app_config_values=persistent_stack_frontend_app_config_values, + persistent_stack_frontend_app_config_values_cosmetology=persistent_stack_frontend_app_config_values_cosmetology, provider_users_stack_frontend_app_config_values=provider_users_stack_frontend_app_config_values, ) diff --git a/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/deployment.py b/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/deployment.py index dc6f714f1..2bd452ad3 100644 --- a/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/deployment.py +++ b/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/deployment.py @@ -24,6 +24,7 @@ def __init__( ui_bucket: IBucket, environment_context: dict, persistent_stack_app_config_values: PersistentStackFrontendAppConfigValues, + persistent_stack_app_config_values_cosmetology: PersistentStackFrontendAppConfigValues, provider_users_stack_app_config_values: ProviderUsersStackFrontendAppConfigValues, ): stack = Stack.of(scope) @@ -64,11 +65,17 @@ def __init__( 'VUE_APP_API_LICENSE_ROOT': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.api_domain_name}', 'VUE_APP_API_SEARCH_ROOT': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.search_api_domain_name}', 'VUE_APP_API_USER_ROOT': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.api_domain_name}', + 'VUE_APP_API_STATE_ROOT_COSMO': f'{HTTPS_PREFIX}{persistent_stack_app_config_values_cosmetology.api_domain_name}', + 'VUE_APP_API_LICENSE_ROOT_COSMO': f'{HTTPS_PREFIX}{persistent_stack_app_config_values_cosmetology.api_domain_name}', + 'VUE_APP_API_SEARCH_ROOT_COSMO': f'{HTTPS_PREFIX}{persistent_stack_app_config_values_cosmetology.search_api_domain_name}', + 'VUE_APP_API_USER_ROOT_COSMO': f'{HTTPS_PREFIX}{persistent_stack_app_config_values_cosmetology.api_domain_name}', 'VUE_APP_COGNITO_REGION': 'us-east-1', 'VUE_APP_COGNITO_AUTH_DOMAIN_STAFF': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.staff_cognito_domain}', 'VUE_APP_COGNITO_CLIENT_ID_STAFF': persistent_stack_app_config_values.staff_cognito_client_id, 'VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE': f'{HTTPS_PREFIX}{provider_users_stack_app_config_values.provider_cognito_domain}', 'VUE_APP_COGNITO_CLIENT_ID_LICENSEE': provider_users_stack_app_config_values.provider_cognito_client_id, + 'VUE_APP_COGNITO_AUTH_DOMAIN_STAFF_COSMO': f'{HTTPS_PREFIX}{persistent_stack_app_config_values_cosmetology.staff_cognito_domain}', + 'VUE_APP_COGNITO_CLIENT_ID_STAFF_COSMO': persistent_stack_app_config_values_cosmetology.staff_cognito_client_id, 'VUE_APP_RECAPTCHA_KEY': recaptcha_public_key, 'VUE_APP_STATSIG_KEY': statsig_client_key, }, diff --git a/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/distribution.py b/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/distribution.py index d9a3e781e..e25758dc4 100644 --- a/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/distribution.py +++ b/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/distribution.py @@ -1,3 +1,6 @@ +# ruff: noqa: E501 line-too-long +# For the sake of readability, we don't want to break up environment values into separate lines + import os from aws_cdk.aws_certificatemanager import Certificate, CertificateValidation @@ -34,6 +37,7 @@ def generate_csp_lambda_code( persistent_stack_values: PersistentStackFrontendAppConfigValues, + persistent_stack_values_cosmetology: PersistentStackFrontendAppConfigValues, provider_users_stack_values: ProviderUsersStackFrontendAppConfigValues, ) -> str: """ @@ -45,6 +49,7 @@ def generate_csp_lambda_code( This function reads the template file and replaces placeholders with actual values. :param persistent_stack_values: The values from the persistent stack + :param persistent_stack_values_cosmetology: The values from the cosmetology persistent stack :param provider_users_stack_values: The values from the provider users stack :return: The generated Lambda function code """ @@ -56,12 +61,18 @@ def generate_csp_lambda_code( # Replace placeholders with actual values replacements = { '##WEB_FRONTEND##': persistent_stack_values.ui_domain_name, + # JCC '##DATA_API##': persistent_stack_values.api_domain_name, '##SEARCH_API##': persistent_stack_values.search_api_domain_name, '##S3_UPLOAD_URL_STATE##': f'{persistent_stack_values.bulk_uploads_bucket_name}{S3_URL_SUFFIX}', '##S3_UPLOAD_URL_PROVIDER##': f'{persistent_stack_values.provider_users_bucket_name}{S3_URL_SUFFIX}', '##COGNITO_STAFF##': persistent_stack_values.staff_cognito_domain, '##COGNITO_PROVIDER##': provider_users_stack_values.provider_cognito_domain, + # COSMETOLOGY + '##DATA_API_COSMO##': persistent_stack_values_cosmetology.api_domain_name, + '##SEARCH_API_COSMO##': persistent_stack_values_cosmetology.search_api_domain_name, + '##S3_UPLOAD_URL_STATE_COSMO##': f'{persistent_stack_values_cosmetology.bulk_uploads_bucket_name}{S3_URL_SUFFIX}', + '##COGNITO_STAFF_COSMO##': persistent_stack_values_cosmetology.staff_cognito_domain, } for placeholder, value in replacements.items(): @@ -80,6 +91,7 @@ def __init__( security_profile: SecurityProfile = SecurityProfile.RECOMMENDED, access_logs_bucket: AccessLogsBucket, persistent_stack_frontend_app_config_values: PersistentStackFrontendAppConfigValues, + persistent_stack_frontend_app_config_values_cosmetology: PersistentStackFrontendAppConfigValues, provider_users_stack_frontend_app_config_values: ProviderUsersStackFrontendAppConfigValues, ): stack: AppStack = AppStack.of(scope) @@ -118,7 +130,9 @@ def __init__( # Generate the CSP Lambda code with injected values csp_function_code = generate_csp_lambda_code( - persistent_stack_frontend_app_config_values, provider_users_stack_frontend_app_config_values + persistent_stack_frontend_app_config_values, + persistent_stack_frontend_app_config_values_cosmetology, + provider_users_stack_frontend_app_config_values ) self.csp_function = Function( diff --git a/backend/compact-connect-ui-app/tests/resources/snapshots/BetaFrontend-FrontendDeploymentStack-UI_DISTRIBUTION.json b/backend/compact-connect-ui-app/tests/resources/snapshots/BetaFrontend-FrontendDeploymentStack-UI_DISTRIBUTION.json index 385fb6a91..dc8b372d4 100644 --- a/backend/compact-connect-ui-app/tests/resources/snapshots/BetaFrontend-FrontendDeploymentStack-UI_DISTRIBUTION.json +++ b/backend/compact-connect-ui-app/tests/resources/snapshots/BetaFrontend-FrontendDeploymentStack-UI_DISTRIBUTION.json @@ -39,7 +39,7 @@ { "EventType": "viewer-response", "LambdaFunctionARN": { - "Ref": "CSPFunctionCurrentVersionB61A6611b3705bb7a3e3440bc14c943a82111069" + "Ref": "CSPFunctionCurrentVersionB61A6611f2f39c390a3d6bdfa6597b3ffeb69920" } } ], diff --git a/backend/compact-connect-ui-app/tests/resources/snapshots/BetaFrontend-FrontendDeploymentStack-UI_DISTRIBUTION_LAMBDA_FUNCTION.json b/backend/compact-connect-ui-app/tests/resources/snapshots/BetaFrontend-FrontendDeploymentStack-UI_DISTRIBUTION_LAMBDA_FUNCTION.json index 3daaba274..7f25a36d5 100644 --- a/backend/compact-connect-ui-app/tests/resources/snapshots/BetaFrontend-FrontendDeploymentStack-UI_DISTRIBUTION_LAMBDA_FUNCTION.json +++ b/backend/compact-connect-ui-app/tests/resources/snapshots/BetaFrontend-FrontendDeploymentStack-UI_DISTRIBUTION_LAMBDA_FUNCTION.json @@ -2,7 +2,7 @@ "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "ZipFile": "//\n// index.js\n// CompactConnect\n//\n// Created by InspiringApps on 7/22/2024.\n//\n\n// ============================================================================\n// CONFIGURATION =\n// ============================================================================\n/**\n * Configuration of supported domains.\n *\n * These values are injected into the lambda function at build time. See the\n * `generate_csp_lambda_code` function in\n * backend/compact-connect/stacks/frontend_deployment_stack/distribution.py\n * @type {object}\n */\nconst environmentValues = {\n webFrontend: `test-ui.example.com`,\n dataApi: `test-api.example.com`,\n searchApi: `test-search-api.example.com`,\n s3UploadUrlState: `test-bulk-uploads-bucket-name.s3.amazonaws.com`,\n s3UploadUrlProvider: `test-provider-users-bucket-name.s3.amazonaws.com`,\n cognitoStaff: `test-staff-domain`,\n cognitoProvider: `test-provider-domain`,\n};\n\n// ============================================================================\n// HELPERS =\n// ============================================================================\n/**\n * Get the request domain from the lambda event record.\n * @param {object} eventRecord The cloudfront record from the lambda event.\n * @return {string} The bare domain of the request domain.\n */\n\n/**\n * Get a fully qualified domain URI with the protocol scheme.\n * @param {string} domain The bare domain string.\n * @return {string} The fully-qualified domain string.\n */\nconst getFullyQualified = (domain) => {\n const protocol = 'https://';\n let fullyQualified = '';\n\n if (domain && typeof domain === 'string' && !domain.startsWith(protocol)) {\n fullyQualified = `${protocol}${domain}`;\n }\n\n return fullyQualified;\n};\n\n/**\n * Helper to get the fully-qualified domains for connected services.\n * @return {object} A map of fully-qualified domains for the environment.\n * @return {string} dataApi The data API fully-qualified domain.\n * @return {string} s3UploadUrlState The S3 fully-qualified domain for uploading state files.\n * @return {string} s3UploadUrlProvider The S3 fully-qualified domain for uploading provider files.\n * @return {string} cognitoStaff The Cognito fully-qualified domain for authenticating staff users.\n */\nconst getEnvironmentUrls = () => {\n const environmentUrls = {};\n\n environmentUrls.dataApi = getFullyQualified(environmentValues.dataApi);\n environmentUrls.searchApi = getFullyQualified(environmentValues.searchApi);\n environmentUrls.s3UploadUrlState = getFullyQualified(environmentValues.s3UploadUrlState);\n environmentUrls.s3UploadUrlProvider = getFullyQualified(environmentValues.s3UploadUrlProvider);\n environmentUrls.cognitoStaff = getFullyQualified(environmentValues.cognitoStaff);\n environmentUrls.cognitoProvider = getFullyQualified(environmentValues.cognitoProvider);\n\n return environmentUrls;\n};\n\n/**\n * Helper to escape CSP src keywords.\n * @param {string} keyword The standard keyword value.\n * @return {string} The escaped keyword value.\n */\nconst srcKeywordEscape = (keyword) => {\n let escaped = '';\n\n if (keyword && typeof keyword === 'string') {\n escaped = `'${keyword}'`.toLowerCase();\n }\n\n return escaped;\n};\n\n/**\n * Helper to automatically escape and prep CSP src keyword values (by reference).\n * @param {Array} srcList The CSP src list.\n * @param {string} [listName=''] Optional src list name for logging.\n */\nconst srcKeywordsEscape = (srcList, listName = '') => {\n // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#keyword_values\n const srcKeywordsConfig = [\n { value: 'self', isAllowed: true },\n { value: 'none', isAllowed: true },\n { value: 'strict-dynamic', isAllowed: true },\n { value: 'report-sample', isAllowed: true },\n { value: 'inline-speculation-rules', isAllowed: true },\n { value: 'unsafe-inline', isAllowed: false },\n { value: 'unsafe-eval', isAllowed: false },\n { value: 'unsafe-hashes', isAllowed: false },\n { value: 'wasm-unsafe-eval', isAllowed: false },\n ];\n const srcKeywords = srcKeywordsConfig.map((config) => config.value.toLowerCase());\n\n if (Array.isArray(srcList)) {\n srcList.forEach((srcItem, idx) => {\n const isString = typeof srcItem === 'string';\n\n if (!isString) {\n srcList[idx] = '';\n } else {\n const srcItemLowerCase = srcItem.toLowerCase();\n\n if (srcKeywords.includes(srcItemLowerCase)) {\n const keywordConfig = srcKeywordsConfig.find(\n (config) => srcItemLowerCase === config.value.toLowerCase()\n );\n\n if (keywordConfig) {\n if (!keywordConfig.isAllowed) {\n console.warn(`${listName} ${srcItem} keyword is not allowed in srcKeywordsConfig policy. We likely should not be using this keyword for security reasons.`.trim());\n srcList[idx] = '';\n } else {\n srcList[idx] = srcKeywordEscape(srcItem);\n }\n }\n }\n }\n });\n }\n};\n\n/**\n * Helper to build a CSP src group list string from input params.\n * @param {string} name src name for the CSP group.\n * @param {Array} list The static src list for the CSP group.\n * @return {string} The prepped src list string;\n */\nconst buildSrcString = (name = '', list = []) => {\n let srcString = '';\n\n if (Array.isArray(list)) {\n srcKeywordsEscape(list, name);\n srcString = `${name} ${list.join(' ')};`;\n }\n\n return srcString;\n};\n\n// ============================================================================\n// RESPONSE HEADERS =\n// ============================================================================\n/**\n * Set the CSP header on the response (by reference).\n * @param {object} [headers={}] The event response headers (updated by reference).\n */\nconst setCspHeader = (headers = {}) => {\n const domains = getEnvironmentUrls();\n const cognitoIdpUrl = 'https://cognito-idp.us-east-1.amazonaws.com';\n\n // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy\n headers['content-security-policy'] = [{\n key: 'Content-Security-Policy',\n value: `${[\n `default-src 'none';`,\n buildSrcString('manifest-src', [\n 'self',\n ]),\n buildSrcString('script-src', [\n 'self',\n 'https://www.google.com/recaptcha/',\n 'https://www.gstatic.com/recaptcha/',\n 'https://jstest.authorize.net/',\n 'https://js.authorize.net/',\n ]),\n buildSrcString('script-src-elem', [\n 'self',\n 'https://www.google.com/recaptcha/',\n 'https://www.gstatic.com/recaptcha/',\n 'https://jstest.authorize.net/',\n 'https://js.authorize.net/',\n ]),\n buildSrcString('script-src-attr', [\n 'self',\n ]),\n buildSrcString('worker-src', [\n 'self',\n ]),\n buildSrcString('style-src', [\n 'self',\n 'https://fonts.googleapis.com',\n 'https://www.gstatic.com/recaptcha/',\n ]),\n buildSrcString('style-src-elem', [\n 'self',\n 'https://fonts.googleapis.com',\n 'https://jstest.authorize.net/',\n 'https://js.authorize.net/',\n `'sha256-YwWQHXh4Vw0oD2Oo8pV9huEF2sE9mD8i5nZUuHzEg9A='`, //