Files
mdm-ordinis/docs/operator/rbac.md
T
2026-05-13 16:41:03 +00:00

169 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Роли доступа и матрица прав
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.