Operator-Auth full implementation. #70

Merged
serversdown merged 14 commits from feat/operator-auth into dev 2026-06-18 16:36:01 -04:00
4 changed files with 36 additions and 1 deletions
Showing only changes of commit 68161298a4 - Show all commits
+4
View File
@@ -185,6 +185,10 @@ async def operator_gate(request: Request, call_next):
if not OPERATOR_AUTH_ENABLED: if not OPERATOR_AUTH_ENABLED:
return await call_next(request) return await call_next(request)
# CORS preflight carries no auth and must reach CORSMiddleware, not the gate.
if request.method == "OPTIONS":
return await call_next(request)
path = request.url.path path = request.url.path
if _is_exempt(path): if _is_exempt(path):
return await call_next(request) return await call_next(request)
+13 -1
View File
@@ -12,9 +12,21 @@ from backend.operator_auth import (
require_role, create_operator, reset_operator_password, require_role, create_operator, reset_operator_password,
set_operator_active, set_operator_role, set_operator_active, set_operator_role,
) )
import backend.operator_auth as operator_auth
from backend.utils.timezone import format_local_datetime from backend.utils.timezone import format_local_datetime
router = APIRouter(tags=["operator-users"])
def _require_auth_enabled():
"""The operator-management surface does not exist while operator auth is
disabled — otherwise these net-new endpoints would be world-open with the
flag off (the default), letting anyone pre-seed a superadmin. Read the flag
as a live module attribute so the test monkeypatch and a runtime flip both
take effect."""
if not operator_auth.OPERATOR_AUTH_ENABLED:
raise HTTPException(status_code=404, detail="Not found")
router = APIRouter(tags=["operator-users"], dependencies=[Depends(_require_auth_enabled)])
_superadmin = require_role("superadmin") _superadmin = require_role("superadmin")
+7
View File
@@ -67,3 +67,10 @@ def test_must_change_user_on_api_gets_403_json_not_redirect(client, db_session,
r = client.get("/api/status-snapshot", follow_redirects=False) r = client.get("/api/status-snapshot", follow_redirects=False)
assert r.status_code == 403 assert r.status_code == 403
assert r.json()["detail"] == "Password change required" assert r.json()["detail"] == "Password change required"
def test_options_preflight_passes_through_gate(client, db_session, monkeypatch):
wire_operator_auth(monkeypatch, db_session, enabled=True)
# CORS preflight has no cookie; the gate must not 303/401 it.
r = client.options("/api/status-snapshot", follow_redirects=False)
assert r.status_code not in (303, 401)
+12
View File
@@ -118,3 +118,15 @@ def test_deferred_operator_role_rejected_by_api(client, db_session, monkeypatch)
_login_as(client, su) _login_as(client, su)
assert client.post("/api/admin/users", json={"email": "op@x.com", "name": "Op", "role": "operator"}).status_code == 400 assert client.post("/api/admin/users", json={"email": "op@x.com", "name": "Op", "role": "operator"}).status_code == 400
assert client.post(f"/api/admin/users/{target.id}/role", json={"role": "operator"}).status_code == 400 assert client.post(f"/api/admin/users/{target.id}/role", json={"role": "operator"}).status_code == 400
def test_admin_surface_404s_when_flag_off(client, db_session, monkeypatch):
su, _ = create_operator(db_session, "su@x.com", "Su", "superadmin", password="pw-123456")
wire_operator_auth(monkeypatch, db_session, enabled=False)
_login_as(client, su)
# With operator auth OFF, the management surface must not exist (404), even
# though require_role passes through — otherwise it'd be world-open.
assert client.get("/admin/users").status_code == 404
assert client.get("/api/admin/users").status_code == 404
assert client.post("/api/admin/users",
json={"email": "x@x.com", "name": "X", "role": "admin"}).status_code == 404