Source code for flask_more_smorest.perms.models.abstract_role

"""Abstract role and domain models for Flask-More-Smorest.

Provides abstract bases for Domain and UserRole models for
multi-domain role-based access control.
"""

from __future__ import annotations

import enum
import uuid
from typing import TYPE_CHECKING

import sqlalchemy as sa
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import Mapped, mapped_column, relationship

from ..base_perms_model import BasePermsModel

if TYPE_CHECKING:
    from .abstract_user import AbstractUser


[docs] class AbstractDomain(BasePermsModel): """Abstract Domain model for multi-domain support. This is an abstract base class - it does NOT create a database table. Subclasses must define concrete fields and table configuration. Domains represent distinct contexts within an application (e.g., organizations, tenants, or projects) where roles can be scoped. **Subclassing example:** .. code-block:: python from flask_more_smorest.perms import AbstractDomain class CustomDomain(AbstractDomain): __tablename__ = "domain" name: Mapped[str] = mapped_column(db.String(255), nullable=False) display_name: Mapped[str] = mapped_column(db.String(255), nullable=False) active: Mapped[bool] = mapped_column(db.Boolean, default=True, nullable=False) # Optional: custom fields organization_id: Mapped[str] = mapped_column(db.String(50)) settings: Mapped[dict] = mapped_column(db.JSON, default={}) """ __abstract__ = True # No table is created __tablename__ = "domain" __table_args__ = {"extend_existing": True} # noqa: RUF012 name: Mapped[str] = mapped_column(sa.String(255), nullable=False) display_name: Mapped[str] = mapped_column(sa.String(255), nullable=False) active: Mapped[bool] = mapped_column(sa.Boolean(), default=True, nullable=False)
[docs] class AbstractUserRole(BasePermsModel): """Abstract UserRole model with domain scoping for multi-domain applications. This is an abstract base class - it does NOT create a database table. Subclasses must define concrete fields and table configuration. Supports custom role enums by accepting any string/enum value: .. code-block:: python from enum import Enum class CustomRole(str, Enum): SUPERADMIN = "SUPERADMIN" ADMIN = "ADMIN" MANAGER = "MANAGER" USER = "USER" class CustomUserRole(AbstractUserRole): __tablename__ = "user_role" user_id: Mapped[uuid.UUID] = mapped_column( sa.Uuid(as_uuid=True), db.ForeignKey("user.id"), nullable=False ) domain_id: Mapped[uuid.UUID | None] = mapped_column( sa.Uuid(as_uuid=True), db.ForeignKey("domain.id"), nullable=True, default=None, ) _role: Mapped[str] = mapped_column("role", sa.String(50), nullable=False) # Create roles with custom enum values role = CustomUserRole(user=user, role=CustomRole.MANAGER) """ __abstract__ = True # No table is created __tablename__ = "user_role" __table_args__ = {"extend_existing": True} # noqa: RUF012 user_id: Mapped[uuid.UUID] = mapped_column(sa.Uuid(as_uuid=True), sa.ForeignKey("user.id"), nullable=False) domain_id: Mapped[uuid.UUID | None] = mapped_column( sa.Uuid(as_uuid=True), sa.ForeignKey("domain.id"), nullable=True, default=None, ) _role: Mapped[str] = mapped_column("role", sa.String(50), nullable=False) @declared_attr def user(cls) -> Mapped[AbstractUser]: from ..user_registry import get_user_model return relationship( lambda: get_user_model(), back_populates="roles", enable_typechecks=False, ) @declared_attr def domain(cls) -> Mapped[AbstractDomain | None]: from ..user_registry import get_domain_model return relationship(lambda: get_domain_model()) @property def role(self) -> str: """Get role as string value. Returns: Role name as string """ return self._role @role.setter def role(self, value: str | enum.Enum) -> None: """Set role value from enum or string. Args: value: Role value (enum or string) """ # Normalize role to uppercase string for consistency # This handles both enum values (already uppercase) and string inputs self._role = self._normalise_role(value)
[docs] def __init__( self, domain_id: uuid.UUID | str | None = None, role: str | enum.Enum | None = None, **kwargs: object, ) -> None: """Initialize role with domain and role handling. Args: domain_id: Domain UUID, None for all domains, or '*' string (converted to None) role: Role value (enum or string) **kwargs: Additional field values """ # Note: domain_id is no longer defaulted here - user must provide it explicitly # Force explicit use of '*' to set domain_id to None: if domain_id == "*": domain_id = None if isinstance(domain_id, str): raise TypeError("Expected domain_id to be UUID, None or '*'") # Handle role parameter - normalize to uppercase if role is not None: kwargs["_role"] = self._normalise_role(role) super().__init__(domain_id=domain_id, **kwargs)
def _normalise_role(self, role: str | enum.Enum) -> str: """Normalize role value for consistent comparisons. Returns: Normalized role string """ if isinstance(role, enum.Enum): return str(role.value).upper() return str(role).upper()