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

POST /api/v1/companies/my/users/{userId}/block — Блокировка пользователя компании

Перевод учётки сотрудника своей компании Active|Invited → Blocked с отзывом всех refresh-сессий (INV-S7 user-session.md). Обратная операция unblock — симметричная (см. «Совместимость»).

Назначение

Руководитель блокирует сотрудника (увольнение, инцидент): вход и все действующие сессии прекращаются, исторические данные (сделки, фиксации) сохраняются. Owning-контекст — identity. Feature group — companies.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (пользователь компании)
Auth policy / Permissionsidentity.users.block (дефолт: agency-owner; admin — через admin-API, ADR-0053)
Scope (RBAC)организация: Active RoleAssignment со scope Platform или Organization(CompanyId)
Record-levelцелевой пользователь обязан принадлежать компании актора (target.CompanyId == CompanyId)
Tenant isolationTenantId = CompanyId из claims; интерсептор INullableTenantedEntity дополнительно отсекает чужих
Auditupdate: UpdatedByUserId, Version, события UserBlockedKafkaEvent + SessionRevokedEvent — привилегированная операция, полностью аудируема
MFA/approvalне требуется (в границах организации); самоблокировка запрещена (шаг 2)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/companies/my/users/{userId}/block
Success status200
Idempotency headerне применяется — повтор по уже заблокированному возвращает IDENTITY_USER_INVALID_STATUS_TRANSITION (безопасно)
Correlationсквозной trace через заголовок traceparent

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

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

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

ПолеТипОбязательностьИсточникMaskingОписание
UserIdGuidДаUserEntity.IdКто заблокирован.
Statusstring (enum UserStatus)ДаUserEntity.StatusВсегда Blocked.
RevokedSessionsCountintДачисло отозванных RefreshSessionСколько сессий отозвано.
BlockedAtDateTimeOffsetДаUserEntity.UpdatedAtМомент блокировки.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь компании; tenant — его компания (CompanyId); требуется право identity.users.block в scope Organization(CompanyId).
  2. Проверка входа и предусловий.
    • UserId непустой, Reason ≤ 512 символов; иначе — ValidationError.
    • Самоблокировка запрещена: UserId == ActorUserIdIDENTITY_USER_SELF_BLOCK_FORBIDDEN.
    • Целевой пользователь существует и принадлежит компании актора (target.CompanyId == CompanyId); иначе — IDENTITY_USER_NOT_FOUND.
    • Статус пользователя допускает блокировку: Active или Invited; Blocked/ArchivedIDENTITY_USER_INVALID_STATUS_TRANSITION.
  3. Что читаем. Целевого пользователя (для проверок выше). Активные refresh-сессии в память не загружаются — отзываются массовым обновлением на шаге 5.
  4. Бизнес-правила и переходы. Пользователь переходит Active|Invited → Blocked: фиксируются момент и автор блокировки; поднимается доменный факт UserBlockedEvent. Назначения ролей (RoleAssignment) не отзываются — блокировка обратима, назначения ждут разблокировки; вход запрещается по статусу (инвариант IDENTITY_USER_BLOCKED, user.md).
  5. Что меняется. Обновляется учётка пользователя; все его активные refresh-сессии массово переводятся в Revoked с причиной AdminBlock (INV-S7), без загрузки в память; число затронутых строк → RevokedSessionsCount.
  6. Какие факты/события фиксируются.
    • UserBlockedKafkaEvent (топик pin.identity.users.v1, ключ UserId) — Id, BlockedAt, причина в виде enum-кода (без свободного текста);
    • SessionRevokedEvent — доменный факт на каждую логическую пачку отозванных сессий (лента расследований, user-session.md);
    • UserAccessChangedKafkaEvent не публикуется: назначения не изменились; потребители обязаны проверять Status пользователя из pin.identity.users.v1.
  7. Согласованность. Учётка, батч сессий и события фиксируются в одном согласованном изменении. Access-JWT, выданные до блокировки, живут до истечения (≤ 15 мин): на привилегированных операциях gateway дополнительно вызывает check-permission, который вернёт отказ по Status = Blocked.
  8. Результат. Идентификатор пользователя, статус Blocked, число отозванных сессий, момент блокировки. Кэш эффективных прав (perm:{userId}) инвалидируется после фиксации (best-effort).

Диаграмма

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

ТипДетали
Интеграционные событияUserBlockedKafkaEventpin.identity.users.v1 (потребители: crm, messenger, notifications, user-activity)
Проекции / кэшкэш эффективных прав perm:{userId} — инвалидация после фиксации (best-effort)
Уведомленияidentity сам не уведомляет; notifications может уведомить по событию

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Пустой UserId / длинный ReasonСписок ошибокno retry
IDENTITY_USER_SELF_BLOCK_FORBIDDEN409UserId == ActorUserId«Нельзя заблокировать себя»no retry
IDENTITY_USER_NOT_FOUND404Пользователь не существует / чужой компании«Пользователь не найден»no retry
IDENTITY_USER_INVALID_STATUS_TRANSITION409Status ∈ {Blocked, Archived}«Пользователь уже заблокирован или архивирован»no retry
IDENTITY_CONCURRENCY_CONFLICT409Конфликт оптимистичной блокировки«Повторите операцию»retry
FORBIDDEN403Нет identity.users.block«Недостаточно прав»no retry

Acceptance Criteria

  • Given сотрудник компании Active с активными сессиями, When руководитель с identity.users.block блокирует его, Then статус Active|Invited → Blocked, все refresh-сессии переходят Revoked (AdminBlock), ответ содержит RevokedSessionsCount и BlockedAt, публикуется UserBlockedKafkaEvent (key = UserId); назначения ролей НЕ отзываются (блокировка обратима).
  • Given UserId == ActorUserId, When block, Then IDENTITY_USER_SELF_BLOCK_FORBIDDEN (409).
  • Given целевой пользователь другой компании, When block, Then IDENTITY_USER_NOT_FOUND (404) — cross-tenant не раскрывается.
  • Given пользователь уже Blocked или Archived, When block, Then IDENTITY_USER_INVALID_STATUS_TRANSITION (409).
  • Given Reason длиннее 512 символов, When block, Then ValidationError (400).
  • Given пользователь заблокирован, у него живой access-JWT (≤ 15 мин), When привилегированная операция, Then gateway вызывает check-permission → отказ по Status = Blocked; вход любым способом запрещён немедленно.
  • Given UserAccessChangedKafkaEvent не публикуется (назначения не менялись), Then потребители сверяют Status пользователя из pin.identity.users.v1 (иначе краткий промежуток считают его активным).
  • Given нет identity.users.block в scope организации, When block, Then FORBIDDEN (403).

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

  • Обратная операция POST /api/v1/companies/my/users/{userId}/unblock — зеркальная: guard Blocked → Active, событие UserUpsertedKafkaEvent (snapshot со Status = Active), сессии не восстанавливаются. Документируется тем же контрактом (дельта — только переход).
  • Logs: ActorUserId, UserId, RevokedSessionsCount (без Reason в общих логах — только audit); metrics: identity_user_block_total{result}; traces: OTel; dashboard identity-overview; runbook: массовый отзыв сессий — README identity.

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

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

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