Перейти к основному содержимому

PUT /api/v1/roles/{roleId}/permissions — Настройка permissions роли

Полная замена состава прав роли компании (RolePermission): клиент присылает целевой набор PermissionIds, система вычисляет дельту grant/revoke и рассылает UserAccessChangedKafkaEvent всем пользователям с активными назначениями этой роли.

Назначение

Руководитель настраивает, что может делать роль («может публиковать новости, но не может редактировать проект»). Owning-контекст — identity. Feature group — roles.

Почему PUT, а не grant/revoke

UI ролей работает чекбоксами по каталогу прав. PUT-семантика (declarative set) — клиент присылает весь целевой набор PermissionIds, сервер сам вычисляет дельту — атомарна и идемпотентна, поэтому выбрана вместо пары grant/revoke-эндпоинтов.

Metadata

ПолеЗначение
Сервис/контекстidentity
API areapublic
Feature grouproles
Статусdraft
Документdocs/06-services/identity/api/company-user/set-role-permissions.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (пользователь компании)
Auth policy / Permissionsidentity.roles.manage
Scope (RBAC)организация: Active RoleAssignment со scope Platform или Organization(CompanyId)
Record-levelроль обязана принадлежать компании актора
Tenant isolationTenantId = CompanyId
Auditupdate: RolePermission.CreatedByUserId на добавленных, RoleUpdatedEvent с дельтой, UserAccessChangedKafkaEvent fan-out
MFA/approvalне требуется; эскалация прав невозможна by-design: в набор допускаются только IsAssignableByCompanies = true (INV-P4) — платформенные права компания включить не может

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/v1/roles/{roleId}/permissions
Success status200
Idempotency headerне применяется (PUT declarative set идемпотентен)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДаRequired
CompanyIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
RoleIdroute {roleId}GuidДа!= Guid.Empty
PermissionIdsbodyIReadOnlyList<Guid>Да (пустой = снять все права)<= 113, без дубликатов; каждый — существующий, IsAssignableByCompanies = true, IsDeprecated = false (INV-R7, INV-P4)
VersionbodylongДа>= 1; сверка с RoleDefinition.Version

Модель ответа SetRolePermissionsCommandResult

ПолеТипОбязательностьИсточникMaskingОписание
RoleIdGuidДаRoleDefinition.IdId роли.
GrantedCodesIReadOnlyList<string>Дадельта: добавленные PermissionDefinition.CodeЧто добавлено.
RevokedCodesIReadOnlyList<string>Дадельта: удалённые кодыЧто снято.
PermissionCodesIReadOnlyList<string>Даитоговый составПолный целевой набор.
AffectedUsersCountintДачисло пользователей с Active-назначениями ролиСкольким пересчитаны права.
VersionlongДаRoleDefinition.VersionНовая версия роли.

Предусловия и постусловия

Предусловия. Актор аутентифицирован и имеет identity.roles.manage в scope Organization(CompanyId); роль существует, принадлежит компании актора, не системная (INV-R1) и Active; переданная Version совпадает с текущей версией роли; все PermissionIds — существующие, назначимые компаниями и не устаревшие.

Постусловия при успехе. Состав прав роли равен целевому PermissionIds; версия роли увеличена, зафиксирован автор; на каждого пользователя с активным назначением этой роли пересчитаны эффективные права и опубликовано UserAccessChangedKafkaEvent (при > 100 затронутых — через background-джобу батчами по 100); кэши perm:{userId} затронутых инвалидированы (best-effort). При пустой дельте изменений и событий нет.

Постусловия при отказе. Состав прав и версия роли не изменились, события не публикуются.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь компании; tenant — его компания (CompanyId); требуется право identity.roles.manage в scope Organization(CompanyId). Эскалация прав невозможна by-design: в набор допускаются только права с IsAssignableByCompanies = true (INV-P4).
  2. Проверка входа и предусловий.
    • Синтаксис: лимит и отсутствие дубликатов в PermissionIds; иначе — ValidationError.
    • Роль существует и принадлежит компании актора; иначе — IDENTITY_ROLE_NOT_FOUND.
    • Роль не системная (INV-R1: состав прав системных ролей меняет только платформа); системная — IDENTITY_ROLE_SYSTEM_IMMUTABLE.
    • Роль в статусе Active; ArchivedIDENTITY_ROLE_ARCHIVED.
    • Все переданные права существуют в SEED-каталоге (INV-R7); отсутствующие → IDENTITY_PERMISSION_NOT_FOUND.
    • Каждое право назначимо компаниями и не устарело (INV-P4): IsAssignableByCompanies = true, IsDeprecated = false; иначе — IDENTITY_PERMISSION_NOT_ASSIGNABLE (в ответе — коды-нарушители).
    • Оптимистичная блокировка: переданная Version совпадает с версией роли; иначе — IDENTITY_CONCURRENCY_CONFLICT.
  3. Что читаем. Роль с текущим составом прав; каталог прав (in-memory SEED). При непустой дельте — идентификаторы пользователей с активными назначениями этой роли (для fan-out).
  4. Бизнес-правила и переходы. Вычисляется дельта относительно текущего состава: toGrant = целевые \ текущие, toRevoke = текущие \ целевые. Пустая дельта → короткий путь: результат с пустыми списками, без изменений и событий. Иначе: добавляются привязки прав toGrant (с автором), удаляются привязки toRevoke; версия роли увеличивается, фиксируется автор изменения. Поднимается доменный факт RoleUpdatedEvent с дельтой.
  5. Что меняется. Обновляются роль и её привязки прав (добавление/удаление). Конкурентный конфликт → IDENTITY_CONCURRENCY_CONFLICT.
  6. Какие факты/события фиксируются. На каждого пользователя с активным назначением роли публикуется UserAccessChangedKafkaEvent (топик pin.identity.access.v1, ключ UserId) с пересобранным списком активных назначений {RoleCode, ScopeType, ScopeIds} и версией. При более чем 100 затронутых пользователях fan-out делегируется background-джобе батчами по 100: вместо N событий фиксируется задача (role.md «События»).
  7. Согласованность. Роль, её привязки прав и события (или маркер джобы) фиксируются в одном согласованном изменении.
  8. Результат. Идентификатор роли, дельта (добавленные/снятые коды), итоговый состав прав, число затронутых пользователей и новая версия (200). После фиксации — best-effort инвалидация кэша perm:{userId} затронутых.

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событияUserAccessChangedKafkaEvent ×N → pin.identity.access.v1 (сброс кэшей авторизации у crm/catalog/chessboard/gateway)
Проекции / кэшкэш perm:{userId} затронутых — инвалидация после фиксации

