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()для запросов.