Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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