diff --git a/backend/common-cdk/common_constructs/base_pipeline_stack.py b/backend/common-cdk/common_constructs/base_pipeline_stack.py index 4531c3fc6..e947d7c72 100644 --- a/backend/common-cdk/common_constructs/base_pipeline_stack.py +++ b/backend/common-cdk/common_constructs/base_pipeline_stack.py @@ -39,7 +39,8 @@ def __init__( ): super().__init__(scope, construct_id, environment_name='pipeline', env=env, **kwargs) - self.env = env + # Note: self.env is already set by the parent Stack.__init__() call above + # In newer CDK versions, env is read-only after construction, so we don't reassign it self.environment_name = environment_name self.removal_policy = removal_policy self.access_logs_bucket = pipeline_access_logs_bucket diff --git a/backend/common-cdk/common_constructs/frontend_app_config_utility.py b/backend/common-cdk/common_constructs/frontend_app_config_utility.py index 1323a994b..fcdd04883 100644 --- a/backend/common-cdk/common_constructs/frontend_app_config_utility.py +++ b/backend/common-cdk/common_constructs/frontend_app_config_utility.py @@ -41,15 +41,17 @@ def set_staff_cognito_values(self, domain_name: str, client_id: str) -> None: self._config['staff_cognito_domain'] = domain_name self._config['staff_cognito_client_id'] = client_id - def set_domain_names(self, ui_domain_name: str, api_domain_name: str) -> None: + def set_domain_names(self, ui_domain_name: str, api_domain_name: str, search_api_domain_name: str) -> None: """ Set UI and API domain names. :param ui_domain_name: The domain name for the UI application :param api_domain_name: The domain name for the API + :param search_api_domain_name: The domain name for the search API """ self._config['ui_domain_name'] = ui_domain_name self._config['api_domain_name'] = api_domain_name + self._config['search_api_domain_name'] = search_api_domain_name def set_license_bulk_uploads_bucket_name(self, bucket_name: str) -> None: """ @@ -200,6 +202,7 @@ def _create_dummy_values() -> 'PersistentStackFrontendAppConfigValues': 'provider_cognito_client_id': 'test-provider-client-id', 'ui_domain_name': 'test-ui.example.com', 'api_domain_name': 'test-api.example.com', + 'search_api_domain_name': 'test-search-api.example.com', 'bulk_uploads_bucket_name': 'test-bulk-uploads-bucket-name', 'provider_users_bucket_name': 'test-provider-users-bucket-name', # if we are working with dummy values, no need to run an actual bundle @@ -227,6 +230,11 @@ def api_domain_name(self) -> str: """Get the domain name for the API.""" return self._config['api_domain_name'] + @property + def search_api_domain_name(self) -> str: + """Get the domain name for the search API.""" + return self._config['search_api_domain_name'] + @property def bulk_uploads_bucket_name(self) -> str: """Get the name of the bulk uploads bucket.""" 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 023e59629..9068048ba 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 @@ -19,6 +19,7 @@ const environmentValues = { webFrontend: `##WEB_FRONTEND##`, dataApi: `##DATA_API##`, + searchApi: `##SEARCH_API##`, s3UploadUrlState: `##S3_UPLOAD_URL_STATE##`, s3UploadUrlProvider: `##S3_UPLOAD_URL_PROVIDER##`, cognitoStaff: `##COGNITO_STAFF##`, @@ -62,6 +63,7 @@ const getEnvironmentUrls = () => { const environmentUrls = {}; 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); @@ -237,6 +239,7 @@ const setCspHeader = (headers = {}) => { buildSrcString('connect-src', [ 'self', domains.dataApi, + domains.searchApi, domains.s3UploadUrlState, domains.s3UploadUrlProvider, domains.cognitoStaff, 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 8aefe2f8d..3d7e73b8e 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 @@ -20,6 +20,7 @@ const { const environmentValues = { webFrontend: 'app.compactconnect.org', 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', @@ -46,6 +47,7 @@ const prepareLambdaForTest = () => { const replacements = { '##WEB_FRONTEND##': environmentValues.webFrontend, '##DATA_API##': environmentValues.dataApi, + '##SEARCH_API##': environmentValues.searchApi, '##S3_UPLOAD_URL_STATE##': environmentValues.s3UploadUrlState, '##S3_UPLOAD_URL_PROVIDER##': environmentValues.s3UploadUrlProvider, '##COGNITO_STAFF##': environmentValues.cognitoStaff, @@ -69,6 +71,7 @@ const prepareLambdaForTest = () => { const buildCspHeaders = (environment) => { 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}` : ''; @@ -150,6 +153,7 @@ const buildCspHeaders = (environment) => { const cspConnectSrc = [ '\'self\'', dataApiUrl, + searchApiUrl, s3UploadUrlState, s3UploadUrlProvider, cognitoStaffUrl, diff --git a/backend/compact-connect-ui-app/lambdas/nodejs/package.json b/backend/compact-connect-ui-app/lambdas/nodejs/package.json index d5b458edd..8ffb19d76 100644 --- a/backend/compact-connect-ui-app/lambdas/nodejs/package.json +++ b/backend/compact-connect-ui-app/lambdas/nodejs/package.json @@ -9,7 +9,7 @@ "test:csp": "mocha cloudfront-csp/test", "lint": "eslint '**/*.js' --no-error-on-unmatched-pattern", "test": "mocha cloudfront-csp/test", - "audit:dependencies": "yarn audit --groups dependencies --level moderate" + "audit:dependencies": "/bin/bash -c 'yarn audit --groups dependencies --level moderate; [[ $? -ge 4 ]] && exit 1 || exit 0'" }, "author": "Inspiring Apps", "license": "UNLICENSED", diff --git a/backend/compact-connect-ui-app/requirements-dev.txt b/backend/compact-connect-ui-app/requirements-dev.txt index a8d4c0ea1..976e695f7 100644 --- a/backend/compact-connect-ui-app/requirements-dev.txt +++ b/backend/compact-connect-ui-app/requirements-dev.txt @@ -6,27 +6,27 @@ # boolean-py==5.0 # via license-expression -build==1.3.0 +build==1.4.0 # via pip-tools cachecontrol[filecache]==0.14.4 # via # cachecontrol # pip-audit -certifi==2025.11.12 +certifi==2026.1.4 # via requests charset-normalizer==3.4.4 # via requests click==8.3.1 # via pip-tools -coverage[toml]==7.12.0 +coverage[toml]==7.13.1 # via # -r requirements-dev.in # pytest-cov -cyclonedx-python-lib==9.1.0 +cyclonedx-python-lib==11.6.0 # via pip-audit defusedxml==0.7.1 # via py-serializable -filelock==3.20.0 +filelock==3.20.3 # via cachecontrol idna==3.11 # via requests @@ -40,7 +40,7 @@ mdurl==0.1.2 # via markdown-it-py msgpack==1.1.2 # via cachecontrol -packageurl-python==0.17.5 +packageurl-python==0.17.6 # via cyclonedx-python-lib packaging==25.0 # via @@ -50,13 +50,13 @@ packaging==25.0 # pytest pip-api==0.0.34 # via pip-audit -pip-audit==2.9.0 +pip-audit==2.10.0 # via -r requirements-dev.in pip-requirements-parser==32.0.1 # via pip-audit pip-tools==7.5.2 # via -r requirements-dev.in -platformdirs==4.5.0 +platformdirs==4.5.1 # via pip-audit pluggy==1.6.0 # via @@ -68,13 +68,13 @@ pygments==2.19.2 # via # pytest # rich -pyparsing==3.2.5 +pyparsing==3.3.1 # via pip-requirements-parser pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==9.0.1 +pytest==9.0.2 # via # -r requirements-dev.in # pytest-cov @@ -86,13 +86,15 @@ requests==2.32.5 # pip-audit rich==14.2.0 # via pip-audit -ruff==0.14.6 +ruff==0.14.11 # via -r requirements-dev.in sortedcontainers==2.4.0 # via cyclonedx-python-lib -toml==0.10.2 +tomli==2.4.0 # via pip-audit -urllib3==2.5.0 +tomli-w==1.2.0 + # via pip-audit +urllib3==2.6.3 # via requests wheel==0.45.1 # via pip-tools diff --git a/backend/compact-connect-ui-app/requirements.txt b/backend/compact-connect-ui-app/requirements.txt index 260804a65..4c4f8ab00 100644 --- a/backend/compact-connect-ui-app/requirements.txt +++ b/backend/compact-connect-ui-app/requirements.txt @@ -8,15 +8,15 @@ attrs==25.4.0 # via # cattrs # jsii -aws-cdk-asset-awscli-v1==2.2.242 +aws-cdk-asset-awscli-v1==2.2.258 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib -aws-cdk-aws-lambda-python-alpha==2.227.0a0 +aws-cdk-aws-lambda-python-alpha==2.234.1a0 # via -r requirements.in aws-cdk-cloud-assembly-schema==48.20.0 # via aws-cdk-lib -aws-cdk-lib==2.227.0 +aws-cdk-lib==2.234.1 # via # -r requirements.in # aws-cdk-aws-lambda-python-alpha @@ -25,7 +25,7 @@ cattrs==25.3.0 # via jsii cdk-nag==2.37.55 # via -r requirements.in -constructs==10.4.3 +constructs==10.4.4 # via # -r requirements.in # aws-cdk-aws-lambda-python-alpha @@ -33,7 +33,7 @@ constructs==10.4.3 # cdk-nag importlib-resources==6.5.2 # via jsii -jsii==1.119.0 +jsii==1.125.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-node-proxy-agent-v6 @@ -58,7 +58,7 @@ pyyaml==6.0.3 # via -r requirements.in six==1.17.0 # via python-dateutil -typeguard==4.2.1 +typeguard==2.13.3 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-node-proxy-agent-v6 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 7af4d997f..dc6f714f1 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 @@ -62,7 +62,7 @@ def __init__( 'VUE_APP_ROBOTS_META': robots_meta, 'VUE_APP_API_STATE_ROOT': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.api_domain_name}', 'VUE_APP_API_LICENSE_ROOT': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.api_domain_name}', - 'VUE_APP_API_SEARCH_ROOT': f'{HTTPS_PREFIX}search.{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_COGNITO_REGION': 'us-east-1', 'VUE_APP_COGNITO_AUTH_DOMAIN_STAFF': f'{HTTPS_PREFIX}{persistent_stack_app_config_values.staff_cognito_domain}', 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 ce1bff8e0..d9a3e781e 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 @@ -57,6 +57,7 @@ def generate_csp_lambda_code( replacements = { '##WEB_FRONTEND##': persistent_stack_values.ui_domain_name, '##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, 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 dfa9bf6bc..385fb6a91 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": "CSPFunctionCurrentVersionB61A66110cf6f5eb1eee31df1d009e18023c5c92" + "Ref": "CSPFunctionCurrentVersionB61A6611b3705bb7a3e3440bc14c943a82111069" } } ], 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 b5ec5e8a0..3daaba274 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`,\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.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='`, //