from __future__ import annotations import copy import hashlib import importlib.util import uuid from datetime import UTC, datetime, timedelta from pathlib import Path import pytest from alembic.migration import MigrationContext from alembic.operations import Operations from fastapi import HTTPException from fastapi.testclient import TestClient from pydantic import SecretStr, ValidationError from sqlalchemy import create_engine, delete, event, select, update from sqlalchemy.exc import OperationalError from sqlalchemy.orm import Session from sqlalchemy.pool import StaticPool from modelforge_api.api.authorization import Principal, PrincipalRole, authenticate_operator_token from modelforge_api.db import get_session from modelforge_api.domain.managed_identity import ( ManagedIdentityChange, ManagedIdentityCreate, ManagedIdentityRevoke, ManagedIdentityRotate, ) from modelforge_api.main import app from modelforge_api.persistence.models import ( AuditEvent, Base, ManagedOperatorCredential, ManagedOperatorCredentialActivation, ManagedOperatorIdentity, ManagedOperatorState, ) from modelforge_api.services import audit as audit_module from modelforge_api.services.operator_identities import ( ManagedIdentityError, ManagedIdentityService, authenticate_managed_operator, ) from modelforge_api.services.recovery_fingerprint import ( CURRENT_TRUTH_TABLES, MANAGED_ACTIVATION_TABLE, MANAGED_IDENTITY_SCHEMA, SECRET_SCHEMA, _without_creative, _without_lab_execution, _without_managed_identities, _without_model_changes, _without_model_updates, _without_secrets, _without_selected_comparisons, audit_fingerprint_differences_are_compatible, fingerprint_compatibility, fingerprint_session, ) from modelforge_api.settings import Settings, get_settings ANCHOR = Principal("legacy-operator", PrincipalRole.ADMIN, "legacy_admin_token") REASON = "Reviewed operator identity lifecycle" @pytest.fixture def session(): engine = create_engine( "sqlite://", poolclass=StaticPool, connect_args={"check_same_thread": False} ) Base.metadata.create_all(engine) with Session(engine) as session: yield session engine.dispose() def service(session: Session, principal: Principal = ANCHOR) -> ManagedIdentityService: return ManagedIdentityService(session, principal, "managed-request-42") def create(session: Session, slug: str = "alice", role: str = "admin"): return service(session).create( ManagedIdentityCreate( slug=slug, role=role, expires_in_days=30, expected_revision=service(session).list().revision, reason=REASON, ) ) def test_issue_metadata_auth_and_audit(session: Session) -> None: issued = create(session) assert issued.one_time_key.startswith("mfopm_") and len(issued.one_time_key) == 49 assert issued.revision == 1 and issued.identity.subject == "managed:alice" principal = authenticate_managed_operator(session, issued.one_time_key) session.commit() assert principal.subject == "managed:alice" and principal.role == PrincipalRole.ADMIN metadata = service(session).list().model_dump_json() digest = hashlib.sha256(issued.one_time_key.encode()).hexdigest() assert ( issued.one_time_key not in metadata and digest not in metadata and "token_sha256" not in metadata ) assert service(session).list().identities[0].credentials[0].last_used_at is not None audit = session.scalar( select(AuditEvent).where(AuditEvent.action == "managed_identity.created") ) assert audit is not None and audit.actor_id == "legacy-operator" assert audit.correlation_id == "managed-request-42" and issued.one_time_key not in str( audit.details ) def test_credential_metadata_preserves_utc_after_database_reload(session: Session) -> None: create(session) instant = datetime(2026, 9, 8, 10, 30, tzinfo=UTC) session.execute(update(ManagedOperatorCredential).values( revoked_at=instant, superseded_at=instant, last_used_at=instant, )) session.commit() session.expire_all() credential = service(session).list().identities[0].credentials[0] assert credential.revoked_at == instant assert credential.superseded_at == instant assert credential.last_used_at == instant @pytest.mark.parametrize("schema", ["20260908_0026", MANAGED_IDENTITY_SCHEMA]) def test_recovery_allows_one_marker_but_preserves_history(session: Session, schema: str) -> None: create(session) before = _without_lab_execution(_without_model_changes( _without_secrets( _without_model_updates( _without_selected_comparisons(_without_creative(fingerprint_session(session))) ) ) )) audit_module.AuditWriter(session, "operator", "recovery").write( "RECOVERY_RECONCILIATION_COMPLETED", "restore", None, {"restored": True}, ) session.commit() after = _without_lab_execution(_without_model_changes( _without_secrets( _without_model_updates( _without_selected_comparisons(_without_creative(fingerprint_session(session))) ) ) )) if schema == "20260908_0026": before, after = _without_managed_identities(before), _without_managed_identities(after) contract = fingerprint_compatibility(before, after, source_schema_revision=schema) assert audit_fingerprint_differences_are_compatible(before, after, contract) for table in before["tables"]: altered = copy.deepcopy(after) if table == "audit_events": altered["tables"][table]["row_count"] += 1 elif table == "audit_chain_heads": altered["tables"][table]["row_count"] = 0 elif table == "operational_alerts": altered["tables"][table]["row_count"] += 1 else: altered["tables"][table]["digest"] = "0" * 64 assert not audit_fingerprint_differences_are_compatible(before, altered, contract), table @pytest.mark.parametrize("operation", ["revoke", "disable", "demote"]) def test_last_admin_is_preserved_atomically(session: Session, operation: str) -> None: issued = create(session) before = service(session).list().model_dump_json() events = list(session.scalars(select(AuditEvent.id))) with pytest.raises(ManagedIdentityError) as refused: if operation == "revoke": service(session).revoke( "alice", ManagedIdentityRevoke( expected_version=1, confirm_subject="managed:alice", reason=REASON ), ) else: service(session).change( "alice", ManagedIdentityChange( expected_version=1, role="viewer" if operation == "demote" else "admin", enabled=operation != "disable", reason=REASON, ), ) assert refused.value.code == "LAST_MANAGED_ADMIN" assert service(session).list().model_dump_json() == before assert list(session.scalars(select(AuditEvent.id))) == events assert authenticate_managed_operator(session, issued.one_time_key).role == PrincipalRole.ADMIN def test_initial_admin_and_revision_conflicts(session: Session) -> None: with pytest.raises(ManagedIdentityError, match="first managed"): create(session, role="viewer") assert service(session).list().revision == 0 create(session) with pytest.raises(ManagedIdentityError) as conflict: service(session).create( ManagedIdentityCreate( slug="bob", role="viewer", expires_in_days=1, expected_revision=0, reason=REASON ) ) assert conflict.value.code == "MANAGED_IDENTITY_VERSION_CONFLICT" assert session.get(ManagedOperatorIdentity, "bob") is None def test_bounded_overlap_revoke_and_no_fallback(session: Session) -> None: first = create(session) rotated = service(session).rotate( "alice", ManagedIdentityRotate( expected_version=1, expires_in_days=30, overlap_seconds=900, reason=REASON ), ) assert len(rotated.identity.credentials) == 2 assert authenticate_managed_operator(session, first.one_time_key).subject == "managed:alice" latest = service(session).rotate( "alice", ManagedIdentityRotate( expected_version=2, expires_in_days=30, overlap_seconds=0, reason=REASON ), ) for token in (first.one_time_key, rotated.one_time_key): with pytest.raises(ManagedIdentityError): authenticate_managed_operator(session, token) assert authenticate_managed_operator(session, latest.one_time_key) with pytest.raises(HTTPException): authenticate_operator_token( Settings(_env_file=None, operator_api_key=SecretStr(latest.one_time_key)), latest.one_time_key, ) def test_initiator_revoked_after_authentication_is_rechecked(session: Session) -> None: alice = create(session) create(session, "bob") stale = authenticate_managed_operator(session, alice.one_time_key) session.commit() service(session).revoke( "alice", ManagedIdentityRevoke(expected_version=1, confirm_subject="managed:alice", reason=REASON), ) with pytest.raises(ManagedIdentityError) as denied: service(session, stale).change( "bob", ManagedIdentityChange(expected_version=1, role="admin", enabled=True, reason=REASON), ) assert denied.value.code == "MANAGED_ADMIN_NO_LONGER_AUTHORIZED" assert session.get(ManagedOperatorIdentity, "bob").version == 1 def test_recovery_activation_quarantine_preserves_authoritative_history(session: Session) -> None: original = create(session) before = _without_lab_execution(_without_model_changes( _without_model_updates( _without_selected_comparisons(_without_creative(fingerprint_session(session))) ) )) assert MANAGED_ACTIVATION_TABLE in CURRENT_TRUTH_TABLES session.execute(delete(ManagedOperatorCredentialActivation)) session.commit() after = _without_lab_execution(_without_model_changes( _without_model_updates( _without_selected_comparisons(_without_creative(fingerprint_session(session))) ) )) contract = fingerprint_compatibility( before, after, source_schema_revision=SECRET_SCHEMA ) assert audit_fingerprint_differences_are_compatible(before, after, contract) with pytest.raises(ManagedIdentityError): authenticate_managed_operator(session, original.one_time_key) metadata = service(session).list().identities[0].credentials[0] assert not metadata.activated and not metadata.usable recovered = service(session).rotate( "alice", ManagedIdentityRotate( expected_version=1, expires_in_days=30, overlap_seconds=900, reason=REASON ), ) assert authenticate_managed_operator(session, recovered.one_time_key) with pytest.raises(ManagedIdentityError): authenticate_managed_operator(session, original.one_time_key) def test_audit_failure_rolls_back_key_and_revision( session: Session, monkeypatch: pytest.MonkeyPatch ) -> None: def failed(*args, **kwargs): raise RuntimeError("audit unavailable") monkeypatch.setattr(audit_module, "_append_canonical_audit_event", failed) with pytest.raises(RuntimeError): create(session) assert service(session).list().revision == 0 assert session.scalar(select(ManagedOperatorCredential)) is None def test_metadata_failure_preserves_previous_key_and_audit( session: Session, monkeypatch: pytest.MonkeyPatch ) -> None: issued = create(session) before = list(session.scalars(select(AuditEvent.id))) def unavailable(*args, **kwargs): raise RuntimeError("metadata unavailable") subject = service(session) monkeypatch.setattr(subject, "_metadata", unavailable) with pytest.raises(RuntimeError, match="metadata unavailable"): subject.rotate( "alice", ManagedIdentityRotate( expected_version=1, expires_in_days=30, overlap_seconds=0, reason=REASON ), ) assert list(session.scalars(select(AuditEvent.id))) == before assert service(session).list().revision == 1 assert authenticate_managed_operator(session, issued.one_time_key) def test_issuance_returns_snapshot_without_reads_after_commit(session: Session) -> None: committed = False def mark_commit(session): nonlocal committed committed = True def reject_late_read(orm_execute_state): if committed: raise AssertionError("database read after commit") event.listen(session, "after_commit", mark_commit) event.listen(session, "do_orm_execute", reject_late_read) try: issued = create(session) assert issued.revision == 1 and issued.identity.credentials[0].usable assert "one_time_key" in issued.model_dump_json() finally: event.remove(session, "after_commit", mark_commit) event.remove(session, "do_orm_execute", reject_late_read) @pytest.mark.parametrize("alteration", ["expiry", "disabled", "revoked", "quarantined"]) def test_unusable_credential_rejected(session: Session, alteration: str) -> None: issued = create(session) credential_id = uuid.UUID(issued.identity.credentials[0].id) if alteration == "expiry": session.execute( update(ManagedOperatorCredential) .where(ManagedOperatorCredential.id == credential_id) .values(expires_at=datetime.now(UTC) - timedelta(seconds=1)) ) elif alteration == "revoked": session.execute( update(ManagedOperatorCredential) .where(ManagedOperatorCredential.id == credential_id) .values(revoked_at=datetime.now(UTC)) ) elif alteration == "disabled": session.execute(update(ManagedOperatorIdentity).values(enabled=False)) else: session.execute(delete(ManagedOperatorCredentialActivation)) session.commit() with pytest.raises(ManagedIdentityError) as error: authenticate_managed_operator(session, issued.one_time_key) assert error.value.status_code == 401 assert not service(session).list().identities[0].credentials[0].usable def test_non_admin_management_and_exact_revoke_confirmation(session: Session) -> None: create(session) viewer = create(session, "viewer", "viewer") principal = authenticate_managed_operator(session, viewer.one_time_key) session.commit() with pytest.raises(ManagedIdentityError) as denied: service(session, principal).list() assert denied.value.status_code == 403 with pytest.raises(ManagedIdentityError) as denied: service(session).revoke( "viewer", ManagedIdentityRevoke(expected_version=1, confirm_subject="viewer", reason=REASON), ) assert denied.value.code == "MANAGED_IDENTITY_CONFIRMATION_REQUIRED" result = service(session).revoke( "viewer", ManagedIdentityRevoke(expected_version=1, confirm_subject="managed:viewer", reason=REASON), ) assert result.identity.version == 2 and not result.identity.credentials[0].usable def test_metadata_history_is_bounded(session: Session) -> None: create(session) for version in range(1, 12): service(session).rotate( "alice", ManagedIdentityRotate( expected_version=version, expires_in_days=1, overlap_seconds=0, reason=REASON ), ) assert len(service(session).list().identities[0].credentials) == 10 def test_migration_empty_downgrade_and_history_refusal() -> None: path = ( Path(__file__).resolve().parents[1] / "alembic/versions/20260908_0027_managed_operator_identities.py" ) specification = importlib.util.spec_from_file_location("managed_identity_migration", path) assert specification is not None and specification.loader is not None migration = importlib.util.module_from_spec(specification) specification.loader.exec_module(migration) engine = create_engine("sqlite://") with engine.begin() as connection, Operations.context(MigrationContext.configure(connection)): migration.upgrade() assert connection.scalar(select(ManagedOperatorState.revision)) == 0 migration.downgrade() migration.upgrade() connection.execute( ManagedOperatorIdentity.__table__.insert().values( slug="retained", role="admin", enabled=True, version=1, created_at=datetime.now(UTC), ) ) with pytest.raises(RuntimeError, match="history exists"): migration.downgrade() engine.dispose() @pytest.mark.parametrize( "field,value", [ ("expires_in_days", True), ("expires_in_days", "30"), ("expires_in_days", 366), ("expected_revision", False), ("reason", " "), ("reason", "copied mfopm_" + "a" * 43), ("slug", "managed:alice"), ], ) def test_strict_inputs_and_secret_safe_reason(field: str, value: object) -> None: body = {"slug": "alice", "role": "admin", "expires_in_days": 30, "expected_revision": 0, "reason": REASON} body[field] = value with pytest.raises(ValidationError): ManagedIdentityCreate.model_validate(body) def test_http_boundaries_no_store_and_dependency_failure( session: Session, monkeypatch: pytest.MonkeyPatch ) -> None: settings = Settings(_env_file=None, env="test", operator_api_key="anchor-secret") app.dependency_overrides[get_settings] = lambda: settings app.dependency_overrides[get_session] = lambda: session try: client = TestClient(app) url = "/api/v1/operator/managed-identities" headers = { "X-ModelForge-Admin-Token": "anchor-secret", "X-Correlation-ID": "http-managed-42", } assert client.post(url, content="invalid").status_code == 401 response = client.post( url, json={ "slug": "alice", "role": "admin", "expires_in_days": 30, "expected_revision": 0, "reason": REASON }, headers=headers, ) assert response.status_code == 201, response.text token = response.json()["one_time_key"] assert ( response.headers["cache-control"] == "no-store" and "set-cookie" not in response.headers ) own = {"X-ModelForge-Admin-Token": token} identity = client.get("/api/v1/operator/identity", headers=own) assert identity.json()["subject"] == "managed:alice" assert token not in client.get(url, headers=own).text failure = client.post( url, json={ "slug": "bob", "role": "admin", "expires_in_days": 30, "expected_revision": 1, "reason": "copied " + token, }, headers=own, ) assert failure.status_code == 422 and token not in failure.text def unavailable(*args, **kwargs): raise OperationalError("hidden", {}, Exception("private database detail")) monkeypatch.setattr("modelforge_api.main.authenticate_managed_operator", unavailable) response = client.post(url, content="not JSON", headers=own) assert response.status_code == 503 assert "private database detail" not in response.text assert client.get("/api/v1/operator/identity", headers=headers).status_code == 200 finally: app.dependency_overrides.clear()