Интеграционная последовательность и согласованность

Изменение состава прав роли затрагивает всех её носителей — это fan-out:

  1. Вычисляется дельта toGrant/toRevoke. Пустая дельта → короткий путь: результат с пустыми списками, без изменений и без событий (идемпотентность повторного PUT того же набора).
  2. При непустой дельте состав прав и версия роли фиксируются; определяется множество пользователей с активными назначениями роли.
  3. На каждого затронутого публикуется UserAccessChangedKafkaEvent → топик pin.identity.access.v1, ключ UserId (упорядоченность на пользователя — по ключу). При числе затронутых ≤ 100 события уходят в том же согласованном изменении; при > 100 фиксируется задача, и фактический fan-out выполняет background-джоба батчами по 100 (защита от «толстой» транзакции).
  4. Потребители авторизации (crm, catalog, chessboard, gateway) по каждому событию сбрасывают кэш доступа соответствующего пользователя.
Eventual consistency при fan-out

Окно eventual-consistency тем шире, чем больше носителей роли (крупные роли — через джобу); на время рассылки разные пользователи могут увидеть новые права роли неодновременно. Отказоустойчивость: событие фиксируется атомарно с изменением состава прав (не теряется при сбое отправки), недоставленные — ретраятся; исчерпавшие ретраи уходят в DLQ и переигрываются runbook-ом «access fan-out replay» (наблюдаемость — метрика identity_access_fanout_size, алерт на лаг/DLQ). Повтор всей операции безопасен: PUT — declarative set, тот же набор даёт пустую дельту.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Лимит/дубликаты PermissionIdsСписок ошибокno retry
IDENTITY_ROLE_NOT_FOUND404Роль не существует / чужая«Роль не найдена»no retry
IDENTITY_ROLE_SYSTEM_IMMUTABLE409IsSystem = true (INV-R1)«Состав прав системной роли меняется только платформой»no retry
IDENTITY_ROLE_ARCHIVED409Status = Archived«Роль архивирована»no retry
IDENTITY_PERMISSION_NOT_FOUND400Id вне SEED-каталога (INV-R7)«Право не найдено»no retry
IDENTITY_PERMISSION_NOT_ASSIGNABLE409Платформенное/deprecated право (INV-P4)«Право недоступно для ролей компании» + кодыno retry
IDENTITY_CONCURRENCY_CONFLICT409Версия роли изменилась параллельно«Данные изменены параллельно»retry после перечитывания
FORBIDDEN403Нет identity.roles.manage«Недостаточно прав»no retry

Совместимость и наблюдаемость

Совместимость

Контракт PUT-набора стабилен; появление deprecated-права в существующем составе не ломает роль (permission.md «Совместимость»: работает до миграции), но повторный PUT обязан его исключить.

  • Logs: RoleId, размер дельты, AffectedUsersCount; metrics: identity_role_permissions_set_total{result}, identity_access_fanout_size; traces: OTel; dashboard identity-overview; alert: DLQ/лаг fan-out; runbook: access fan-out replay.

Acceptance Criteria

  • Given роль компании Active с набором прав {A, B} и актуальная Version, When PUT с PermissionIds = {B, C}, Then 200; GrantedCodes = {C}, RevokedCodes = {A}, PermissionCodes = {B, C}, версия увеличена; на носителей роли опубликован UserAccessChangedKafkaEvent.
  • Given целевой набор равен текущему, When PUT, Then 200 с пустыми GrantedCodes/RevokedCodes; изменений и событий нет (идемпотентно).
  • Given PermissionIds = [], When PUT, Then все текущие права сняты, RevokedCodes = прежний состав, PermissionCodes = [].
  • Given роль IsSystem = true, When PUT, Then IDENTITY_ROLE_SYSTEM_IMMUTABLE (409) — INV-R1.
  • Given роль другой компании, When PUT, Then IDENTITY_ROLE_NOT_FOUND (404).
  • Given переданная Version не совпадает с текущей, When PUT, Then IDENTITY_CONCURRENCY_CONFLICT (409); состав прав не изменён.
  • Given в наборе право с IsAssignableByCompanies = false или IsDeprecated = true, When PUT, Then IDENTITY_PERMISSION_NOT_ASSIGNABLE (409) с кодами-нарушителями.
  • Given в наборе id вне SEED-каталога, When PUT, Then IDENTITY_PERMISSION_NOT_FOUND (400) — INV-R7.
  • Given роль назначена > 100 пользователям и дельта непуста, When PUT, Then 200 немедленно, AffectedUsersCount отражает число носителей, рассылка UserAccessChangedKafkaEvent делегируется background-джобе батчами по 100.
  • Given роль Archived, When PUT, Then IDENTITY_ROLE_ARCHIVED (409).

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы