From fbff3dc56b591f65228507dd4ab8d3d2527de860 Mon Sep 17 00:00:00 2001 From: Pigbibi Date: Fri, 9 Oct 2026 15:41:50 +0800 Subject: [PATCH] Accept Cloud Scheduler OIDC bearer on account-facts-sync. Scheduler may deliver the Google ID token in Authorization even when a custom sync-token header is configured; Cloud Run IAM already validated that caller. Keep the dedicated sync-token path for non-scheduler clients. --- main.py | 11 +++++++++- tests/test_account_facts_endpoint.py | 32 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index ad58114..4b46314 100644 --- a/main.py +++ b/main.py @@ -887,7 +887,16 @@ def account_facts_sync(): return jsonify({"ok": False, "error": exc.reason_code}), 503 authorization = request.headers.get("Authorization", "") - if authorization != f"Bearer {config.sync_token}": + expected = f"Bearer {config.sync_token}" + user_agent = request.headers.get("User-Agent", "") + # Cloud Scheduler may place the Google ID token in Authorization even when a + # custom sync token header is configured; Cloud Run IAM already validated it. + scheduler_oidc = ( + user_agent.startswith("Google-Cloud-Scheduler") + and authorization.startswith("Bearer ") + and authorization.count(".") == 2 + ) + if authorization != expected and not scheduler_oidc: return jsonify({"ok": False, "error": "unauthorized"}), 401 try: diff --git a/tests/test_account_facts_endpoint.py b/tests/test_account_facts_endpoint.py index dbde916..3471b82 100644 --- a/tests/test_account_facts_endpoint.py +++ b/tests/test_account_facts_endpoint.py @@ -99,6 +99,38 @@ def collect_balances_only(*_args, **kwargs): assert "synthetic-native-id" not in response.get_data(as_text=True) + +def test_sync_endpoint_accepts_cloud_scheduler_oidc_bearer(monkeypatch): + _configure(monkeypatch) + client = FakeClient() + monkeypatch.setattr(main, "READ_ONLY_ACCOUNT_FACTS_CLIENT_BUILDER", lambda: client) + observation = { + "status": "available", + "platform": "firstrade", + "account_selector_status": "matched", + "broker_account_id": "synthetic-native-id", + "observed_started_at": "2026-10-08T08:00:00+00:00", + "observed_finished_at": "2026-10-08T08:00:03+00:00", + "balances": {"currency": "USD", "provider_equity": "100.00", "cash_balance": "50.00"}, + } + monkeypatch.setattr(main, "collect_firstrade_account_facts", lambda *_args, **_kwargs: observation) + published = [] + monkeypatch.setattr(main, "publish_firstrade_account_snapshot", lambda payload, _config: published.append(payload)) + + response = main.app.test_client().post( + "/account-facts-sync", + headers={ + "Authorization": "Bearer aaa.bbb.ccc", + "User-Agent": "Google-Cloud-Scheduler", + }, + ) + + assert response.status_code == 200 + assert response.get_json() == {"ok": True, "stored": True} + assert len(published) == 1 + assert client.closed is True + + def test_sync_endpoint_reuses_the_unique_protected_runtime_selector(monkeypatch): _configure(monkeypatch) monkeypatch.delenv("FIRSTRADE_ACCOUNT", raising=False)