from __future__ import annotations import uuid from datetime import datetime from typing import Any from sqlalchemy import Boolean, DateTime, ForeignKey, Index, String, Text, UniqueConstraint, JSON, text from sqlalchemy.orm import Mapped, mapped_column, relationship from govoplan_access.backend.db.base import AccessBase, TimestampMixin from govoplan_core.tenancy.scope import Tenant def new_uuid() -> str: return str(uuid.uuid4()) class Account(AccessBase, TimestampMixin): """Global login identity shared by one or more tenant memberships.""" __tablename__ = "access_accounts" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) email: Mapped[str] = mapped_column(String(320), nullable=False) normalized_email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False, index=True) display_name: Mapped[str | None] = mapped_column(String(255)) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) auth_provider: Mapped[str] = mapped_column(String(50), default="local", nullable=False) password_hash: Mapped[str | None] = mapped_column(String(500)) password_reset_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) memberships: Mapped[list[User]] = relationship(back_populates="account") identity_links: Mapped[list[IdentityAccountLink]] = relationship( back_populates="account", cascade="all, delete-orphan" ) auth_sessions: Mapped[list[AuthSession]] = relationship(back_populates="account", cascade="all, delete-orphan") system_role_assignments: Mapped[list[SystemRoleAssignment]] = relationship( back_populates="account", cascade="all, delete-orphan" ) class Identity(AccessBase, TimestampMixin): __tablename__ = "access_identities" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) display_name: Mapped[str | None] = mapped_column(String(255)) external_subject: Mapped[str | None] = mapped_column(String(255), index=True) source: Mapped[str] = mapped_column(String(50), default="local", nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) account_links: Mapped[list[IdentityAccountLink]] = relationship( back_populates="identity", cascade="all, delete-orphan" ) class IdentityAccountLink(AccessBase, TimestampMixin): __tablename__ = "access_identity_account_links" __table_args__ = ( UniqueConstraint("identity_id", "account_id", name="uq_identity_account_links_identity_account"), Index( "uq_identity_account_links_primary_account", "account_id", unique=True, sqlite_where=text("is_primary = 1"), postgresql_where=text("is_primary IS TRUE"), ), Index( "uq_identity_account_links_primary_identity", "identity_id", unique=True, sqlite_where=text("is_primary = 1"), postgresql_where=text("is_primary IS TRUE"), ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) identity_id: Mapped[str] = mapped_column(ForeignKey("access_identities.id", ondelete="CASCADE"), nullable=False, index=True) account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True) is_primary: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) source: Mapped[str] = mapped_column(String(50), default="local", nullable=False) identity: Mapped[Identity] = relationship(back_populates="account_links") account: Mapped[Account] = relationship(back_populates="identity_links") class User(AccessBase, TimestampMixin): __tablename__ = "access_users" __table_args__ = ( UniqueConstraint("tenant_id", "email", name="uq_users_tenant_email"), UniqueConstraint("tenant_id", "account_id", name="uq_users_tenant_account"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True) email: Mapped[str] = mapped_column(String(320), nullable=False, index=True) display_name: Mapped[str | None] = mapped_column(String(255)) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) is_tenant_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) auth_provider: Mapped[str] = mapped_column(String(50), default="local", nullable=False) password_hash: Mapped[str | None] = mapped_column(String(500)) last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) mail_profile_policy: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) account: Mapped[Account] = relationship(back_populates="memberships") api_keys: Mapped[list[ApiKey]] = relationship(back_populates="user", cascade="all, delete-orphan") auth_sessions: Mapped[list[AuthSession]] = relationship(back_populates="user", cascade="all, delete-orphan") class Group(AccessBase, TimestampMixin): __tablename__ = "access_groups" __table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_groups_tenant_slug"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) slug: Mapped[str] = mapped_column(String(100), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) system_template_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True) system_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) mail_profile_policy: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) class Role(AccessBase, TimestampMixin): __tablename__ = "access_roles" __table_args__ = ( UniqueConstraint("tenant_id", "slug", name="uq_roles_tenant_slug"), Index( "uq_roles_system_slug", "slug", unique=True, sqlite_where=text("tenant_id IS NULL"), postgresql_where=text("tenant_id IS NULL"), ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True) slug: Mapped[str] = mapped_column(String(100), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text) permissions: Mapped[list[str]] = mapped_column(JSON, default=list) is_builtin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) is_assignable: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) system_template_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True) system_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) class OrganizationUnit(AccessBase, TimestampMixin): __tablename__ = "access_organization_units" __table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_organization_units_tenant_slug"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) parent_id: Mapped[str | None] = mapped_column(ForeignKey("access_organization_units.id", ondelete="SET NULL"), nullable=True, index=True) slug: Mapped[str] = mapped_column(String(100), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) class Function(AccessBase, TimestampMixin): __tablename__ = "access_functions" __table_args__ = (UniqueConstraint("tenant_id", "organization_unit_id", "slug", name="uq_functions_tenant_ou_slug"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) organization_unit_id: Mapped[str] = mapped_column(ForeignKey("access_organization_units.id", ondelete="CASCADE"), nullable=False, index=True) slug: Mapped[str] = mapped_column(String(100), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text) delegable: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) act_in_place_allowed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) class FunctionRoleAssignment(AccessBase, TimestampMixin): __tablename__ = "access_function_role_assignments" __table_args__ = (UniqueConstraint("tenant_id", "function_id", "role_id", name="uq_function_role_assignments"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) function_id: Mapped[str] = mapped_column(ForeignKey("access_functions.id", ondelete="CASCADE"), nullable=False, index=True) role_id: Mapped[str] = mapped_column(ForeignKey("access_roles.id", ondelete="CASCADE"), nullable=False, index=True) class FunctionAssignment(AccessBase, TimestampMixin): __tablename__ = "access_function_assignments" __table_args__ = ( UniqueConstraint( "tenant_id", "account_id", "function_id", "organization_unit_id", name="uq_function_assignments_account_scope", ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True) identity_id: Mapped[str | None] = mapped_column(ForeignKey("access_identities.id", ondelete="SET NULL"), nullable=True, index=True) function_id: Mapped[str] = mapped_column(ForeignKey("access_functions.id", ondelete="CASCADE"), nullable=False, index=True) organization_unit_id: Mapped[str] = mapped_column(ForeignKey("access_organization_units.id", ondelete="CASCADE"), nullable=False, index=True) applies_to_subunits: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) source: Mapped[str] = mapped_column(String(50), default="direct", nullable=False) delegated_from_assignment_id: Mapped[str | None] = mapped_column(ForeignKey("access_function_assignments.id", ondelete="SET NULL"), nullable=True, index=True) acting_for_account_id: Mapped[str | None] = mapped_column(ForeignKey("access_accounts.id", ondelete="SET NULL"), nullable=True, index=True) valid_from: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) valid_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) class FunctionDelegation(AccessBase, TimestampMixin): __tablename__ = "access_function_delegations" __table_args__ = ( UniqueConstraint( "tenant_id", "function_assignment_id", "delegate_account_id", "mode", name="uq_function_delegations_assignment_delegate_mode", ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) function_assignment_id: Mapped[str] = mapped_column(ForeignKey("access_function_assignments.id", ondelete="CASCADE"), nullable=False, index=True) delegator_account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True) delegate_account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True) mode: Mapped[str] = mapped_column(String(30), default="delegate", nullable=False) reason: Mapped[str | None] = mapped_column(Text) valid_from: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) valid_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) class SystemRoleAssignment(AccessBase, TimestampMixin): __tablename__ = "access_system_role_assignments" __table_args__ = (UniqueConstraint("account_id", "role_id", name="uq_system_role_assignments"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True) role_id: Mapped[str] = mapped_column(ForeignKey("access_roles.id", ondelete="CASCADE"), nullable=False, index=True) account: Mapped[Account] = relationship(back_populates="system_role_assignments") role: Mapped[Role] = relationship() class UserGroupMembership(AccessBase, TimestampMixin): __tablename__ = "access_user_group_memberships" __table_args__ = (UniqueConstraint("tenant_id", "user_id", "group_id", name="uq_user_group_memberships"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) user_id: Mapped[str] = mapped_column(ForeignKey("access_users.id", ondelete="CASCADE"), nullable=False, index=True) group_id: Mapped[str] = mapped_column(ForeignKey("access_groups.id", ondelete="CASCADE"), nullable=False, index=True) class UserRoleAssignment(AccessBase, TimestampMixin): __tablename__ = "access_user_role_assignments" __table_args__ = (UniqueConstraint("tenant_id", "user_id", "role_id", name="uq_user_role_assignments"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) user_id: Mapped[str] = mapped_column(ForeignKey("access_users.id", ondelete="CASCADE"), nullable=False, index=True) role_id: Mapped[str] = mapped_column(ForeignKey("access_roles.id", ondelete="CASCADE"), nullable=False, index=True) class GroupRoleAssignment(AccessBase, TimestampMixin): __tablename__ = "access_group_role_assignments" __table_args__ = (UniqueConstraint("tenant_id", "group_id", "role_id", name="uq_group_role_assignments"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) group_id: Mapped[str] = mapped_column(ForeignKey("access_groups.id", ondelete="CASCADE"), nullable=False, index=True) role_id: Mapped[str] = mapped_column(ForeignKey("access_roles.id", ondelete="CASCADE"), nullable=False, index=True) class ApiKey(AccessBase, TimestampMixin): __tablename__ = "access_api_keys" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) user_id: Mapped[str] = mapped_column(ForeignKey("access_users.id", ondelete="CASCADE"), nullable=False, index=True) name: Mapped[str] = mapped_column(String(255), nullable=False) prefix: Mapped[str] = mapped_column(String(16), nullable=False, index=True) key_hash: Mapped[str] = mapped_column(String(128), nullable=False) scopes: Mapped[list[str]] = mapped_column(JSON, default=list) expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) user: Mapped[User] = relationship(back_populates="api_keys") class AuthSession(AccessBase, TimestampMixin): __tablename__ = "access_auth_sessions" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) user_id: Mapped[str] = mapped_column(ForeignKey("access_users.id", ondelete="CASCADE"), nullable=False, index=True) account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True) token_hash: Mapped[str] = mapped_column(String(128), nullable=False, unique=True, index=True) csrf_token_hash: Mapped[str | None] = mapped_column(String(128), nullable=True) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) last_seen_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True) user_agent: Mapped[str | None] = mapped_column(String(500)) ip_address: Mapped[str | None] = mapped_column(String(100)) user: Mapped[User] = relationship(back_populates="auth_sessions") account: Mapped[Account] = relationship(back_populates="auth_sessions") __all__ = [ "Account", "ApiKey", "AuthSession", "Function", "FunctionAssignment", "FunctionDelegation", "FunctionRoleAssignment", "Group", "GroupRoleAssignment", "Identity", "IdentityAccountLink", "OrganizationUnit", "Role", "SystemRoleAssignment", "Tenant", "User", "UserGroupMembership", "UserRoleAssignment", "new_uuid", ]