activity-core/tests/test_ops_runs_api.py
tegwick b4a7a84211
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 5s
Build and Publish Container Image / build-and-push (push) Successful in 39s
Bind multiple queue worker identities, one token each (WP-0039-T01)
ACTIVITY_CORE_WORKERS maps worker_id=ENV_NAME, where each token env must be
ACTIVITY_CORE_WORKER_TOKEN[_SUFFIX]. Without the map, the legacy single pair
behaves exactly as before. Duplicate identities, missing or shared tokens, a
token equal to the operator token, and an unlisted legacy identity all fail
worker mutations closed with 503. Operator/SSO reads keep working.

Declare per-identity OpenBao paths and an ExternalSecret, not yet applied.
The policy, seeding and rollout are waiting tasks T02-T04, answering
secrets-engine SECRETS-WP-0009-T03 and SECRETS-WP-0011-T04.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 151606@bnt-lap001
Assistant-Session: 3c0a4ad5-bb8b-4bf7-b9f0-fa5f29204e48
2026-09-23 17:38:41 +02:00

277 lines
9.5 KiB
Python

"""Authentication boundary tests for the ops_run worker API."""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from activity_core.ops_run_queue import CloseOpsRunOutcome
from activity_core.ops_runs_api import (
_close_response,
bind_worker_id,
require_worker,
require_worker_or_operator,
router,
)
def _request(headers: dict[str, str] | None = None) -> MagicMock:
request = MagicMock()
request.headers = headers or {}
return request
def _client() -> TestClient:
app = FastAPI()
app.include_router(router)
return TestClient(app)
def test_worker_token_binds_configured_identity(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ACTIVITY_CORE_WORKER_TOKEN", "worker-secret")
monkeypatch.setenv("ACTIVITY_CORE_WORKER_ID", "rein-aharness@railiance01")
authenticated = require_worker(
_request(),
x_worker_token="worker-secret",
)
assert authenticated == "rein-aharness@railiance01"
assert bind_worker_id("rein-aharness@railiance01", authenticated) == authenticated
def test_spoofed_worker_id_is_rejected(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ACTIVITY_CORE_WORKER_TOKEN", "worker-secret")
monkeypatch.setenv("ACTIVITY_CORE_WORKER_ID", "rein-aharness@railiance01")
authenticated = require_worker(_request(), x_worker_token="worker-secret")
with pytest.raises(HTTPException) as exc:
bind_worker_id("another-worker", authenticated)
assert exc.value.status_code == 403
def test_worker_token_without_identity_fails_closed(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("ACTIVITY_CORE_WORKER_TOKEN", "worker-secret")
monkeypatch.delenv("ACTIVITY_CORE_WORKER_ID", raising=False)
with pytest.raises(HTTPException) as exc:
require_worker(_request(), x_worker_token="worker-secret")
assert exc.value.status_code == 503
assert "ACTIVITY_CORE_WORKER_ID" in exc.value.detail
def test_invalid_worker_token_is_rejected(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ACTIVITY_CORE_WORKER_TOKEN", "worker-secret")
monkeypatch.setenv("ACTIVITY_CORE_WORKER_ID", "rein-aharness@railiance01")
with pytest.raises(HTTPException) as exc:
require_worker(_request(), x_worker_token="wrong")
assert exc.value.status_code == 401
def test_operator_or_sso_is_not_worker_identity(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("ACTIVITY_CORE_WORKER_TOKEN", raising=False)
monkeypatch.delenv("ACTIVITY_CORE_WORKER_ID", raising=False)
monkeypatch.setenv("ACTIVITY_CORE_OPERATOR_TOKEN", "operator-secret")
for request, kwargs in (
(_request({"Remote-User": "alice"}), {}),
(_request(), {"authorization": "Bearer operator-secret"}),
):
with pytest.raises(HTTPException) as exc:
require_worker(request, **kwargs)
assert exc.value.status_code == 503
def test_claim_endpoint_rejects_operator_and_spoofed_identity(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("ACTIVITY_CORE_WORKER_TOKEN", "worker-secret")
monkeypatch.setenv("ACTIVITY_CORE_WORKER_ID", "rein-aharness@railiance01")
monkeypatch.setenv("ACTIVITY_CORE_OPERATOR_TOKEN", "operator-secret")
client = _client()
body = {"worker_id": "rein-aharness@railiance01", "limit": 1}
operator = client.post(
"/ops-runs/claim",
json=body,
headers={"X-Operator-Token": "operator-secret"},
)
spoofed = client.post(
"/ops-runs/claim",
json={"worker_id": "another-worker", "limit": 1},
headers={"X-Worker-Token": "worker-secret"},
)
assert operator.status_code == 401
assert spoofed.status_code == 403
def test_unauthenticated_dev_worker_requires_explicit_opt_in(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("ACTIVITY_CORE_WORKER_TOKEN", raising=False)
monkeypatch.delenv("ACTIVITY_CORE_WORKER_ID", raising=False)
monkeypatch.delenv("ACTIVITY_CORE_OPERATOR_TOKEN", raising=False)
monkeypatch.delenv("ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS", raising=False)
with pytest.raises(HTTPException) as exc:
require_worker(_request())
assert exc.value.status_code == 503
monkeypatch.setenv("ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS", "true")
assert require_worker(_request()) == "dev:unauth"
assert bind_worker_id("local-worker", "dev:unauth") == "local-worker"
def test_read_auth_no_longer_defaults_open(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("ACTIVITY_CORE_WORKER_TOKEN", raising=False)
monkeypatch.delenv("ACTIVITY_CORE_WORKER_ID", raising=False)
monkeypatch.delenv("ACTIVITY_CORE_OPERATOR_TOKEN", raising=False)
monkeypatch.delenv("ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS", raising=False)
with pytest.raises(HTTPException) as exc:
require_worker_or_operator(_request())
assert exc.value.status_code == 503
def test_close_response_distinguishes_reconciled_and_refusal_codes(
monkeypatch: pytest.MonkeyPatch,
) -> None:
row = MagicMock()
monkeypatch.setattr(
"activity_core.ops_runs_api.ops_run_to_dict",
lambda value: {"id": "run-1", "state": value.state},
)
row.state = "succeeded"
response = _close_response(CloseOpsRunOutcome("reconciled", row))
assert response == {
"id": "run-1",
"state": "succeeded",
"close_disposition": "reconciled",
}
with pytest.raises(HTTPException) as exc:
_close_response(CloseOpsRunOutcome("expired_lease", row))
assert exc.value.status_code == 409
assert exc.value.detail["code"] == "expired_lease"
def _clear_worker_env(monkeypatch: pytest.MonkeyPatch) -> None:
for name in (
"ACTIVITY_CORE_WORKERS",
"ACTIVITY_CORE_WORKER_ID",
"ACTIVITY_CORE_WORKER_TOKEN",
"ACTIVITY_CORE_WORKER_TOKEN_METERED",
"ACTIVITY_CORE_OPERATOR_TOKEN",
"ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS",
):
monkeypatch.delenv(name, raising=False)
def _two_workers(monkeypatch: pytest.MonkeyPatch) -> None:
_clear_worker_env(monkeypatch)
monkeypatch.setenv(
"ACTIVITY_CORE_WORKERS",
"rein-aharness@railiance01=ACTIVITY_CORE_WORKER_TOKEN,"
"rein-aharness-metered@railiance01=ACTIVITY_CORE_WORKER_TOKEN_METERED",
)
monkeypatch.setenv("ACTIVITY_CORE_WORKER_TOKEN", "loop-secret")
monkeypatch.setenv("ACTIVITY_CORE_WORKER_TOKEN_METERED", "metered-secret")
def test_each_worker_token_binds_exactly_its_own_identity(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_two_workers(monkeypatch)
loop = require_worker(_request(), x_worker_token="loop-secret")
metered = require_worker(_request(), authorization="Bearer metered-secret")
assert loop == "rein-aharness@railiance01"
assert metered == "rein-aharness-metered@railiance01"
with pytest.raises(HTTPException) as exc:
bind_worker_id("rein-aharness@railiance01", metered)
assert exc.value.status_code == 403
with pytest.raises(HTTPException) as exc:
require_worker(_request(), x_worker_token="unknown")
assert exc.value.status_code == 401
def test_claim_endpoint_rejects_cross_identity_body(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_two_workers(monkeypatch)
response = _client().post(
"/ops-runs/claim",
json={"worker_id": "rein-aharness@railiance01", "limit": 1},
headers={"X-Worker-Token": "metered-secret"},
)
assert response.status_code == 403
@pytest.mark.parametrize(
("workers", "env", "fragment"),
[
("a=ACTIVITY_CORE_WORKER_TOKEN,b=ACTIVITY_CORE_WORKER_TOKEN_METERED",
{"ACTIVITY_CORE_WORKER_TOKEN": "same", "ACTIVITY_CORE_WORKER_TOKEN_METERED": "same"},
"shared"),
("a=ACTIVITY_CORE_WORKER_TOKEN,a=ACTIVITY_CORE_WORKER_TOKEN_METERED",
{"ACTIVITY_CORE_WORKER_TOKEN": "x", "ACTIVITY_CORE_WORKER_TOKEN_METERED": "y"},
"duplicate"),
("a=ACTIVITY_CORE_WORKER_TOKEN,b=ACTIVITY_CORE_WORKER_TOKEN_METERED",
{"ACTIVITY_CORE_WORKER_TOKEN": "x"},
"ACTIVITY_CORE_WORKER_TOKEN_METERED"),
("a=ACTIVITY_CORE_OPERATOR_TOKEN",
{"ACTIVITY_CORE_OPERATOR_TOKEN": "op"},
"invalid"),
("a=ACTIVITY_CORE_WORKER_TOKEN",
{"ACTIVITY_CORE_WORKER_TOKEN": "op", "ACTIVITY_CORE_OPERATOR_TOKEN": "op"},
"operator"),
("a=ACTIVITY_CORE_WORKER_TOKEN",
{"ACTIVITY_CORE_WORKER_TOKEN": "x", "ACTIVITY_CORE_WORKER_ID": "b"},
"not listed"),
],
)
def test_ambiguous_worker_config_fails_closed(
monkeypatch: pytest.MonkeyPatch,
workers: str,
env: dict[str, str],
fragment: str,
) -> None:
_clear_worker_env(monkeypatch)
monkeypatch.setenv("ACTIVITY_CORE_WORKERS", workers)
for name, value in env.items():
monkeypatch.setenv(name, value)
with pytest.raises(HTTPException) as exc:
require_worker(_request(), x_worker_token="x")
assert exc.value.status_code == 503
assert fragment in exc.value.detail
def test_broken_worker_config_does_not_lock_out_operator(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_clear_worker_env(monkeypatch)
monkeypatch.setenv("ACTIVITY_CORE_WORKERS", "a=ACTIVITY_CORE_WORKER_TOKEN_MISSING")
monkeypatch.setenv("ACTIVITY_CORE_OPERATOR_TOKEN", "operator-secret")
principal = require_worker_or_operator(
_request(), authorization="Bearer operator-secret"
)
assert principal == "operator:token"