diff --git a/.github/workflows/check-webroot.yml b/.github/workflows/check-webroot.yml index 836fb733a..a8180b8e5 100644 --- a/.github/workflows/check-webroot.yml +++ b/.github/workflows/check-webroot.yml @@ -17,6 +17,11 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: +# Permission can be added at job level or workflow level +permissions: + id-token: write # This is required for requesting the JWT + contents: read # This is required for actions/checkout + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: CheckWebroot: @@ -78,6 +83,7 @@ jobs: env: NODE_ENV: production BASE_URL: ${{ env.BASE_URL }} + VUE_APP_ENV: production VUE_APP_DOMAIN: ${{ env.VUE_APP_DOMAIN }} VUE_APP_ROBOTS_META: ${{ env.VUE_APP_ROBOTS_META }} VUE_APP_API_STATE_ROOT: ${{ env.VUE_APP_API_STATE_ROOT }} @@ -88,5 +94,6 @@ jobs: 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_RECAPTCHA_KEY: ${{ env.VUE_APP_RECAPTCHA_KEY }} + VUE_APP_MOCK_API: true run: yarn build working-directory: ./webroot diff --git a/backend/compact-connect-ui-app/cdk.context.beta-example.json b/backend/compact-connect-ui-app/cdk.context.beta-example.json index 593898f91..fb0ab02dc 100644 --- a/backend/compact-connect-ui-app/cdk.context.beta-example.json +++ b/backend/compact-connect-ui-app/cdk.context.beta-example.json @@ -13,7 +13,9 @@ "region": "us-east-1", "domain_name": "beta.compactconnect.org", "recaptcha_public_key": "123-KFEUsjehfuejILDVUKkRnAF9SSzb8o9uv5lY7Ih", - "robots_meta": "noindex,nofollow" + "robots_meta": "noindex,nofollow", + "statsig_key": "client-KFEUsjehfuejILDVUKkRnAF9SSzb8o9uv5lY7Ih", + "app_env": "beta" } } } diff --git a/backend/compact-connect-ui-app/cdk.context.prod-example.json b/backend/compact-connect-ui-app/cdk.context.prod-example.json index f591fdead..53bd78d0f 100644 --- a/backend/compact-connect-ui-app/cdk.context.prod-example.json +++ b/backend/compact-connect-ui-app/cdk.context.prod-example.json @@ -13,7 +13,9 @@ "region": "us-east-1", "domain_name": "compactconnect.org", "robots_meta": "index,follow", - "recaptcha_public_key": "123-KFEUsjehfuejILDVUKkRnAF9SSzb8o9uv5lY7Ih" + "recaptcha_public_key": "123-KFEUsjehfuejILDVUKkRnAF9SSzb8o9uv5lY7Ih", + "statsig_key": "client-KFEUsjehfuejILDVUKkRnAF9SSzb8o9uv5lY7Ih", + "app_env": "production" } } } diff --git a/backend/compact-connect-ui-app/cdk.context.sandbox-example.json b/backend/compact-connect-ui-app/cdk.context.sandbox-example.json index af98a3d87..7818710c4 100644 --- a/backend/compact-connect-ui-app/cdk.context.sandbox-example.json +++ b/backend/compact-connect-ui-app/cdk.context.sandbox-example.json @@ -10,7 +10,9 @@ "region": "us-east-1", "domain_name": "justin.compactconnect.org", "recaptcha_public_key": "123-KFEUsjehfuejILDVUKkRnAF9SSzb8o9uv5lY7Ih", - "robots_meta": "noindex,nofollow" + "robots_meta": "noindex,nofollow", + "statsig_key": "client-KFEUsjehfuejILDVUKkRnAF9SSzb8o9uv5lY7Ih", + "app_env": "local" } } } diff --git a/backend/compact-connect-ui-app/cdk.context.test-example.json b/backend/compact-connect-ui-app/cdk.context.test-example.json index c291c1468..2a1071aa1 100644 --- a/backend/compact-connect-ui-app/cdk.context.test-example.json +++ b/backend/compact-connect-ui-app/cdk.context.test-example.json @@ -13,7 +13,9 @@ "region": "us-east-1", "domain_name": "test.compactconnect.org", "recaptcha_public_key": "123-KFEUsjehfuejILDVUKkRnAF9SSzb8o9uv5lY7Ih", - "robots_meta": "noindex,nofollow" + "robots_meta": "noindex,nofollow", + "statsig_key": "client-KFEUsjehfuejILDVUKkRnAF9SSzb8o9uv5lY7Ih", + "app_env": "csg-test" } } } diff --git a/backend/compact-connect-ui-app/lambdas/nodejs/README.md b/backend/compact-connect-ui-app/lambdas/nodejs/README.md index 89e7d6036..20ed86c09 100644 --- a/backend/compact-connect-ui-app/lambdas/nodejs/README.md +++ b/backend/compact-connect-ui-app/lambdas/nodejs/README.md @@ -22,7 +22,7 @@ _[back to top](#ingest-event-reporter-lambda)_ --- ## Local development - **Linting** - - `yarn run lint` + - `yarn lint` - Lints all code in all the Lambda function - **Running an individual Lambda** - The easiest way to execute the Lambda is to run the tests ([see below](#tests)) @@ -33,7 +33,6 @@ _[back to top](#ingest-event-reporter-lambda)_ --- ## Testing This project uses `jest` and `aws-sdk-client-mock` for approachable unit testing. The code in this folder can be tested by running: -- `yarn install` -- `yarn test` +- `yarn test:csg` or by using the utility scripts located at `backend/bin`. 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 26c896b3f..023e59629 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 @@ -243,6 +243,18 @@ const setCspHeader = (headers = {}) => { domains.cognitoProvider, cognitoIdpUrl, 'https://www.google.com/recaptcha/', + // Begin Statsig domains + 'https://api.statsig.com/', + 'https://featuregates.org/', + 'https://statsigapi.net/', + 'https://events.statsigapi.net/', + 'https://api.statsigcdn.com/', + 'https://featureassets.org/', + 'https://assetsconfigcdn.org/', + 'https://prodregistryv2.org/', + 'https://cloudflare-dns.com/', + 'https://beyondwickedmapping.org/', + // End Statsig domains ]), ].join(' ')}`, }]; 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 4ba75f99d..157766d5b 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 @@ -156,6 +156,18 @@ const buildCspHeaders = (environment) => { cognitoProviderUrl, cognitoIdpUrl, 'https://www.google.com/recaptcha/', + // Begin Statsig domains + 'https://api.statsig.com/', + 'https://featuregates.org/', + 'https://statsigapi.net/', + 'https://events.statsigapi.net/', + 'https://api.statsigcdn.com/', + 'https://featureassets.org/', + 'https://assetsconfigcdn.org/', + 'https://prodregistryv2.org/', + 'https://cloudflare-dns.com/', + 'https://beyondwickedmapping.org/', + // End Statsig domains ].join(' '); return `${[ 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 8a0641e0d..c61778af3 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 @@ -31,6 +31,8 @@ def __init__( # Get environment-specific values from context recaptcha_public_key = environment_context['recaptcha_public_key'] robots_meta = environment_context['robots_meta'] + statsig_client_key = environment_context['statsig_key'] + app_env = environment_context['app_env'] super().__init__( scope, @@ -56,6 +58,7 @@ def __init__( image=DockerImage('public.ecr.aws/lts/ubuntu:22.04_stable'), environment={ 'BASE_URL': '/', + 'VUE_APP_ENV': app_env, 'VUE_APP_DOMAIN': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.ui_domain_name}', 'VUE_APP_ROBOTS_META': robots_meta, 'VUE_APP_API_STATE_ROOT': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.api_domain_name}', @@ -67,6 +70,7 @@ def __init__( 'VUE_APP_COGNITO_AUTH_DOMAIN_LICENSEE': f'{HTTPS_PREFIX}{provider_users_stack_app_config_values.provider_cognito_domain}{COGNITO_AUTH_DOMAIN_SUFFIX}', 'VUE_APP_COGNITO_CLIENT_ID_LICENSEE': provider_users_stack_app_config_values.provider_cognito_client_id, 'VUE_APP_RECAPTCHA_KEY': recaptcha_public_key, + 'VUE_APP_STATSIG_KEY': statsig_client_key, }, entrypoint=['bash'], command=['bin/build.sh'], 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 3e9d89a57..d82fbaaac 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": "CSPFunctionCurrentVersionB61A66115988ea1180930366a7af32c8681342bd" + "Ref": "CSPFunctionCurrentVersionB61A6611c49f5a41519db73b59488deeb4e8a5bc" } } ], 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 edb3ad0d9..1b9a360fa 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 s3UploadUrlState: `test-bulk-uploads-bucket-name.s3.amazonaws.com`,\n s3UploadUrlProvider: `test-provider-users-bucket-name.s3.amazonaws.com`,\n cognitoStaff: `test-staff-domain.auth.us-east-1.amazoncognito.com`,\n cognitoProvider: `test-provider-domain.auth.us-east-1.amazoncognito.com`,\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.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='`, // diff --git a/webroot/src/components/UserAccount/UserAccount.vue b/webroot/src/components/UserAccount/UserAccount.vue index 6fef6b507..b6d2461e1 100644 --- a/webroot/src/components/UserAccount/UserAccount.vue +++ b/webroot/src/components/UserAccount/UserAccount.vue @@ -10,9 +10,9 @@

{{ $t('account.accountTitle') }}