169 lines
12 KiB
Markdown
169 lines
12 KiB
Markdown
# Роли доступа и матрица прав
|
||
|
||
Ordinis использует **Keycloak realm roles** (claim `realm_access.roles` в JWT) для двух уровней контроля доступа:
|
||
|
||
1. **Scope ACL** — что пользователь может **читать** (видимость записей и словарей)
|
||
2. **Workflow roles** — какие **действия** пользователь может выполнять (approve, publish, reject)
|
||
|
||
Все роли используют colon-separated namespace `ordinis:<домен>:<действие>`.
|
||
|
||
## Полная матрица ролей
|
||
|
||
### Scope ACL (видимость данных)
|
||
|
||
| Realm role | Видит | Подразумевает |
|
||
|---|---|---|
|
||
| `ordinis:client:public` | только PUBLIC записи | — |
|
||
| `ordinis:client:internal` | PUBLIC + INTERNAL | автоматически включает public |
|
||
| `ordinis:client:restricted` | PUBLIC + INTERNAL + RESTRICTED | автоматически включает оба нижних |
|
||
|
||
Без любой из этих ролей пользователь считается **anonymous** и видит только `PUBLIC` записи (fallback).
|
||
|
||
> Альтернативные dash-формы (`ordinis-public`, `ordinis-internal`, `ordinis-restricted`) также распознаются для совместимости. Канонические — colon-форма.
|
||
|
||
### Workflow роли (право на действия)
|
||
|
||
| Realm role | Право |
|
||
|---|---|
|
||
| `ordinis:schema:reviewer` | Approve / Запросить правки / Отклонить **schema draft** |
|
||
| `ordinis:schema:publisher` | Publish approved schema draft → live |
|
||
| `ordinis:record:reviewer` | Approve / Reject **record draft** (Approval Workflow v2) |
|
||
| `ordinis:admin` | **Super-role** — автоматически разрешает любой workflow action. Convenience для small teams: assign one role вместо трёх. |
|
||
|
||
Backend проверяет роль перед каждым действием. Без роли — `403 forbidden_role` с сообщением о требуемой роли. Frontend дополнительно скрывает кнопки, для которых нет роли.
|
||
|
||
> **JWT claim path:** на staging/prod backend читает роли из `resource_access.ordinis.roles` (client-scoped roles в Keycloak `ordinis` client). Default fallback — `realm_access.roles`. Настраивается через `ORDINIS_AUTH_ROLES_CLAIM` env var.
|
||
|
||
> **Maker side** (создание / submit / withdraw draft'а) специальной ролью не gated — любой authenticated user с required scope может создать draft. Backend защищает invariant maker-checker через `403 self_approve_forbidden` (один и тот же user не может approve свой draft).
|
||
|
||
## Полная матрица действий
|
||
|
||
| Действие | Endpoint | Scope требуется | Workflow роль требуется |
|
||
|---|---|---|---|
|
||
| Просмотр PUBLIC dict | `GET /api/v1/dictionaries` | public+ | — |
|
||
| Просмотр INTERNAL/RESTRICTED dict | `GET /api/v1/dictionaries` | соответствующий scope | — |
|
||
| Создание dict | `POST /api/v1/dictionaries` | соответствующий scope | — |
|
||
| Редактирование dict schema (через draft) | `POST /api/v1/dictionaries/{name}/drafts` | соответствующий scope | — (maker) |
|
||
| Submit schema draft на ревью | `POST .../drafts/{id}/review` | соответствующий scope | — (maker) |
|
||
| **Approve / Request changes / Reject schema draft** | `POST .../drafts/{id}/decision` | соответствующий scope | `ordinis:schema:reviewer` |
|
||
| **Publish approved schema draft** | `POST .../drafts/{id}/publish` | соответствующий scope | `ordinis:schema:publisher` |
|
||
| Withdraw собственный schema draft | `POST .../drafts/{id}/withdraw` | соответствующий scope | — (maker self) |
|
||
| Создание record draft | `POST .../records/.../drafts` | соответствующий scope | — (maker) |
|
||
| **Approve record draft** | `POST /api/v1/drafts/{id}/approve` | соответствующий scope | `ordinis:record:reviewer` |
|
||
| **Reject record draft** | `POST /api/v1/drafts/{id}/reject` | соответствующий scope | `ordinis:record:reviewer` |
|
||
| Просмотр webhook subscriptions | `GET /api/v1/admin/webhooks/subscriptions` | `internal` или `restricted` | — |
|
||
| Просмотр audit log | `GET /api/v1/admin/audit` | соответствующий scope | — |
|
||
|
||
## Типичные профили пользователей
|
||
|
||
### Только чтение (consumer, integrator)
|
||
- `ordinis:client:public`
|
||
|
||
Видит публичные справочники, не может ничего менять.
|
||
|
||
### Maker (контент-редактор)
|
||
- `ordinis:client:internal` (видит данные своей области)
|
||
|
||
Создаёт и редактирует справочники, записи, schema drafts, submit'ит их на ревью. **Не может** approve / publish свои собственные действия (maker-checker).
|
||
|
||
### Reviewer
|
||
- `ordinis:client:restricted`
|
||
- `ordinis:schema:reviewer`
|
||
- `ordinis:record:reviewer`
|
||
|
||
Может всё что maker, плюс approve / reject чужих drafts. Publish — отдельной ролью.
|
||
|
||
### Publisher
|
||
- `ordinis:client:restricted`
|
||
- `ordinis:schema:reviewer` (опционально)
|
||
- `ordinis:schema:publisher`
|
||
|
||
Финальный gatekeeper для production публикации схем. Обычно более узкая группа чем reviewers.
|
||
|
||
### Admin (полный доступ)
|
||
|
||
**Простой путь** — single role `ordinis:admin` (super-role): backend и frontend оба recognize её как override для любого workflow check'а. Достаточно одного назначения.
|
||
|
||
**Канонический путь (composite в Keycloak)** — `ordinis:admin` как composite, агрегирующий:
|
||
- `ordinis:client:restricted`
|
||
- `ordinis:schema:reviewer`
|
||
- `ordinis:schema:publisher`
|
||
- `ordinis:record:reviewer`
|
||
|
||
Composite-роль в Keycloak автоматически expands в JWT — token содержит все включённые роли. Это работает и без super-role override.
|
||
|
||
## Настройка в Keycloak
|
||
|
||
### Создание ролей
|
||
|
||
1. Открой **Keycloak Admin Console** → realm `nstart` → **Realm roles**.
|
||
2. Нажми **Create role** и создай по очереди:
|
||
- `ordinis:client:public`
|
||
- `ordinis:client:internal`
|
||
- `ordinis:client:restricted`
|
||
- `ordinis:schema:reviewer`
|
||
- `ordinis:schema:publisher`
|
||
- `ordinis:record:reviewer`
|
||
|
||
### Composite role `ordinis:admin` (опционально, но удобно)
|
||
|
||
3. **Create role** → `ordinis:admin`.
|
||
4. На вкладке роли → **Action** → **Add associated roles** → выбери все 6 ролей выше → **Assign**.
|
||
|
||
После этого можно назначать одну роль `ordinis:admin` users'ам, и они автоматически получают все права.
|
||
|
||
> **Заметка про token mapper:** composite role в Keycloak **разворачивается в JWT** только если есть соответствующий mapper. У client `ordinis` в realm `nstart` должен быть mapper типа **User Realm Role** или **User Client Role** с `Multivalued: ON` и `Add to access token: ON`. Без mapper'а composite роль будет на user'е, но в JWT попадёт только сама `ordinis:admin`, а её components (`ordinis:schema:reviewer` и т.д.) — нет.
|
||
>
|
||
> Backend handles это двумя путями:
|
||
> 1. **Mapper настроен** (рекомендуется): JWT содержит all 6 expanded roles → `WorkflowRoles.hasRole(specific)` находит её напрямую.
|
||
> 2. **Mapper не настроен** (default Keycloak install): JWT содержит только `ordinis:admin` → `WorkflowRoles.hasRole(role)` всё равно проходит через super-role override (`roles.contains(ADMIN)`).
|
||
>
|
||
> Super-role override — fallback. Mapper — proper solution. Если admin user'ов больше 5 — настройте mapper, чтобы scope ACL и audit log видели правильные конкретные роли.
|
||
|
||
### Per-dictionary минимальная роль ревьюера (Phase 2)
|
||
|
||
В `DictionaryEditorDialog` есть поле **«Минимальная роль ревьюера (опционально)»** (`approvalMinRole`). Если задано (e.g. `ordinis:restricted`) — backend требует чтобы reviewer держал эту роль **поверх** стандартной `ordinis:record:reviewer` / `ordinis:schema:reviewer` / `ordinis:schema:publisher`.
|
||
|
||
Use case: критичный справочник (PII, госуд. реестр) — `approvalMinRole = ordinis:restricted` гарантирует что approve может только user с restricted-scope ролью, даже если у него есть reviewer на других dict'ах. `ordinis:admin` super-role проходит всегда.
|
||
|
||
Поле сохраняется в DB как plain string. Backend (`DraftService.requireDictMinRoleIfSet`, `SchemaDraftService.requireDictMinRoleIfSet`) делает `workflowRoles.requireRole(approvalMinRole)` перед состоянием перехода.
|
||
|
||
### Назначение пользователю
|
||
|
||
1. **Users** → найди user'а → **Role mapping**.
|
||
2. **Assign role** → отметь нужные роли (или `ordinis:admin` для админов).
|
||
|
||
### Применение
|
||
|
||
Пользователю нужно **logout / login** в Ordinis UI, чтобы получить новый JWT с обновлённым `realm_access.roles` claim'ом.
|
||
|
||
## Диагностика
|
||
|
||
### «Не могу нажать Approve — кнопка скрыта»
|
||
|
||
UI скрывает кнопку если у user'а нет нужной workflow роли. Проверить:
|
||
|
||
1. F12 → Application → Session Storage → `oidc.user:...`.
|
||
2. В JSON найти `profile.realm_access.roles`.
|
||
3. Должна быть `ordinis:schema:reviewer` (для schema) или `ordinis:record:reviewer` (для record).
|
||
|
||
Если роли нет — назначить в Keycloak и сделать logout/login.
|
||
|
||
### «403 forbidden_role» от backend
|
||
|
||
Backend вернул 403 с кодом `forbidden_role`. Сообщение содержит требуемую роль. Назначь её в Keycloak и refresh JWT.
|
||
|
||
### «403 self_approve_forbidden»
|
||
|
||
Пользователь пытается approve свой собственный draft. Это **invariant maker-checker** — backend защищает от self-approve. Нужен другой reviewer.
|
||
|
||
### «404 / пустая страница на /webhooks»
|
||
|
||
Webhook subscriptions требуют `internal` или `restricted` scope. Назначь `ordinis:client:internal` минимум.
|
||
|
||
## История
|
||
|
||
- **Phase 1a/1b/1c** (до 2026-05-12) — backend гейтил только maker-checker, любой authenticated мог review.
|
||
- **Phase 1d** (2026-05-13) — backend проверяет workflow роли; frontend скрывает кнопки. Migration require: создать роли в Keycloak и assign actual reviewer-users до или одновременно с deploy.
|
||
- **Phase 2** (2026-05-13, vечером) — per-dict `approvalMinRole` enforced. Поле существовало с миграции 0019, но backend его игнорировал (UI hint прямо говорил "(Phase 2 — enforcement TBD)"). Теперь required gate в approve/reject/decide/publish.
|