from __future__ import annotations import os import pytest from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine from backend.app.services import user_access _REAL_CURRENT_USER = user_access.current_user _PROVISION_PATH = "/api/apps/authentik/forge/controller-reader/provision" @pytest.fixture def forge_reader_operator_client(tmp_path, monkeypatch: pytest.MonkeyPatch): from backend.app.core.config import settings from backend.app.db import session as db_session from backend.app.db.init_db import init_db from backend.app.main import app if os.name == "nt": from backend.app.services import forge_oidc_provisioning monkeypatch.setattr( forge_oidc_provisioning, "_windows_private_path", lambda *_args, **_kwargs: None, ) else: from backend.app.services import forge_oidc_provisioning monkeypatch.setattr( forge_oidc_provisioning, "_FORGE_OIDC_LOCK_ROOT", tmp_path / "forge-reader-api-lock", ) engine = create_engine( f"sqlite:///{tmp_path / 'forge-reader-operator.db'}", connect_args={"check_same_thread": False}, ) monkeypatch.setattr(db_session, "engine", engine) monkeypatch.setenv("APPOPS_REQUIRE_OPERATOR_AUTH", "false") monkeypatch.setattr(settings, "operator_proxy_auth_enabled", False) monkeypatch.setattr(settings, "auth_admin_subjects", ["authentik:owner"], raising=False) monkeypatch.setattr(settings, "auth_admin_emails", [], raising=False) monkeypatch.setattr(settings, "data_dir", str(tmp_path)) monkeypatch.setattr(user_access, "current_user", _REAL_CURRENT_USER) SQLModel.metadata.create_all(engine) init_db() client = TestClient(app, client=("198.18.10.20", 42000)) yield client engine.dispose() def _spy_actual_connector_loader(monkeypatch: pytest.MonkeyPatch): from backend.app.services import setup_service original = setup_service.apply_stored_connector_settings calls = [] def tracked(session, connector_ids=None): calls.append(connector_ids) return original(session, connector_ids) monkeypatch.setattr(setup_service, "apply_stored_connector_settings", tracked) return calls def _sign_in(client: TestClient, subject: str) -> None: from backend.app.db import session as db_session from backend.app.services.user_access import issue_session, provision_user with Session(db_session.engine) as session: user = provision_user( session, "authentik", "https://auth.example/application/o/appops/", {"sub": subject, "email": f"{subject}@example.com"}, ) cookie = issue_session(session, user) client.cookies.set("appops_user_session", cookie) def test_forge_reader_route_requires_authenticated_operator_and_csrf( forge_reader_operator_client: TestClient, monkeypatch: pytest.MonkeyPatch, ): loader_calls = _spy_actual_connector_loader(monkeypatch) unauthenticated = forge_reader_operator_client.post(_PROVISION_PATH, json={}) assert unauthenticated.status_code == 401 _sign_in(forge_reader_operator_client, "owner") missing_csrf = forge_reader_operator_client.post(_PROVISION_PATH, json={}) assert missing_csrf.status_code == 403 assert loader_calls == [] def test_forge_reader_route_rejects_read_only_user_and_caller_selectors( forge_reader_operator_client: TestClient, monkeypatch: pytest.MonkeyPatch, ): loader_calls = _spy_actual_connector_loader(monkeypatch) _sign_in(forge_reader_operator_client, "reader") denied = forge_reader_operator_client.post( _PROVISION_PATH, json={}, headers={"X-AppOps-Session-CSRF": "1"}, ) assert denied.status_code == 403 _sign_in(forge_reader_operator_client, "owner") invalid = forge_reader_operator_client.post( _PROVISION_PATH, json={"token": "caller-selected"}, headers={"X-AppOps-Session-CSRF": "1"}, ) assert invalid.status_code == 422 assert loader_calls == [] def test_forge_reader_default_denied_policy_does_not_load_connector_or_mutate( forge_reader_operator_client: TestClient, monkeypatch: pytest.MonkeyPatch, ): from backend.app.db import session as db_session from backend.app.db.models import Policy loader_calls = _spy_actual_connector_loader(monkeypatch) _sign_in(forge_reader_operator_client, "owner") with Session(db_session.engine) as session: policy = session.get(Policy, "policy-forge-oidc-provision") policy.manual = "blocked" session.add(policy) session.commit() response = forge_reader_operator_client.post( _PROVISION_PATH, json={}, headers={"X-AppOps-Session-CSRF": "1"}, ) assert response.status_code == 200 assert response.json()["status"] == "blocked" assert response.json()["message"] == "forge_reader_exact_target_policy_required" assert loader_calls == [] def test_forge_reader_route_passes_operator_and_projects_only_safe_result( forge_reader_operator_client: TestClient, monkeypatch: pytest.MonkeyPatch, ): from backend.app.api import routes_authentik _sign_in(forge_reader_operator_client, "owner") loader_calls = _spy_actual_connector_loader(monkeypatch) calls = [] held_token = "reader-token-must-never-be-returned" def provision(session, *, actor_identity): calls.append((session, actor_identity)) return { "success": True, "status": "ready", "message": "forge_reader_ready", "ready": True, "secretRef": "ITWORX_FORGE_AUTHENTIK_READ_TOKEN", } monkeypatch.setattr(routes_authentik, "provision_forge_controller_reader", provision) response = forge_reader_operator_client.post( _PROVISION_PATH, json={}, headers={"X-AppOps-Session-CSRF": "1"}, ) assert response.status_code == 200 assert response.json() == { "success": True, "status": "ready", "message": "forge_reader_ready", "ready": True, "secretRef": "ITWORX_FORGE_AUTHENTIK_READ_TOKEN", } assert calls and calls[0][1] == "authentik:owner" assert loader_calls == [] assert held_token not in response.text