From ac74551178b0c3230d6494115fde067dea286c2f Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Wed, 2 Sep 2026 06:48:07 +0800 Subject: [PATCH] Harden Cloud Run execution endpoints Co-Authored-By: Codex --- .github/workflows/sync-cloud-run-env.yml | 6 ++- main.py | 2 - tests/test_request_handling.py | 63 +++++++++++++++++------ tests/test_sync_cloud_run_env_workflow.py | 35 +++++++++++++ 4 files changed, 88 insertions(+), 18 deletions(-) diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index 5e88947..61f2849 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -260,9 +260,10 @@ jobs: --platform=managed \ --image="${image}" \ --service-account="${GCP_RUNTIME_SERVICE_ACCOUNT}" \ + --no-allow-unauthenticated \ --ingress=internal \ --max-instances=1 \ - --concurrency=80 \ + --concurrency=1 \ --memory=512Mi \ --cpu=1 \ --timeout=300s \ @@ -756,6 +757,9 @@ jobs: --project="${GCP_PROJECT_ID}" \ --location="${scheduler_location}" \ --uri="${scheduler_uri}" \ + --http-method=POST \ + --oidc-service-account-email="${GCP_SCHEDULER_SERVICE_ACCOUNT}" \ + --oidc-token-audience="${service_url}" \ --schedule="${desired_schedule}" \ --time-zone="${market_timezone}" \ --max-retry-attempts=3 \ diff --git a/main.py b/main.py index 0fd7fa7..ad600c2 100644 --- a/main.py +++ b/main.py @@ -651,7 +651,6 @@ def session_check(): @app.post("/run") -@app.get("/run") def run_strategy(): if not _flag("FIRSTRADE_RUN_STRATEGY_ON_HTTP"): return ( @@ -789,7 +788,6 @@ def paper_execution_command_consumer(): @app.post("/probe") -@app.get("/probe") def probe(): return session_check() diff --git a/tests/test_request_handling.py b/tests/test_request_handling.py index ebcd3e0..6154c38 100644 --- a/tests/test_request_handling.py +++ b/tests/test_request_handling.py @@ -28,11 +28,11 @@ def test_cloud_run_route_contracts_are_registered(): "/healthz": ["GET"], "/profiles": ["GET"], "/smoke": ["GET"], - "/run": ["GET", "POST"], + "/run": ["POST"], "/dry-run": ["GET", "POST"], "/paper-command-consumer": ["POST"], "/monitor-dispatch": ["GET", "POST"], - "/probe": ["GET", "POST"], + "/probe": ["POST"], "/static/": ["GET"], } @@ -56,16 +56,59 @@ def test_health_route_returns_service_contract(monkeypatch): assert "as_of" in payload -def test_run_endpoint_is_disabled_without_explicit_http_gate(monkeypatch): - monkeypatch.delenv("FIRSTRADE_RUN_STRATEGY_ON_HTTP", raising=False) +@pytest.mark.parametrize("strategy_gate", [None, "false", "true"]) +@pytest.mark.parametrize("path", ["/run", "/probe"]) +def test_execution_routes_reject_get_without_runtime_calls(monkeypatch, strategy_gate, path): + if strategy_gate is None: + monkeypatch.delenv("FIRSTRADE_RUN_STRATEGY_ON_HTTP", raising=False) + else: + monkeypatch.setenv("FIRSTRADE_RUN_STRATEGY_ON_HTTP", strategy_gate) + monkeypatch.setenv("FIRSTRADE_RUN_SESSION_CHECK_ON_HTTP", "true") + + def fail_runtime_call(*_args, **_kwargs): + pytest.fail("GET must not reach strategy, session, or broker runtime code") + + monkeypatch.setattr(main, "_runtime_target_enabled_env", fail_runtime_call) + monkeypatch.setattr(main, "_run_strategy_cycle_with_report", fail_runtime_call) + monkeypatch.setattr(main, "run_session_check", fail_runtime_call) + monkeypatch.setattr(main, "FirstradeBrokerClient", fail_runtime_call) client = main.app.test_client() - response = client.get("/run") + response = client.get(path) + + assert response.status_code == 405 + assert "POST" in response.headers["Allow"] + + +@pytest.mark.parametrize( + ("path", "gate"), + [ + ("/run", "FIRSTRADE_RUN_STRATEGY_ON_HTTP"), + ("/probe", "FIRSTRADE_RUN_SESSION_CHECK_ON_HTTP"), + ], +) +def test_execution_posts_still_require_explicit_http_gate(monkeypatch, path, gate): + monkeypatch.delenv(gate, raising=False) + + def fail_runtime_call(*_args, **_kwargs): + pytest.fail("disabled POST must not reach strategy, session, or broker runtime code") + + monkeypatch.setattr(main, "_run_strategy_cycle_with_report", fail_runtime_call) + monkeypatch.setattr(main, "run_session_check", fail_runtime_call) + monkeypatch.setattr(main, "FirstradeBrokerClient", fail_runtime_call) + + response = main.app.test_client().post(path) assert response.status_code == 403 assert response.get_json()["ok"] is False +def test_health_endpoint_remains_available_via_get(): + response = main.app.test_client().get("/health") + + assert response.status_code == 200 + + def test_run_endpoint_calls_strategy_cycle_when_gate_enabled(monkeypatch): monkeypatch.setenv("FIRSTRADE_RUN_STRATEGY_ON_HTTP", "true") monkeypatch.setattr(main, "_should_skip_for_market_hours", lambda: (False, None)) @@ -185,16 +228,6 @@ def test_run_endpoint_returns_500_for_retryable_execution_block(monkeypatch): assert payload["execution_block_retryable"] is True -def test_probe_endpoint_is_disabled_without_explicit_http_gate(monkeypatch): - monkeypatch.delenv("FIRSTRADE_RUN_SESSION_CHECK_ON_HTTP", raising=False) - client = main.app.test_client() - - response = client.get("/probe") - - assert response.status_code == 403 - assert response.get_json()["ok"] is False - - def test_probe_endpoint_calls_service_when_gate_enabled(monkeypatch): monkeypatch.setenv("FIRSTRADE_RUN_SESSION_CHECK_ON_HTTP", "true") sent_messages = [] diff --git a/tests/test_sync_cloud_run_env_workflow.py b/tests/test_sync_cloud_run_env_workflow.py index 69236f7..37638f6 100644 --- a/tests/test_sync_cloud_run_env_workflow.py +++ b/tests/test_sync_cloud_run_env_workflow.py @@ -230,3 +230,38 @@ def test_sync_cloud_run_env_workflow_syncs_scheduler_from_sync_plan(): assert direct_gate < workflow.index( 'desired_precheck_schedule="$(CURRENT_SCHEDULE="${desired_schedule}"' ) + + +def test_sync_cloud_run_env_workflow_hardens_deploy_runtime_boundary(): + workflow_path = Path(__file__).resolve().parents[1] / ".github/workflows/sync-cloud-run-env.yml" + workflow = workflow_path.read_text(encoding="utf-8") + deploy_block = workflow[ + workflow.index('gcloud run deploy "${CLOUD_RUN_SERVICE}"') : workflow.index( + " - name: Check whether env sync is enabled" + ) + ] + + assert "--no-allow-unauthenticated" in deploy_block + assert "--allow-unauthenticated" not in deploy_block + assert "--ingress=internal" in deploy_block + assert "--max-instances=1" in deploy_block + assert "--concurrency=1" in deploy_block + assert "--concurrency=80" not in deploy_block + + +def test_main_scheduler_update_and_create_are_authenticated_post_requests(): + workflow_path = Path(__file__).resolve().parents[1] / ".github/workflows/sync-cloud-run-env.yml" + workflow = workflow_path.read_text(encoding="utf-8") + scheduler_block = workflow[ + workflow.index('scheduler_uri="${service_url}/run"') : workflow.index( + 'managed_scheduler_jobs=("${job_name}")' + ) + ] + + assert scheduler_block.count('gcloud scheduler jobs update http "${job_name}"') == 1 + assert scheduler_block.count('gcloud scheduler jobs create http "${job_name}"') == 1 + assert scheduler_block.count("--http-method=POST") == 2 + assert scheduler_block.count( + '--oidc-service-account-email="${GCP_SCHEDULER_SERVICE_ACCOUNT}"' + ) == 2 + assert scheduler_block.count('--oidc-token-audience="${service_url}"') == 2