- OsvDataType.NU после BU, DBML_VERSION = 5.15, Makefile/dbml_parity/docstring'и на data-model-v5.15.dbml; - миграция 0003_dbml_v5_15: ADD VALUE 'NU' AFTER 'BU' + lower(app_user.email); downgrade пересоздаёт osv_data_type (регистр email не возвращается); - email хранится в нижнем регистре: users_admin.create_user и seed (_apply_users); с UNIQUE по строчному email гонка создания в другом регистре теперь ловится индексом (тест test_api_constraint_races обновлён); - тесты: enum NU, откат на шаг, lower email на миграции, seed/админка, вставка NU, фиктивная v5.16. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
76 lines
3.9 KiB
Python
76 lines
3.9 KiB
Python
"""Часть 02 DBML v5.15 «Пользователи и доступ»: пользователи, роли, права как данные (`effective_permissions`)."""
|
||
|
||
import datetime
|
||
import uuid
|
||
|
||
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, String, Text, text
|
||
from sqlalchemy.dialects.postgresql import UUID
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from .base import Base, UUIDPrimaryMixin
|
||
from .enums import BusinessSegment, PermissionAction, PermissionObject, pg_enum
|
||
|
||
|
||
class AppUser(UUIDPrimaryMixin, Base):
|
||
"""Пользователь; аутентификация внешняя (JWT РАИСА, email → `app_user`)."""
|
||
|
||
__tablename__ = "app_user"
|
||
|
||
external_id: Mapped[str | None] = mapped_column(String, unique=True)
|
||
email: Mapped[str | None] = mapped_column(String, unique=True)
|
||
full_name: Mapped[str] = mapped_column(String, nullable=False)
|
||
employee_no: Mapped[str | None] = mapped_column(String)
|
||
regional_office_id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True), ForeignKey("regional_office.id"), nullable=False
|
||
)
|
||
business_segment: Mapped[BusinessSegment | None] = mapped_column(pg_enum("business_segment"))
|
||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("true"))
|
||
created_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
|
||
updated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
|
||
|
||
|
||
class Role(UUIDPrimaryMixin, Base):
|
||
"""Роль; полномочия — через `role_permission`, код роли в бизнес-логике не проверяется."""
|
||
|
||
__tablename__ = "role"
|
||
|
||
code: Mapped[str] = mapped_column(String, nullable=False, unique=True)
|
||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||
description: Mapped[str | None] = mapped_column(Text)
|
||
is_system: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("false"))
|
||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("true"))
|
||
|
||
|
||
class Permission(UUIDPrimaryMixin, Base):
|
||
"""Справочник прав «объект × действие» (seed, привязан к коду)."""
|
||
|
||
__tablename__ = "permission"
|
||
__table_args__ = (Index("uq_permission_object_type_action", "object_type", "action", unique=True),)
|
||
|
||
code: Mapped[str] = mapped_column(String, nullable=False, unique=True)
|
||
object_type: Mapped[PermissionObject] = mapped_column(pg_enum("permission_object"), nullable=False)
|
||
action: Mapped[PermissionAction] = mapped_column(pg_enum("permission_action"), nullable=False)
|
||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||
description: Mapped[str | None] = mapped_column(Text)
|
||
|
||
|
||
class RolePermission(Base):
|
||
"""Матрица доступов как данные; составной PK `(role_id, permission_id)`, суррогатного `id` нет."""
|
||
|
||
__tablename__ = "role_permission"
|
||
|
||
role_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("role.id"), primary_key=True)
|
||
permission_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("permission.id"), primary_key=True)
|
||
granted_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"))
|
||
granted_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
|
||
|
||
|
||
class UserRole(UUIDPrimaryMixin, Base):
|
||
"""Назначение ролей пользователю (M:N, current-state); история — `audit_event(USER_ROLE_CHANGE)`."""
|
||
|
||
__tablename__ = "user_role"
|
||
__table_args__ = (Index("uq_user_role_user_id_role_id", "user_id", "role_id", unique=True),)
|
||
|
||
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"), nullable=False)
|
||
role_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("role.id"), nullable=False)
|