Перейти к основному содержимому

ADR-004: Доступ к нескольким отделам

  • Статус: Принято
  • Дата: 16 апреля 2026
  • Автор: CTO Agent (KALA-148)

Контекст

User.departmentId изначально представлял полный data scope пользователя. Это работает для сотрудника одного отдела, но не для менеджеров и shared-services команд. Project manager-у может понадобиться доступ к Legal, Finance и HR, при этом должен оставаться один primary отдел для ownership и дефолта.

В системе уже была изоляция отделов в REST-маршрутах, WebSocket-подписках, RAG-запросах и фоновых воркерах. Multi-department access должен был расширить эту модель изоляции, не ослабляя её.

Решение

Оставить User.departmentId и User.primaryDepartmentId как primary/default поля отдела и добавить department scope через role и user-override массивы:

  • SystemRole.allDepartments;
  • SystemRole.departmentIds;
  • User.extraDepartmentIds;
  • User.revokedDepartmentIds.

Исходный дизайн рассматривал join-таблицу UserDepartmentAccess(userId, departmentId). Текущая реализация использует массивы, потому что ожидаемый scope-сет невелик, вычисление доступа идёт на каждый read, а override-ы и так вычисляются рядом с permissions.

Эффективный department access:

if role.allDepartments or legacy role is admin:
all departments
else:
role.departmentIds
+ primaryDepartmentId
+ departmentId
+ extraDepartmentIds
- revokedDepartmentIds

Рассмотренные альтернативы

Заменить departmentId join-таблицей

Пока отклонено. Это усложнило бы обычный single-department case и потребовало бы на каждом маршруте джоинить или предзагружать access-строки.

Оставить только departmentId

Отклонено. Не позволяет моделировать cross-functional менеджеров без выдачи полного admin-доступа.

Только SystemRole.departmentIds

Отклонено. Делает одноразовые исключения дорогими — каждое исключение требует новой роли.

Последствия

Плюсы

  • Пользователи одного отдела остаются простыми.
  • Менеджеры получают ограниченный multi-department доступ, не становясь админами.
  • Revoke может убрать отдел, даже если базовая роль его разрешает.
  • Один и тот же хелпер forUser() поддерживает прямые Prisma-фильтры и вложенные.

Минусы

  • Массивы требуют валидации при приёме department ID из API-запросов.
  • Очень большие department scopes лучше представлять join-таблицей.
  • Код, которому нужен ровно один отдел, не должен вызывать requireDepartmentId() для multi-department пользователей.

Заметки по реализации

  • effectiveDepartments() делает арифметику.
  • forUser() отдаёт deptIds, directWhere(), nestedWhere() и canSeeDept().
  • Новый route-код должен использовать scope.canSeeDept(id) для загруженных ресурсов и scope.directWhere() или scope.nestedWhere() для запросов.