143 lines
4.9 KiB
Python
143 lines
4.9 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_runs_api import (
|
||
|
|
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
|