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

POST /api/v1/role-assignments/{assignmentId}/revoke — Отзыв назначения роли

Перевод RoleAssignment Active → Revoked (stateDiagram role.md). Запись не удаляется — полная история назначений сохраняется для аудита доступов (role.md «Audit»).

Назначение

Руководитель снимает с сотрудника роль (смена обязанностей, сужение доступа). Права пересчитываются немедленно; consumers получают UserAccessChangedKafkaEvent. Owning-контекст — identity. Feature group — role-assignments.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (пользователь компании)
Auth policy / Permissionsidentity.role-assignments.manage
Scope (RBAC)INV-R8: permission актора обязан покрывать scope отзываемого назначения (иерархия Platform ⊃ Organization ⊃ ProjectGroup ⊃ Project)
Record-levelназначение обязано принадлежать компании актора (assignment.CompanyId == CompanyId)
Tenant isolationTenantId = CompanyId; платформенные назначения (CompanyId = null) недостижимы этим методом — только администратор платформы (ADR-0053)
Auditupdate: RevokedByUserId = actor, RevokedAt; запись остаётся навсегда
MFA/approvalне требуется; защита от самоликвидации последнего админа (шаг 2)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/role-assignments/{assignmentId}/revoke
Success status200
Idempotency headerне применяется — повтор по Revoked возвращает IDENTITY_ASSIGNMENT_ALREADY_REVOKED
Correlationсквозной trace через заголовок traceparent
Почему POST, а не DELETE

Отзыв — доменный переход статуса с причиной и аудитом, а не удаление ресурса, поэтому семантика выбрана POST-действием, а не DELETE /role-assignments/{id}.

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДаRequired
CompanyIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
AssignmentIdroute {assignmentId}GuidДа!= Guid.Empty
Reasonbodystring?Нет<= 512; свободный текст для аудитане отдаётся наружу

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

ПолеТипОбязательностьИсточникMaskingОписание
AssignmentIdGuidДаRoleAssignment.IdId назначения.
UserIdGuidДаRoleAssignment.UserIdУ кого отозвано.
Statusstring (enum RoleAssignmentStatus)ДаRoleAssignment.StatusВсегда Revoked.
RevokedAtDateTimeOffsetДаRoleAssignment.RevokedAtМомент отзыва.

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

Предусловия. Актор аутентифицирован и имеет identity.role-assignments.manage в scope, покрывающем scope отзываемого назначения (INV-R8); назначение существует, принадлежит компании актора и в статусе Active; отзыв не оставляет организацию без единственного администратора (guard последнего админа).

Постусловия при успехе. Назначение переведено Active → Revoked (запись не удаляется — история сохраняется), зафиксированы момент, автор и причина; опубликовано UserAccessChangedKafkaEvent с пересобранным списком оставшихся активных назначений; кэш perm:{UserId} инвалидирован. Эффективные права пользователя сузились немедленно в источнике и eventually — у потребителей.

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

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь компании; tenant — его компания (CompanyId); требуется право identity.role-assignments.manage. По INV-R8 это право актора должно покрывать scope отзываемого назначения по иерархии Platform ⊃ Organization ⊃ ProjectGroup ⊃ Project (актор с manage только Project(X) не может отозвать назначение scope Organization); нет покрытия → FORBIDDEN.
  2. Проверка входа и предусловий.
    • AssignmentId непустой, Reason ≤ 512 символов; иначе — ValidationError.
    • Назначение существует и принадлежит компании актора (assignment.CompanyId == CompanyId); чужое/платформенное → IDENTITY_ASSIGNMENT_NOT_FOUND.
    • Назначение в статусе Active; RevokedIDENTITY_ASSIGNMENT_ALREADY_REVOKED; ExpiredIDENTITY_ASSIGNMENT_EXPIRED.
    • Защита последнего админа: если актор отзывает у себя (assignment.UserId == ActorUserId) последний источник identity.role-assignments.manage в организации → IDENTITY_ROLE_LAST_ADMIN_PROTECTION (вычисляется по оставшимся назначениям, шаг 3).
  3. Что читаем. Назначение; его роль (для RoleCode в payload события); оставшиеся активные назначения пользователя (для payload события и guard-а последнего админа).
  4. Бизнес-правила и переходы. Назначение переходит Active → Revoked с причиной ManualRevoke: фиксируются момент и автор отзыва. Поднимается доменный факт RoleAssignmentRevokedEvent.
  5. Что меняется. Обновляется запись назначения (не удаляется — история сохраняется). Конкурентный конфликт (параллельный отзыв/экспирация джобой) → IDENTITY_CONCURRENCY_CONFLICT.
  6. Какие факты/события фиксируются. UserAccessChangedKafkaEvent (топик pin.identity.access.v1, ключ UserId) — payload: UserId, CompanyId, список оставшихся активных назначений (уже без отозванного) как {RoleCode, ScopeType, ScopeIds}, версия.
  7. Согласованность. Назначение и событие фиксируются в одном согласованном изменении.
  8. Результат. Идентификатор назначения, пользователь, статус Revoked, момент отзыва (200). После фиксации — инвалидация кэша perm:{UserId}.

