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

DELETE /api/admin/v1/users/{userId} — Удаление пользователя: архив vs полное удаление (admin)

Назначение

Двухрежимное административное удаление учётки (ADR-0053: «архивное vs полное удаление»):

  • Archive — «архивное удаление»: soft-delete (Status = Archived, DeletedAt), исторические ссылки (сделки, фиксации, реферальные связи) живы, данные восстановимы (unarchive — отдельный регламент заботы);
  • Purge — «полное удаление» (право на забвение / регламент PII): анонимизация PII-полей уже архивированной учётки. Строка НЕ удаляется физически (на UserId ссылаются crm/referral/ messenger — FK-целостность и агрегаты сделок обязаны жить), но все персональные данные затираются безвозвратно, а потребители перезаписывают свои реплики анонимизированным снапшотом.
Purge необратим

Анонимизация PII при Purge необратима: восстановить PhoneNumber/Email/Name исходной учётки невозможно — в отличие от Archive, который допускает unarchive.

Metadata

ПолеЗначение
Сервис/контекстidentity
API areaadmin (ADR-0053)
Feature groupadmin-users
Статусdraft
Документdocs/06-services/identity/api/admin/delete-user.md

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

ПроверкаЗначение
Actor typeExternalSystem (система администрирования; m2m client-credentials)
Auth policy / Permissionspolicy AdminSystemOnly; для Mode = Purge дополнительно policy AdminPrivilegedOnly (в системе администрирования операция требует MFA/второго approve — граница admin-identity, ADR-0053)
Scope (RBAC)платформа (кросс-tenant)
Tenant isolationbypass tenant-фильтров под system-scope
Auditобязателен, повышенный: deep audit + отдельная запись PII-erasure (кто, когда, основание LegalBasis)

HTTP-контракт

