diff --git a/.ds.baseline b/.ds.baseline index fd261c5cf..5c24d852f 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -171,7 +171,7 @@ "filename": "app/config.py", "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", "is_verified": false, - "line_number": 122, + "line_number": 123, "is_secret": false } ], @@ -644,5 +644,5 @@ } ] }, - "generated_at": "2025-05-19T21:41:23Z" + "generated_at": "2025-05-22T19:03:11Z" } diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml index e6371a124..b16607c61 100644 --- a/.github/workflows/deploy-demo.yml +++ b/.github/workflows/deploy-demo.yml @@ -64,6 +64,7 @@ jobs: LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-demo.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE" LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs" API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }} + API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }} with: cf_username: ${{ secrets.CLOUDGOV_USERNAME }} cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} @@ -89,6 +90,7 @@ jobs: --var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL" --var LOGIN_PEM="$LOGIN_PEM" --var API_PUBLIC_URL="$API_PUBLIC_URL" + --var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL" --strategy rolling - name: Deploy egress proxy diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 3954b30f0..05c7ccf26 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -64,6 +64,7 @@ jobs: LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://beta.notify.gov/sign-in&response_type=code&scope=openid+email&state=STATE" LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs" API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }} + API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }} with: cf_username: ${{ secrets.CLOUDGOV_USERNAME }} cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} @@ -89,6 +90,7 @@ jobs: --var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL" --var LOGIN_PEM="$LOGIN_PEM" --var API_PUBLIC_URL="$API_PUBLIC_URL" + --var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL" --strategy rolling - name: Deploy egress proxy diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9b3bf1d91..1a95d7ba1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -70,6 +70,7 @@ jobs: LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE" LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs" API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }} + API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }} with: cf_username: ${{ secrets.CLOUDGOV_USERNAME }} cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} @@ -95,6 +96,7 @@ jobs: --var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL" --var LOGIN_PEM="$LOGIN_PEM" --var API_PUBLIC_URL="$API_PUBLIC_URL" + --var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL" --strategy rolling diff --git a/app/__init__.py b/app/__init__.py index 0b2006689..a6a4a615b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -142,8 +142,9 @@ def _csp(config): asset_domain = config["ASSET_DOMAIN"] logo_domain = config["LOGO_CDN_DOMAIN"] api_public_url = config["API_PUBLIC_URL"] + api_public_ws_url = config["API_PUBLIC_WS_URL"] - csp = { + return { "default-src": ["'self'", asset_domain], "frame-src": [ "https://www.youtube.com", @@ -167,22 +168,13 @@ def _csp(config): "'self'", "https://gov-bam.nr-data.net", "https://www.google-analytics.com", + f"{api_public_url}", + f"{api_public_ws_url}", ], "style-src": ["'self'", asset_domain], "img-src": ["'self'", asset_domain, logo_domain], } - if api_public_url: - csp["connect-src"].append(api_public_url) - # this is for web socket - if api_public_url.startswith("http://"): - ws_url = api_public_url.replace("http://", "ws://") - csp["connect-src"].append(ws_url) - elif api_public_url.startswith("https://"): - ws_url = api_public_url.replace("https://", "wss://") - csp["connect-src"].append(ws_url) - return csp - def create_app(application): @application.after_request diff --git a/app/assets/javascripts/socketio.js b/app/assets/javascripts/socketio.js index 46d18c7d4..754ea5c01 100644 --- a/app/assets/javascripts/socketio.js +++ b/app/assets/javascripts/socketio.js @@ -20,6 +20,14 @@ document.addEventListener('DOMContentLoaded', function () { if (featureEnabled) { const socket = io(apiHost); + socket.on('connect_error', (err) => { + console.error('Socket connect_error:', err); + }); + + socket.on('error', (err) => { + console.error('Socket error:', err); + }); + socket.on('connect', () => { socket.emit('join', { room: `job-${jobId}` }); }); diff --git a/app/config.py b/app/config.py index 42bba9b94..fa8ade775 100644 --- a/app/config.py +++ b/app/config.py @@ -13,6 +13,7 @@ class Config(object): NOTIFY_ENVIRONMENT = getenv("NOTIFY_ENVIRONMENT", "development") API_HOST_NAME = getenv("API_HOST_NAME", "localhost") API_PUBLIC_URL = getenv("API_PUBLIC_URL", "localhost") + API_PUBLIC_WS_URL = getenv("API_PUBLIC_WS_URL", "localhost") ADMIN_BASE_URL = getenv("ADMIN_BASE_URL", "http://localhost:6012") HEADER_COLOUR = ( @@ -132,10 +133,9 @@ class Test(Development): WTF_CSRF_ENABLED = False ASSET_DOMAIN = "static.example.com" ASSET_PATH = "https://static.example.com/" - API_HOST_NAME = "http://you-forgot-to-mock-an-api-call-to" API_PUBLIC_URL = "http://you-forgot-to-mock-an-api-call-to" - + API_PUBLIC_WS_URL = "ws://you-forgot-to-mock-an-api-call-to" REDIS_URL = "redis://you-forgot-to-mock-a-redis-call-to" LOGO_CDN_DOMAIN = "static-logos.test.com" diff --git a/deploy-config/demo.yml b/deploy-config/demo.yml index 08d29d096..5e34827ec 100644 --- a/deploy-config/demo.yml +++ b/deploy-config/demo.yml @@ -9,3 +9,4 @@ nr_agent_id: '1134302465' nr_app_id: '1083160688' FEATURE_SOCKET_ENABLED: true API_PUBLIC_URL: https://notify-api-demo.app.cloud.gov +API_PUBLIC_WS_URL: wss://notify-api-demo.app.cloud.gov diff --git a/deploy-config/production.yml b/deploy-config/production.yml index 7d3a14bee..2655d4b5a 100644 --- a/deploy-config/production.yml +++ b/deploy-config/production.yml @@ -9,3 +9,4 @@ nr_agent_id: '1050708682' nr_app_id: '1050708682' FEATURE_SOCKET_ENABLED: false API_PUBLIC_URL: https://notify-api-production.app.cloud.gov +API_PUBLIC_WS_URL: wss://notify-api-production.app.cloud.gov diff --git a/deploy-config/staging.yml b/deploy-config/staging.yml index 36e6ad78d..260bc334d 100644 --- a/deploy-config/staging.yml +++ b/deploy-config/staging.yml @@ -9,3 +9,4 @@ nr_agent_id: '1134291385' nr_app_id: '1031640326' FEATURE_SOCKET_ENABLED: false API_PUBLIC_URL: https://notify-api-staging.app.cloud.gov +API_PUBLIC_WS_URL: wss://notify-api-staging.app.cloud.gov diff --git a/manifest.yml b/manifest.yml index da4538c8d..2322bb376 100644 --- a/manifest.yml +++ b/manifest.yml @@ -62,6 +62,6 @@ applications: LOGIN_DOT_GOV_CERTS_URL: ((LOGIN_DOT_GOV_CERTS_URL)) API_PUBLIC_URL: ((API_PUBLIC_URL)) - + API_PUBLIC_WS_URL: ((API_PUBLIC_WS_URL)) # feature flagging FEATURE_SOCKET_ENABLED: ((FEATURE_SOCKET_ENABLED)) diff --git a/tests/app/main/views/test_headers.py b/tests/app/main/views/test_headers.py index c051224f9..5551d9035 100644 --- a/tests/app/main/views/test_headers.py +++ b/tests/app/main/views/test_headers.py @@ -1,7 +1,5 @@ from re import search -from flask import current_app - def test_owasp_useful_headers_set( client_request, @@ -27,21 +25,28 @@ def test_owasp_useful_headers_set( csp, ) assert search(r"'nonce-[^']+';", csp) - assert search( - r"connect-src 'self' https:\/\/gov-bam\.nr-data\.net https:\/\/www\.google-analytics\.", - csp, + connect_src = next( + ( + directive + for directive in csp.split(";") + if directive.strip().startswith("connect-src") + ), + None, ) + assert connect_src is not None, "connect-src directive is missing" + from flask import current_app + + config = current_app.config + expected_sources = { + "'self'", + "https://gov-bam.nr-data.net", + "https://www.google-analytics.com", + config["API_PUBLIC_URL"], + config["API_PUBLIC_WS_URL"], + } + actual_sources = set(connect_src.strip().split()[1:]) + assert ( + expected_sources <= actual_sources + ), f"Missing sources in connect-src: {expected_sources - actual_sources}" assert search(r"style-src 'self' static\.example\.com 'nonce-.*';", csp) assert search(r"img-src 'self' static\.example\.com static-logos\.test\.com", csp) - api_public_url = current_app.config.get("API_PUBLIC_URL") - assert api_public_url is not None, f"API_PUBLIC_URL: {api_public_url} — is missing" - - assert api_public_url in csp - if api_public_url.startswith("http://"): - assert api_public_url.replace("http://", "ws://") in csp - elif api_public_url.startswith("https://"): - assert api_public_url.replace("https://", "wss://") in csp - else: - raise AssertionError( - f"Unexpected API_PUBLIC_URL format: {api_public_url} — must start with 'http://' or 'https://'" - )