Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions horizon/tests/test_enforcer_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ def test_enforcer_endpoint_invalid_token_returns_401(endpoint):
assert response.json()["detail"] == "Invalid PDP token"


@pytest.mark.parametrize("endpoint", PROTECTED_ENFORCER_ENDPOINTS)
@pytest.mark.parametrize("value", ["garbage", "Bearer", "Bearer ", "Bearer a b c"])
def test_enforcer_endpoint_malformed_header_is_401_not_500(endpoint, value):
client = TestClient(sidecar._app)
response = client.post(endpoint, headers={"authorization": value}, json={})
assert response.status_code == status.HTTP_401_UNAUTHORIZED


def test_health_endpoint_is_public(monkeypatch):
monkeypatch.setattr(stats_manager, "_had_failure", False)
client = TestClient(sidecar._app)
Expand Down
37 changes: 28 additions & 9 deletions horizon/tests/test_legacy_update_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,26 @@ def test_update_policy_rejects_unauthenticated(pdp: MockPermitPDP, monkeypatch):
monkeypatch.setattr(pdp._opal.policy_updater, "trigger_update_policy", trigger)
client = TestClient(pdp._app)

# A missing header is rejected before the handler runs: 422 while the
# `authorization` param has no default (FastAPI required-param validation),
# 401 once enforce_pdp_token gains `= None` (PER-15244 / #317). Accept both
# so this survives either merge order, while still failing on an accidental
# 200 (auth bypass) or 500. An invalid token is 401 in both regimes, and the
# updater must never run for an unauthenticated caller either way.
missing = client.post("/update_policy", follow_redirects=False)
assert missing.status_code in (401, 422)
assert missing.status_code == 401
assert missing.json()["detail"] == "Missing Authorization header"
invalid = client.post("/update_policy", headers={"authorization": "Bearer wrong"}, follow_redirects=False)
assert invalid.status_code == 401
trigger.assert_not_awaited()


@pytest.mark.parametrize("value", ["garbage", "Bearer", "Bearer ", "Bearer a b c"])
def test_update_policy_malformed_header_is_401_not_500(pdp: MockPermitPDP, monkeypatch, value: str):
trigger = AsyncMock()
monkeypatch.setattr(pdp._opal.policy_updater, "trigger_update_policy", trigger)
client = TestClient(pdp._app)

response = client.post("/update_policy", headers={"authorization": value}, follow_redirects=False)

assert response.status_code == 401
trigger.assert_not_awaited()


def test_update_policy_data_triggers_updater(pdp: MockPermitPDP, auth: dict[str, str], monkeypatch):
get_base = AsyncMock()
monkeypatch.setattr(pdp._opal.data_updater, "get_base_policy_data", get_base)
Expand All @@ -78,14 +85,26 @@ def test_update_policy_data_rejects_unauthenticated(pdp: MockPermitPDP, monkeypa
monkeypatch.setattr(pdp._opal.data_updater, "get_base_policy_data", get_base)
client = TestClient(pdp._app)

# See test_update_policy_rejects_unauthenticated for the 401/422 dual regime.
missing = client.post("/update_policy_data", follow_redirects=False)
assert missing.status_code in (401, 422)
assert missing.status_code == 401
assert missing.json()["detail"] == "Missing Authorization header"
invalid = client.post("/update_policy_data", headers={"authorization": "Bearer wrong"}, follow_redirects=False)
assert invalid.status_code == 401
get_base.assert_not_awaited()


@pytest.mark.parametrize("value", ["garbage", "Bearer", "Bearer ", "Bearer a b c"])
def test_update_policy_data_malformed_header_is_401_not_500(pdp: MockPermitPDP, monkeypatch, value: str):
get_base = AsyncMock()
monkeypatch.setattr(pdp._opal.data_updater, "get_base_policy_data", get_base)
client = TestClient(pdp._app)

response = client.post("/update_policy_data", headers={"authorization": value}, follow_redirects=False)

assert response.status_code == 401
get_base.assert_not_awaited()


def test_legacy_routes_do_not_redirect(pdp: MockPermitPDP, auth: dict[str, str], monkeypatch):
# Lock in the fix: the aliases must call the updaters directly, never redirect
# to the canonical routes. Clients drop Authorization on redirects, and
Expand Down
32 changes: 25 additions & 7 deletions horizon/tests/test_opal_trigger_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
The TestClient is used WITHOUT a context manager, so the app lifespan never runs (no OPAL
policy/data fetch, no OPA process, no control-plane connection). ``raise_server_exceptions
=False`` means a request the auth dependency *allows* through but which then fails on real
offline I/O surfaces as a 500 response instead of raising - so an authenticated trigger
call asserts only that it is not blocked (status != 401), not that the handler succeeds.
offline I/O surfaces as a 500 response instead of raising. Valid-token tests boundary-mock
the real (unstarted) updater instances hanging off ``_sidecar._opal`` with an ``AsyncMock``,
so the handler runs to completion and the test asserts an actual 200, not just ``!= 401``.
"""

from unittest.mock import AsyncMock

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
Expand Down Expand Up @@ -57,11 +60,26 @@ def test_trigger_route_without_token_is_401(client: TestClient, path: str):
assert resp.json()["detail"] == "Missing Authorization header"


@pytest.mark.parametrize("path", TRIGGER_ROUTES)
def test_trigger_route_with_valid_token_is_not_blocked(client: TestClient, path: str):
# The dependency passes; the handler may 500 offline, but it must not be a 401.
resp = client.post(path, headers=_auth(VALID_TOKEN))
assert resp.status_code != status.HTTP_401_UNAUTHORIZED
def test_policy_updater_trigger_route_with_valid_token_returns_200(client: TestClient, monkeypatch):
trigger = AsyncMock()
monkeypatch.setattr(_sidecar._opal.policy_updater, "trigger_update_policy", trigger)

resp = client.post("/policy-updater/trigger", headers=_auth(VALID_TOKEN))

assert resp.status_code == status.HTTP_200_OK
assert resp.json() == {"status": "ok"}
trigger.assert_awaited_once_with(force_full_update=True)


def test_data_updater_trigger_route_with_valid_token_returns_200(client: TestClient, monkeypatch):
get_base = AsyncMock()
monkeypatch.setattr(_sidecar._opal.data_updater, "get_base_policy_data", get_base)

resp = client.post("/data-updater/trigger", headers=_auth(VALID_TOKEN))

assert resp.status_code == status.HTTP_200_OK
assert resp.json() == {"status": "ok"}
get_base.assert_awaited_once_with(data_fetch_reason="request from sdk")


@pytest.mark.parametrize("path", TRIGGER_ROUTES)
Expand Down
Loading