ADR-005: Перевизначення дозволів
- Статус: прийнято
- Дата: 2026-04-16
- Автор: CTO Agent (KALA-148)
Контекст
Кастомні ролі покривають стабільну політику tenant, але продакшн-системам завжди потрібні винятки:
- тимчасово видати користувачу доступ до ролі рівнем вище;
- revoke-нути ризикований дозвіл у користувача з широкою роллю;
- додати один відділ під проект;
- прибрати один відділ, який інакше дає shared-роль.
Створювати нову роль під кожен виняток — це role explosion. Повна per-user permission-таблиця додала б більше механіки, ніж зараз вимагає продукт.
Рішення
Тримаємо override-и прямо на User:
extraPermissions String[]
revokedPermissions String[]
extraDepartmentIds String[]
revokedDepartmentIds String[]
Ефективні дозволи обчислюються так:
effectivePerm = role.permissions + extraPermissions - revokedPermissions
Ефективні відділи — той самий add-then-subtract патерн:
effectiveDept = role.departmentIds + primary department + extraDepartmentIds - revokedDepartmentIds
Revoke застосовується останнім, тому перемагає і базову роль, і extra.
Розглянуті альтернативи
Per-user permission join-таблиця
Поки відхилено. Легше історично запитувати, але додає write-шляхи, джойни та складність UI, а дозволи мають достатню cardinality, щоб це виправдати.
Кастомна роль під кожен виняток
Відхилено. Створює шум у списку ролей і приховує те, що зміна — це індивідуальний виняток.
Тільки deny-overrides
Відхилено. Продукт потребує і тимчасових grant-ів, і extra department access.
Наслідки
Позитивні
- Винятки явно видимі на user-записі.
- Ефективний доступ детермінований і легко пояснюється.
- Grant/revoke-API можуть писати audit-записи зі зміненим ключем.
- Revoke дає простий deny-механізм без повноцінної policy-системи.
Негативні
- Масиви менш нормалізовані, ніж join-рядки.
- Перейменування permission-ключа потребує array migration.
- Забагато overrides на одного користувача — це smell, краще створити кастомну роль.
Нотатки про реалізацію
POST /users/:id/permissions/grantдодає permissions або extra departments.POST /users/:id/permissions/revokeдодає permission- або department-revokes.- DELETE-маршрути прибирають grant- чи revoke-записи.
GET /users/:id/effective-accessповертає роль, overrides та обчислений доступ — для дебагу.- Кожна override-мутація пише audit-log-запис.