BR 1.4.4: Единая permission-based ролевая модель (удаление enum)
Зависит от:
Отменяет часть BR 1.4.3
Двухслойная модель «multi-tenancy enum + permissions» из BR 1.4.3 заменяется на единый слой. Секция «Multi-tenancy роль» в спеке Роли и форма создания сотрудника — переделываются.
Контекст
BR 1.4.3 ввёл permissions-роли (Yuma-стиль) как второй слой поверх существующего enum employees.role (admin_franchise/admin_franchisee/manager/cashier). На момент реализации это было техническим компромиссом: все 5 сервисов (Store, Catalog, Warehouse, Order, User) использовали switch (user.getRole()) для авторизации, и разом переписать их на permissions-checks было большим скоупом.
Проблема выявленная при тестировании:
- При создании сотрудника пользователь видит два селекта ролей: системный dropdown «Менеджер / Кассир» (enum) и отдельный блок «Permissions-роли» с созданными им ролями («Бариста», «Курьер» и т.п.).
- Пользователь не понимает зачем два селекта. Интуиция: «я создал роль Бариста — вот она должна быть в dropdown ролей сотрудника».
Ресёрч аналогов общепита:
- iiko — иерархия Корпорация → Организация → Заведение. Роли кастомные, enum отсутствует. Админ корпорации видит всё, админ организации — только свою через контекст текущей выбранной организации.
- Poster — SaaS, плоский multi-location. Роли кастомные, нет enum.
- R-Keeper — enterprise иерархия, тоже без enum-ролей.
Вывод: ни у кого нет enum-роли. Scope определяется через владение юрлицом + назначенные заведения, а не через жёсткий enum.
Цель BR: перейти на единую permission-модель. После этой BR в системе один слой ролей (permissions-роли) и понятный UX.
Требования
1. Удаление enum employees.role
- В БД
employees.roleудаляется (DROP COLUMN) — больше нет никакого enum-разделения ролей. - Все сервисы (Store, Catalog, Warehouse, Order, User) переводят
switch(role)-проверки на:- проверки scope (см. §2) — где применимо (фильтрация данных, доступ к ТТ/ЮЛ/сотрудникам)
- проверки permissions (см. Роли) — где применимо (операции: edit/delete/publish/…)
2. Единое правило scope сотрудника (во всех сервисах)
Как сервис определяет «что видит сотрудник»:
| Условие | Scope |
|---|---|
user = legal_entities.owner_user_id и этого ЮЛ type=franchise | Вся франшиза — все ТТ, все ЮЛ, все сотрудники в рамках franchise_id |
user = legal_entities.owner_user_id и этого ЮЛ type=franchisee | Свои ЮЛ + их ТТ + их сотрудники (может быть несколько ЮЛ у одного владельца) |
| user — обычный сотрудник | ТТ из employee_role_stores (объединение всех его permissions-ролей) |
3. Флаг типа франшизы
Новое поле на уровне франшизы:
franchises.type: 'corporate' | 'individual'
corporate— полноценная франшиза: доступен раздел «Юр. лица», можно создавать партнёров (ЮЛtype=franchisee), есть иерархия владенияindividual(ИП) — одно главное ЮЛ, раздел «Юр. лица» скрыт в UI. API: попытка создать ЮЛtype=franchisee→403 FRANCHISE_TYPE_INDIVIDUAL- Default при создании новой франшизы:
corporate(можно изменить вручную перед запуском клиента) - Текущая (единственная) франшиза при миграции →
corporate
4. POS PIN-логин
Ранее POS определял «можно ли логиниться по PIN» через employees.role = 'cashier'.
Новое правило:
POST /internal/users/validate-pinвозвращает успех, если:- PIN совпадает с
pin_hashсотрудника ТТ - У сотрудника в permissions (агрегат permissions-ролей) есть
pos.access
- PIN совпадает с
- Иначе —
403 POS_ACCESS_DENIED
Permission pos.access уже существует в каталоге (BR 1.4.3).
5. Системная роль «Администратор» и настраиваемые права владельца партнёра
5.1 Системная роль «Администратор»
- Остаётся одна системная роль «Администратор» (как в BR 1.4.3) — автосоздаётся при создании франшизы
- Полный набор permissions включая
pos.access(чтобы владелец мог зайти на POS своей ТТ) - Выдаётся владельцу франшизы при ручном bootstrap (seed / SQL при подписании договора)
5.2 Настраиваемые права владельца партнёра (новое)
При создании ЮЛ партнёра (type=franchisee) главный админ франшизы сам выбирает, какие права дать владельцу партнёра. Форма создания ЮЛ расширяется блоком «Права владельца»:
| Режим | Что происходит |
|---|---|
| «Полный доступ» (default, рекомендуется) | Владельцу партнёра назначается системная роль «Администратор». Получает все permissions, scope ограничен его ЮЛ (через §2) |
| «Настроенные права» | Админ вручную выбирает permissions из каталога. Система создаёт скрытую персональную роль для этого ЮЛ с выбранными permissions и назначает её владельцу |
UX ключевое: роли для владельцев партнёров — не видны в /admin/roles.
Саша управляет правами партнёра через карточку ЮЛ партнёра /admin/legal-entities/{id}, вкладка «Права»:
ИП Петров
[Реквизиты] [Владелец] [Права] [ТТ]
─ Права владельца ──────────
○ Полный доступ (системная роль «Администратор»)
● Настроенные права
Разделы Бэк-офиса:
Меню ☑ чтение ☑ редакт.
Прейскуранты ☑ чтение ☐ редакт.
Публикация ТТ ☐
...
Функции POS:
☑ pos.access
...
В списке /admin/roles Саша видит только настоящие роли (системную «Администратор» + созданные им роли сотрудников: «Бариста», «Курьер» и т.п.). Скрытые роли-владельцев туда не попадают.
Примеры «Настроенные права»:
Саша создаёт партнёра «Петров» и хочет, чтобы Петров:
- мог управлять каталогом, складом, сотрудниками своих ТТ — ✅
- не мог менять цены (изменения прейскуранта) — ❌
- не мог публиковать/снимать ТТ с публикации — ❌
Саша заходит в карточку ЮЛ Петрова → вкладка «Права» → снимает галки → сохраняет.
Что всегда выдаётся автоматически (независимо от выбора):
pos.access— чтобы владелец мог логиниться на POS по PIN (иначе теряется смысл «владелец»)stores.read,employees.read— минимум чтобы видеть свои данные
Переключение режима: если Саша из «Настроенные» переключает на «Полный доступ» → скрытая роль отсоединяется и на её место назначается системная «Администратор». При обратном переключении — заново создаётся скрытая роль (с галками по умолчанию «все включены» или пустая).
Техническая реализация скрытых ролей: в таблице roles добавить служебное поле owner_legal_entity_id (UUID NULL FK → legal_entities.id). Если задано — роль скрыта из /admin/roles списка и редактируется только через карточку соответствующего ЮЛ.
5.3 Разница между владельцами франшизы и партнёра
Определяется не ролью (у обоих может быть «Администратор»), а:
- Владением ЮЛ (
owner_user_id+typeЮЛ — см. §2 про scope) franchises.type(§3 —individualИП не может иметь партнёров вообще)
6. UX формы создания сотрудника
- Убрать dropdown «Менеджер / Кассир» — больше нет enum
- Оставить только единый раздел «Роли» — multi-select из созданных permissions-ролей + для каждой свой набор магазинов (как сейчас в «Permissions-роли» блоке)
- Магазины для назначения ограничены scope’ом текущего пользователя:
- Владелец франшизы → любая ТТ
- Владелец партнёра → только свои ТТ
- Обычный сотрудник → обычно не создаёт других сотрудников (нет
employees.edit)
7. UI для франшиз type=individual (ИП)
- Пункт меню «Юр. лица» скрыт
- На дашборде — нет виджета «Юр. лица»
- Раздел «Торговые точки» — остаётся (у ИП тоже могут быть несколько ТТ на одном ЮЛ)
- Форма создания сотрудника — не меняется (ЮЛ в ней не упоминается)
8. Bootstrap новой франшизы — вручную
- MVP-подход: когда клиент подписывает договор, администратор системы вручную делает SQL:
- Создаёт запись в
franchises(с нужнымtype) - Создаёт главное ЮЛ (
legal_entitiesсtype=franchise,is_primary=true) - Создаёт сотрудника-владельца (email вида
ivan@example.com, пароль — временный, передать клиенту) - Связывает ЮЛ и сотрудника через
owner_user_id - Присваивает сотруднику системную роль «Администратор» (через
employee_roles)
- Создаёт запись в
- Автоматическая регистрация через UI — отдельная BR в будущем
9. Миграция при выкатке
- ALTER TABLE
franchisesADD COLUMNtypeVARCHAR(20) DEFAULT ‘corporate’ NOT NULL - ALTER TABLE
employeesDROP COLUMNrole - Все сервисы пересобираются и деплоятся синхронно (enum из JWT убираем с зависимостями)
admin@erp.localуже:- привязан к системной роли «Администратор» (через миграцию 017 в BR 1.4.3)
- является
owner_user_idглавного ЮЛ → его scope после миграции = вся франшиза, permissions = полный набор. Работает.
- Других сотрудников нет (миграция 017 их удалила) — рисков не создаёт
Бизнес-правила
- Нет enum-ролей. Сотрудник не имеет «роли» как отдельного поля — только набор назначенных permissions-ролей
- Scope определяется по правилам §2, в следующем приоритете: владение
type=franchise→ владениеtype=franchisee→ ТТ изemployee_role_stores - Системная роль «Администратор» неделима: у владельца франшизы и у владельца партнёра permissions одинаковые, разница только в scope
franchises.type=individual: API блокирует создание партнёров, UI прячет соответствующие разделы. Изменение типа франшизы после старта (individual ↔ corporate) — вручную SQL, отдельной UI-операции нет- POS PIN-login: разрешён только сотрудникам с
pos.accessв permissions + заданнымpin_hash. У владельца франшизы/партнёра (системная «Администратор») — разрешено (в системной ролиpos.access=true) - Backward compat JWT: старые токены содержат поле
role. Сервисы не полагаются на него (всё через permissions и scope). Поле в новых JWT — удаляется (или остаётсяnullдля плавности)
Затронутые сервисы
| Сервис | Что меняется |
|---|---|
| User Service | DROP employees.role; ADD franchises.type; переписывается scope-логика в EmployeeService, LegalEntityService, StoreService(internal); форма создания ЮЛ партнёра принимает либо режим «полные права» → системная роль, либо «настроить» → создаётся новая роль «Владелец {name}» с переданными permissions в одной транзакции; форма создания сотрудника — без enum-поля; новый internal endpoint GET /internal/users/{id}/scope (возвращает either {type:all_franchise}, {type:legal_entity_ids, ids:[]}, или {type:store_ids, ids:[]}) |
| Auth Service | JWT payload: убрать role (или null); /auth/me — убрать role из response user; /internal/auth/validate — убрать role из response |
| Store / Catalog / Warehouse / Order Services | Перевод всех switch(role) на scope-check (через новый User Service internal endpoint с кэшем, либо включить scope в response /internal/auth/validate) + permission-check для операций edit/delete/publish |
| Admin BFF | Никаких серверных правок |
| Admin Web | Убрать dropdown multi-tenancy роли в форме сотрудника; единый блок «Роли»; скрыть «Юр. лица» при franchise.type=individual; PermissionContext получает franchise.type; форма создания ЮЛ партнёра получает блок «Права владельца» (radio «Полные / Настроить» + checkboxes permissions при «Настроить») |
| POS mobile (erp-pos) | Никаких клиентских правок (логика PIN-login на бэке) |
Ролевой доступ (итоговая упрощённая матрица)
| Действие | Владелец франшизы (corporate) | Владелец ЮЛ-партнёра | Обычный сотрудник |
|---|---|---|---|
| Определяется через | owner_user_id в главном ЮЛ | owner_user_id в ЮЛ type=franchisee | employee_role_stores |
| Сист. роль «Администратор» | Да | Да | Обычно нет |
| Видит ТТ | Все в франшизе | Только свои | Только свои назначенные |
| Видит сотрудников | Всех | Только в своих ТТ | Только в своих ТТ (если employees.read permission) |
| Создаёт ЮЛ партнёра | Да (если type=corporate) | Нет (scope) | Нет (scope + permission) |
| Логин на POS по PIN | Да (у системной роли есть pos.access) | Да | Если у permissions-роли есть pos.access + есть pin_hash |
Открытые вопросы
- Scope в internal API: оставить
roleв/internal/auth/validateresponse пустым или удалить совсем? Решение влияет на обратную совместимость старых клиентов. - Новый endpoint vs расширение существующего: возвращать scope в
/internal/auth/validateили создать отдельный/internal/users/{id}/scope? Первый вариант проще интегрировать с существующим кэшем Auth Service. - Performance: каждый сервис на каждый запрос делает scope-check. Нужен Redis-кэш
user_scope:{user_id}аналогичноuser_permissions:{user_id}(60s TTL). Проработать на этапе контрактов. - Permission
pos.access: оставить текущее имя или переименовать наpos.login? (текущее устоявшееся — оставляем) - Изменение
franchise.typeпосле старта: нужен ли API/UI или вечно SQL? MVP — SQL. Именование персональной роли владельца партнёра— решено: роль скрыта от пользователя, управляется через карточку ЮЛ. Имя в БД — техническое (владелец его не видит).- Минимум permissions для владельца партнёра (§5.2): жёстко форсить
pos.access + stores.read + employees.read(нельзя снять через UI) или позволить полностью выбирать и хардкодить только serverside-валидацию? Влияет на безопасность. - Шаблоны ролей владельцев партнёров — возможность заранее создать «Партнёр (базовый)» и переиспользовать. Вне скоупа BR 1.4.4, отдельная BR в будущем.
Ссылки
- YumaPOS: Роли
- YumaPOS: Добавление сотрудника
- Ролевая модель — будет обновлена
- Роли — будет обновлена (убрать секцию про multi-tenancy)
- Сотрудники — будет обновлена
- BR 1.4.3