Перейти до основного вмісту

ADR-004: Доступ до кількох відділів

  • Статус: прийнято
  • Дата: 2026-04-16
  • Автор: 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-поля і додаємо department scope через role- і user-override масиви:

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

Початковий дизайн розглядав join-таблицю UserDepartmentAccess(userId, departmentId). Поточна реалізація використовує масиви, бо очікувані scope-набори малі, обчислення доступу hot-path, а overrides і так обчислюються разом із дозволами.

Ефективний департаментний доступ такий:

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

Розглянуті альтернативи

Замінити departmentId на join-таблицю

Відхилено на цій фазі. Це ускладнило б базовий single-department-сценарій і вимагало б від кожного маршруту join або preload access-рядків.

Тримати тільки departmentId

Відхилено. Не моделює cross-functional-менеджерів без видачі повного admin-доступу.

Використати тільки SystemRole.departmentIds

Відхилено. Це робить one-off винятки дорогими, бо кожен виняток потребує нової ролі.

Наслідки

Позитивні

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

Негативні

  • Масиви треба валідувати при прийомі department id з API-запитів.
  • Дуже великі department scope-и краще представляти через join-таблицю.
  • Виклики, яким треба саме один відділ, не мають використовувати requireDepartmentId() для multi-department-користувачів.

Нотатки про реалізацію

  • effectiveDepartments() виконує set-арифметику.
  • forUser() виставляє deptIds, directWhere(), nestedWhere() і canSeeDept().
  • Новий route-код має використовувати scope.canSeeDept(id) для loaded-ресурсів і scope.directWhere() або scope.nestedWhere() для запитів.