ПолеЗначение
MethodDELETE
Route`/api/admin/v1/users/{userId}?mode={Archive
Success status200
Idempotency headerIdempotency-Key (Guid; обязателен)
CorrelationOTel trace (traceparent); X-Admin-User-Id — обязателен

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdrouteGuidДа!= Guid.Empty
Modequery modeUserDeletionMode (enum-string)Да∈ {Archive, Purge}
LegalBasisbodystring?Обязателен при Mode = Purge≤ 512; основание удаления (запрос субъекта данных / регламент retention)только audit
Commentbodystring?Нет≤ 1024только audit
AdminUserIdheader X-Admin-User-IdGuidДа!= Guid.Emptyaudit-stamping
IdempotencyKeyheader Idempotency-KeyGuidДа!= Guid.Empty

Enum UserDeletionMode

ЗначениеСемантикаПредусловие
ArchiveSoft-delete: Status → Archived, DeletedAt = now, отзыв сессий и назначений; PII сохраняетсяStatus ∈ {Active, Invited, Blocked}
PurgeАнонимизация PII архивированной учётки (необратимо)Status = Archived и DeletedAt <= now − 30 дней (регламентный карантин; исключение — явный LegalBasis запроса субъекта данных, тогда карантин не применяется)

Правила анонимизации Purge (поле → значение)

Поле UserEntityЗначение после purge
PhoneNumberpurged:{UserId} (уникальный плейсхолдер — уникальный индекс не нарушается; E.164-вход по нему невозможен)
PhoneNumberConfirmedAt, Email, EmailConfirmedAtnull
Name (owned FullName)FirstName = "Удалённый", LastName = "Пользователь", MiddleName = null
PasswordHash, PinCodeHashnull; IsTwoFactorEnabled = false
ReferrerUrlnull
LastSignInAt, LockoutUntil, FailedAccessAttemptsnull / null / 0
Связанные записиSignInConfirmationRequest пользователя удаляются физически (содержат телефон/IP/UA); у RefreshSession затирается Device (UserAgent/IpAddress/DisplayNamenull), статусы сохраняются для аудита
Поля, которые Purge не затирает

Id, Type, CompanyId, Status = Archived, RegisteredVia, audit-штампы, Version — сохраняются (неперсональные, нужны истории сделок).

Talent-managed учётки

Для SyncSource = TalentIdentity purge запрещён, пока Talent не прислал UserDeletedEvent (источник PII — Talent; см. IDENTITY_USER_PURGE_SYNC_MANAGED ниже).

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

ПолеТипОбязательностьИсточникMaskingОписание
UserIdGuidДаUserEntity.IdПользователь.
Modestring (enum UserDeletionMode)ДакомандаПрименённый режим.
Statusstring (enum UserStatus)ДаUserEntity.StatusИтоговый статус (Archived).
RevokedSessionsCountintДабатч-отзывОтозвано сессий (0 при Purge — уже отозваны архивом).
RevokedAssignmentsCountintДабатч-отзывОтозвано назначений ролей.
PiiErasedboolДавычислениеtrue только при Purge.
VersionlongДаUserEntity.VersionВерсия после мутации.

Алгоритм

  1. Контекст и доступ. Операцию выполняет система администрирования (m2m-клиент pin-admin-system, policy AdminSystemOnly; при Mode = Purge дополнительно AdminPrivilegedOnly). Повтор с тем же Idempotency-Key (admin:user-delete:{IdempotencyKey}) возвращает сохранённый результат.
  2. Проверка входа и предусловий. Mode ∈ {Archive, Purge}; при Purge обязателен LegalBasis — иначе ValidationError (400). Пользователь UserId существует — иначе IDENTITY_USER_NOT_FOUND (404). Допустимость состояния по режиму:
    • Archive — статус ∈ {Active, Invited, Blocked}; если уже Archived — идемпотентный успех без изменений;
    • Purge — статус Archived, иначе IDENTITY_USER_NOT_ARCHIVED (409); карантин DeletedAt <= now − 30 дней либо LegalBasis типа запроса субъекта данных, иначе IDENTITY_USER_PURGE_QUARANTINE (409); учётка не под управлением Talent (SyncSource != TalentIdentity) либо Talent уже прислал UserDeletedEvent, иначе IDENTITY_USER_PURGE_SYNC_MANAGED (409). Активные сделки пользователя — предупреждение, а не блокер (stateDiagram user.md; crm получит событие).
  3. Бизнес-правила и переходы.
    • Archive: учётка переводится в Archived, проставляется DeletedAt; актором удаления записывается служебный actor admin-API (Guid администратора — в audit); фиксируется вид изменения «архивирование».
    • Purge: применяется таблица анонимизации PII выше; статус остаётся Archived (перехода нет); фиксируется вид изменения «pii-erasure».
  4. Что меняется.
    • Archive: статус учётки → Archived; все активные сессии отзываются (Active → Revoked, RevokedReason = AdminBlock); все активные назначения ролей отзываются (Active → Revoked с RevokedAt), полная история назначений сохраняется (аудит доступов, role.md).
    • Purge: одноразовые запросы кодов SignInConfirmationRequest удаляются физически; у сессий затирается слепок устройства (Device → пусто), статусы сохраняются для аудита; PII-поля учётки затираются по таблице анонимизации. Порядок изменения — учётка, затем дочерние записи (целостность связей). Правка не должна затирать конкурентное изменение (CONCURRENCY_CONFLICT).
  5. События.
    • ArchiveUserUpsertedKafkaEvent со Status = Archived (контракт) и UserAccessChangedKafkaEvent (ChangeReason = AssignmentRevoked, ActiveAssignments = [], контракт);
    • PurgeUserUpsertedKafkaEvent с анонимизированным снапшотом (Name = «Удалённый Пользователь», PhoneNumber = purged:{UserId}) — потребители обязаны перезаписать свои PII-реплики (правило распространения erasure). Мутация учётки, дочерние изменения, отметка идемпотентности, audit (включая запись PII-erasure) и публикация событий выполняются в одном согласованном изменении.
  6. Результат. AdminDeleteUserResult: UserId, Mode, итоговый Status = Archived, RevokedSessionsCount, RevokedAssignmentsCount, PiiErased, Version.

Диаграмма

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

ТипДетали
Integration eventsUserUpsertedKafkaEventpin.identity.users.v1; при Archive также UserAccessChangedKafkaEventpin.identity.access.v1 (оба key = UserId)
External callsnone
Cache / projections / searchпроекция UserSearchDocument (../../search/users.md): для company-user учётки документ вырезается (IsActive = false) при Archive и удаляется по UserId при Purge/выходе из компании; инвестор в проекции отсутствует. Кэши прав — событием access
Notificationsnone (notifications останавливает рассылки по снапшоту Archived)

Окно согласованности: identity меняет статус/анонимизирует PII атомарно; потребители применяют изменение асинхронно по событиям. При Archive — crm/referral/messenger видят Status = Archived из UserUpsertedKafkaEvent и снимают доступ по UserAccessChangedKafkaEvent (секунды при штатном лаге). При Purge распространение erasure обязательно: каждый потребитель перезаписывает свою PII-реплику анонимизированным снапшотом; до применения события реплика краткий промежуток хранит прежние PII — контроль полноты перезаписи ведётся по runbook «PII-erasure и реплики потребителей» (алерт при лаге

5 мин). Исторические агрегаты сделок/фиксаций (UserId) остаются целыми в обоих режимах.

Acceptance Criteria

  • Given учётка Active, When DELETE ?mode=Archive, Then статус → Archived, DeletedAt = now, все активные сессии и назначения ролей отозваны, ответ PiiErased = false, публикуются UserUpsertedKafkaEvent (Status = Archived) и UserAccessChangedKafkaEvent (ActiveAssignments = []); полная история назначений сохранена.
  • Given учётка уже Archived, When mode=Archive, Then идемпотентный 200 без изменений.
  • Given учётка Archived с DeletedAt 45 дней назад, privileged-подтверждение и LegalBasis, When mode=Purge, Then PII затирается по таблице анонимизации, статус остаётся Archived, PiiErased = true, SignInConfirmationRequest удаляются физически, у сессий затирается слепок устройства; публикуется UserUpsertedKafkaEvent с анонимизированным снапшотом; потребители перезаписывают PII-реплики.
  • Given учётка Active, When mode=Purge, Then IDENTITY_USER_NOT_ARCHIVED (409) — сначала требуется Archive.
  • Given учётка Archived 5 дней назад без основания субъекта данных, When mode=Purge, Then IDENTITY_USER_PURGE_QUARANTINE (409).
  • Given Talent-managed учётка (SyncSource = TalentIdentity) без UserDeletedEvent от Talent, When mode=Purge, Then IDENTITY_USER_PURGE_SYNC_MANAGED (409).
  • Given mode=Purge без LegalBasis, When запрос, Then ValidationError (400).
  • Given mode=Purge без privileged-подтверждения на стороне системы администрирования, When запрос, Then FORBIDDEN (403).
  • Given повтор с тем же Idempotency-Key, When запрос по таймауту, Then сохранённый результат (replay), дочерние изменения не повторяются.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Mode вне enum / LegalBasis отсутствует при PurgeОшибка валидации запросаno retry
FORBIDDEN403Нет policy (Purge без AdminPrivilegedOnly)Доступ запрещёнno retry
IDENTITY_USER_NOT_FOUND404Пользователь не существуетПользователь не найденno retry
IDENTITY_USER_NOT_ARCHIVED409Purge не из ArchivedПолное удаление возможно только из архиваno retry (сначала Archive)
IDENTITY_USER_PURGE_QUARANTINE409Карантин 30 дней не истёк и нет основания субъектаНе истёк карантин архивного удаленияno retry до истечения
IDENTITY_USER_PURGE_SYNC_MANAGED409Talent-реплика без UserDeletedEvent от TalentИсточник данных — Talent Identity; удаление инициируется тамno retry
CONCURRENCY_CONFLICT409Гонка версийПовторите операциюretry с тем же Idempotency-Key

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

  • Новые optional поля — без breaking; изменение правил анонимизации — только расширение списка затираемых полей (сужение — breaking для комплаенса, требует ADR).
  • Logs: admin-user-archived / admin-user-purged (userId, mode, adminUserId; PII не логируется); metrics: identity_admin_operations_total{operation="delete-user",mode}, identity_pii_erasures_total; traces — OTel; deep audit + PII-erasure-журнал (retention бессрочный). Runbook: «PII-erasure и реплики потребителей» (контроль перезаписи снапшота).

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

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

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