Source code for litestar_permissions.models

from __future__ import annotations

from datetime import datetime
from uuid import uuid4

from sqlalchemy import DateTime, ForeignKey, String, Text, UniqueConstraint, Uuid
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship


# Sentinel classes for re-export from __init__.py
[docs] class Role: """Named role that groups permissions. Use create_models() to get concrete ORM classes."""
[docs] class Permission: """A granular action like 'application:deploy'. Use create_models() to get concrete ORM classes."""
[docs] class RolePermission: """Many-to-many: which permissions belong to which role. Use create_models() to get concrete ORM classes."""
[docs] class UserRoleAssignment: """Assigns a role to a user, optionally scoped to a resource. Use create_models() to get concrete ORM classes."""
[docs] def create_models(base: type[DeclarativeBase], table_prefix: str = "", class_prefix: str = "") -> dict[str, type]: """Factory that creates concrete RBAC models bound to the app's Base. Args: base: The SQLAlchemy declarative base to bind models to. table_prefix: Prefix for database table names. class_prefix: Cosmetic prefix for ORM class ``__name__`` attributes, affecting ``repr()`` and logging output. For actual registry conflict avoidance, remove the conflicting model or use ``PermissionsPlugin(models=...)`` to inject pre-created models. Returns dict with keys: 'Role', 'Permission', 'RolePermission', 'UserRoleAssignment' """ # Lightweight base sharing registry + metadata but without UUIDPrimaryKey/SentinelMixin. # Used for join tables that need a composite PK, not an auto-generated id column. class _JoinBase(DeclarativeBase): __abstract__ = True registry = base.registry metadata = base.metadata class Permission(base): """A granular action like 'application:deploy' or 'config:write'.""" __tablename__ = f"{table_prefix}permissions" id: Mapped[str] = mapped_column(Uuid(), primary_key=True, default=uuid4) codename: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) description: Mapped[str | None] = mapped_column(Text(), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, nullable=False) role_permissions: Mapped[list[RolePermission]] = relationship(back_populates="permission") def __repr__(self) -> str: return f"<Permission {self.codename}>" class Role(base): """Named set of permissions, e.g. 'org-admin', 'project-viewer'.""" __tablename__ = f"{table_prefix}roles" id: Mapped[str] = mapped_column(Uuid(), primary_key=True, default=uuid4) name: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) description: Mapped[str | None] = mapped_column(Text(), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, nullable=False) role_permissions: Mapped[list[RolePermission]] = relationship( back_populates="role", cascade="all, delete-orphan" ) assignments: Mapped[list[UserRoleAssignment]] = relationship( back_populates="role", cascade="all, delete-orphan" ) def __repr__(self) -> str: return f"<Role {self.name}>" class RolePermission(_JoinBase): """Many-to-many: which permissions belong to which role.""" __tablename__ = f"{table_prefix}role_permissions" role_id: Mapped[str] = mapped_column( Uuid(), ForeignKey(f"{table_prefix}roles.id", ondelete="CASCADE"), primary_key=True ) permission_id: Mapped[str] = mapped_column( Uuid(), ForeignKey(f"{table_prefix}permissions.id", ondelete="CASCADE"), primary_key=True ) role: Mapped[Role] = relationship(back_populates="role_permissions") permission: Mapped[Permission] = relationship(back_populates="role_permissions") class UserRoleAssignment(base): """Assigns a role to a user, optionally scoped to a resource.""" __tablename__ = f"{table_prefix}user_role_assignments" __table_args__ = ( UniqueConstraint( "user_id", "role_id", "resource_type", "resource_id", name=f"uq_{table_prefix}user_role_resource" ), ) id: Mapped[str] = mapped_column(Uuid(), primary_key=True, default=uuid4) user_id: Mapped[str] = mapped_column(Uuid(), nullable=False, index=True) role_id: Mapped[str] = mapped_column( Uuid(), ForeignKey(f"{table_prefix}roles.id", ondelete="CASCADE"), nullable=False ) # Resource scoping (nullable = global assignment) resource_type: Mapped[str | None] = mapped_column(String(100), nullable=True) resource_id: Mapped[str | None] = mapped_column(Uuid(), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, nullable=False) role: Mapped[Role] = relationship(back_populates="assignments") def __repr__(self) -> str: scope = f"{self.resource_type}:{self.resource_id}" if self.resource_type else "global" return f"<UserRoleAssignment user={self.user_id} role={self.role_id} scope={scope}>" if class_prefix: for cls in (Permission, Role, RolePermission, UserRoleAssignment): new_name = f"{class_prefix}{cls.__name__}" cls.__name__ = new_name cls.__qualname__ = f"create_models.<locals>.{new_name}" return { "Permission": Permission, "Role": Role, "RolePermission": RolePermission, "UserRoleAssignment": UserRoleAssignment, }