Диаграмма

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

ТипДетали
Интеграционные событияUserAccessChangedKafkaEventpin.identity.access.v1, key = UserId
Проекции / кэшкэш perm:{UserId} — инвалидация после фиксации

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

После фиксации перехода Active → Revoked:

  1. identity инвалидирует perm:{UserId} — self-запрос прав пользователя сразу не показывает отозванную роль.
  2. Асинхронно публикуется UserAccessChangedKafkaEvent → топик pin.identity.access.v1, ключ UserId — payload содержит список оставшихся активных назначений (без отозванного).
  3. Потребители авторизации (crm, catalog, chessboard, gateway) сбрасывают свою копию по этому пользователю; после обработки отзыв действует на их enforcement-точках.
Окно eventual-consistency

Окно между шагами 1→3 — секунды. Риск промежутка: до обработки события downstream-сервис может краткий момент всё ещё пропускать действие по уже отозванному праву (устаревший кэш авторизации) — это допустимо для целевой нагрузки; критичные операции проверяют доступ на источнике/по свежему событию. Обратной несогласованности (право уже вернулось у потребителя, но ещё активно в источнике) не бывает — источник меняется первым.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Пустой id / длинный ReasonСписок ошибокno retry
IDENTITY_ASSIGNMENT_NOT_FOUND404Назначение не существует / чужое / платформенное«Назначение не найдено»no retry
IDENTITY_ASSIGNMENT_ALREADY_REVOKED409Status = Revoked«Назначение уже отозвано»no retry (идемпотентный исход)
IDENTITY_ASSIGNMENT_EXPIRED409Status = Expired«Назначение истекло»no retry
IDENTITY_ROLE_LAST_ADMIN_PROTECTION409Актор лишает себя последнего manage-права«Нельзя отозвать собственное последнее административное назначение»no retry
IDENTITY_CONCURRENCY_CONFLICT409Конфликт оптимистичной блокировки«Повторите операцию»retry
FORBIDDEN403INV-R8 — scope актора не покрывает scope назначения«Недостаточно прав для этой области действия»no retry

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

  • Контракт стабилен; enum-причина в событии — additive-расширяемая (unknown → «прочее» у consumers).
  • Logs: ActorUserId, AssignmentId, UserId; metrics: identity_assignment_revoke_total{result}; traces: OTel; dashboard identity-overview; runbook — не применяется.

Acceptance Criteria

  • Given активное назначение своей компании и актор с покрывающим identity.role-assignments.manage, When POST /revoke, Then 200; назначение Revoked, RevokedAt заполнен, запись сохранена; опубликован UserAccessChangedKafkaEvent с оставшимися назначениями; perm:{UserId} инвалидирован.
  • Given назначение уже Revoked, When повторный POST /revoke, Then IDENTITY_ASSIGNMENT_ALREADY_REVOKED (409).
  • Given назначение Expired, When POST /revoke, Then IDENTITY_ASSIGNMENT_EXPIRED (409).
  • Given актор с manage только в Project(X), When отзывает назначение scope Organization, Then FORBIDDEN (403) — INV-R8.
  • Given назначение другой компании или платформенное (CompanyId = null), When POST /revoke, Then IDENTITY_ASSIGNMENT_NOT_FOUND (404).
  • Given актор отзывает у себя последний источник identity.role-assignments.manage в организации, When POST /revoke, Then IDENTITY_ROLE_LAST_ADMIN_PROTECTION (409); назначение остаётся Active.
  • Given параллельно джоба переводит назначение в Expired, When POST /revoke проигрывает гонку версий, Then IDENTITY_CONCURRENCY_CONFLICT (409) с рекомендацией повторить.
  • Given задан Reason, When POST /revoke, Then причина сохраняется для аудита и наружу в ответе не отдаётся.

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

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

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