36c06ae1da
User report: token has roles в resource_access.ordinis.roles (client-scoped),
backend WorkflowRoles читал hardcoded realm_access.roles → forbidden_role
для всех reviewer actions.
Backend:
- WorkflowRoles: depends OrdinisAuthProperties → reads via rolesClaim path
(же что ScopeContext). Default realm_access.roles, на staging/prod env
var ORDINIS_AUTH_ROLES_CLAIM=resource_access.ordinis.roles (уже wired
в helm helper).
- Nested claim path resolver — mirror ScopeContext.readClaimByPath
- New super-role ADMIN ('ordinis:admin') — holder автоматически проходит
любой workflow check без явного назначения отдельных ролей. Convenience
для small teams.
Frontend (usePermissions.ts):
- tokenRoles() читает оба claim path (realm_access.roles +
resource_access.ordinis.roles) и объединяет — UI gate работает
независимо от backend config
- hasRole() recognizes 'ordinis:admin' как super-role override
- ROLE_ADMIN exported
Docs:
- docs/operator/rbac.md: добавлен ordinis:admin row, JWT claim path note,
Admin profile updated (super-role vs composite explanation)
152 lines
9.1 KiB
Markdown
152 lines
9.1 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'ам, и они автоматически получают все права.
|
||
|
||
### Назначение пользователю
|
||
|
||
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.
|