From c41e34545b7a23e24c24196c06a7889724f63338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=97=D0=B8=D0=BC=D0=B8=D0=BD?= Date: Wed, 13 May 2026 16:41:03 +0000 Subject: [PATCH] =?UTF-8?q?docs(rbac):=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20RBAC=20sections=20=D0=BF=D0=BE=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=20Phase=201d=20+=20Phase=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/integration/api/schema-workflow.md | 40 ++++++++++++++++++++----- docs/operator/rbac.md | 17 +++++++++++ 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/docs/integration/api/schema-workflow.md b/docs/integration/api/schema-workflow.md index 887807f..de3ca46 100644 --- a/docs/integration/api/schema-workflow.md +++ b/docs/integration/api/schema-workflow.md @@ -407,14 +407,38 @@ curl -X POST /api/v1/dictionaries/mission/drafts/8f3a.../publish -d '{ | 403 | `maker_only` | submit/withdraw от чужого actor'а | | 422 | `breaking_change_requires_draft` | inline PATCH вернул это — переходи в workflow | -## RBAC (TBD) +## RBAC -В текущей реализации workflow доступен любому authenticated user'у через -network-level auth policy. Когда Keycloak roles claim прорастёт в JWT, -будут отдельные требования: +Phase 1d (2026-05-13) — backend enforced. Реализация в [`WorkflowRoles`](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/blob/main/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/auth/WorkflowRoles.java). Полная матрица — [`docs/operator/rbac.md`](../../operator/rbac.md). -- `dict_schema_maker` — для create / submit / withdraw -- `dict_schema_reviewer` — для decision -- `dict_schema_publisher` — для publish +| Действие | Workflow role | +|---|---| +| create / submit / update / withdraw schema draft | — (maker — любой authenticated с scope ACL) | +| decide (approve / request_changes / reject) | `ordinis:schema:reviewer` | +| publish (approved → live) | `ordinis:schema:publisher` | +| Super-role для всех workflow actions | `ordinis:admin` | -См. record-level `DraftController` для reference architecture pool model. +Naming convention — colon-separated `ordinis::`, consistent +со scope ACL (`ordinis:client:public/internal/restricted`). + +**Phase 2 (2026-05-13) — per-dict минимальная роль ревьюера.** Поле +`approvalMinRole` на `DictionaryDefinition` (set'ится через +`DictionaryEditorDialog` или import CUOD-bundle манифеста). Если задано — +backend требует чтобы reviewer имел эту роль **дополнительно** к +стандартной `schema:reviewer` / `schema:publisher`. `ordinis:admin` +проходит автоматически. Use case: критичный справочник +(`approvalMinRole = "ordinis:restricted"`) — approve может только user с +restricted-scope role'ю. + +**JWT claim path:** на staging/prod — `resource_access.ordinis.roles` +(client-scoped Keycloak roles), default fallback `realm_access.roles`. +Configurable через `ORDINIS_AUTH_ROLES_CLAIM`. `usePermissions.ts` на +frontend читает оба пути. + +**Maker-checker invariant:** maker (creator) не может decide свой +собственный draft — 409 `self_approve_forbidden`. Backend rule, не +обходится через admin role. + +См. также: +- [`docs/operator/rbac.md`](../../operator/rbac.md) — полная матрица ролей + setup в Keycloak (включая composite `ordinis:admin` role и mapper). +- `DraftController` (record drafts) — reference architecture pool model с теми же gates. diff --git a/docs/operator/rbac.md b/docs/operator/rbac.md index 6966b63..d6ba6b2 100644 --- a/docs/operator/rbac.md +++ b/docs/operator/rbac.md @@ -112,6 +112,22 @@ Composite-роль в Keycloak автоматически expands в JWT — tok После этого можно назначать одну роль `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**. @@ -149,3 +165,4 @@ Webhook subscriptions требуют `internal` или `restricted` scope. На - **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.