arm-core/api/src/models/access.py
Andrey Epifancev ab606dc59a feat(model): DBML v5.15 — osv_data_type NU, email в нижнем регистре, миграция 0003
- 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
2026-10-05 21:28:04 +04:00

76 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Часть 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)