import uuid from datetime import datetime from sqlalchemy import ( JSON, Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from app.core.database import Base from app.core.sql_types import GUID as Uuid from app.core.sql_types import PreciseDateTime # AuthUser has been replaced by IdentityPerson in app.identity.person_models. # Import it here for backward compatibility so that existing code doing # ``from app.auth.models import AuthUser`` still works. from app.identity.person_models import IdentityPerson as AuthUser # noqa: F401 class AuthRole(Base): __tablename__ = "auth_roles" id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) name: Mapped[str] = mapped_column(String(32), unique=True, nullable=False) description: Mapped[str | None] = mapped_column(Text) is_system: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) class AuthUserRole(Base): __tablename__ = "auth_user_roles" user_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("identity_persons.id"), primary_key=True ) role_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("auth_roles.id"), primary_key=True ) granted_by: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("identity_persons.id") ) granted_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) # Provenance of the grant (2026-06-17). ``manual`` = explicitly # granted by an admin via the RBAC page / API / bootstrap (the # default for every pre-existing row and every existing code path). # ``derived`` = auto-granted by a reconciler from a structural fact # — currently only the department→role derivation (Finance BU ⇒ # ``finance``, HR ⇒ ``hr``; see ``app.identity.department_role_sync``). # The distinction lets the department-role reconciler revoke ONLY the # grants it created when a person transfers out of the department, # while never touching a manual grant an admin made on purpose. grant_source: Mapped[str] = mapped_column( String(16), nullable=False, server_default="manual" ) user: Mapped["AuthUser"] = relationship(back_populates="user_roles", foreign_keys=[user_id]) role: Mapped["AuthRole"] = relationship(lazy="joined") class AuthUserRoleCycle(Base): """One holding interval of one role by one person (append-only). ``auth_user_roles`` revokes by DELETING the row, so the live table can only answer "is X an admin now" — never "was X an admin on 2026-03-01", which is the SOC2 CC6.1 privileged-roster question. This ledger answers it in SQL: WHERE role_id = :r AND granted_at <= :d AND (revoked_at IS NULL OR revoked_at > :d) Written exclusively by ``app.core.access_cycle_listener`` on the same transaction as the grant/revoke it describes, so a rolled-back grant leaves no cycle behind. Every revoke path must therefore delete the ``AuthUserRole`` through the ORM — Core-level ``delete(AuthUserRole)`` bypasses the listener and is blocked by a guard test (``tests/test_access_cycle_ledger_guard.py``). No FK columns — see migration 484's docstring. """ __tablename__ = "auth_user_role_cycles" __mapper_args__ = {"eager_defaults": True} id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False) role_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False) role_name: Mapped[str] = mapped_column(String(32), nullable=False) # Microsecond precision — see ``PreciseDateTime``. A revoke and an # immediate re-grant land in the same second, and the two intervals # must still sort deterministically for a reproducible evidence pack. granted_at: Mapped[datetime] = mapped_column(PreciseDateTime, nullable=False) granted_by_person_id: Mapped[uuid.UUID | None] = mapped_column(Uuid) grant_source: Mapped[str] = mapped_column( String(16), nullable=False, server_default="manual", ) revoked_at: Mapped[datetime | None] = mapped_column(PreciseDateTime) revoked_by_person_id: Mapped[uuid.UUID | None] = mapped_column(Uuid) open_trace_id: Mapped[uuid.UUID | None] = mapped_column(Uuid) close_trace_id: Mapped[uuid.UUID | None] = mapped_column(Uuid) detail: Mapped[dict | None] = mapped_column(JSON) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), ) class AuthPermission(Base): __tablename__ = "auth_permissions" __table_args__ = (UniqueConstraint("resource", "action"),) id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) resource: Mapped[str] = mapped_column(String(64), nullable=False) action: Mapped[str] = mapped_column(String(32), nullable=False) class AuthRolePermission(Base): __tablename__ = "auth_role_permissions" role_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("auth_roles.id"), primary_key=True ) permission_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("auth_permissions.id"), primary_key=True ) class AuthEntitlement(Base): __tablename__ = "auth_entitlements" id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("identity_persons.id"), nullable=False, index=True ) resource_type: Mapped[str] = mapped_column(String(64), nullable=False) resource_id: Mapped[str] = mapped_column(String(128), nullable=False) privilege: Mapped[str] = mapped_column(String(128), nullable=False) source_type: Mapped[str] = mapped_column(String(32), nullable=False) source_ref: Mapped[str | None] = mapped_column(String(128)) status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") granted_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))