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-целостность и агрегаты сделок обязаны жить), но все персональные данные затираются безвозвратно, а потребители перезаписывают свои реплики анонимизированным снапшотом.
Анонимизация PII при Purge необратима: восстановить PhoneNumber/Email/Name исходной учётки
невозможно — в отличие от Archive, который допускает unarchive.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | admin (ADR-0053) |
| Feature group | admin-users |
| Статус | draft |
| Документ | docs/06-services/identity/api/admin/delete-user.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | ExternalSystem (система администрирования; m2m client-credentials) |
| Auth policy / Permissions | policy AdminSystemOnly; для Mode = Purge дополнительно policy AdminPrivilegedOnly (в системе администрирования операция требует MFA/второго approve — граница admin-identity, ADR-0053) |
| Scope (RBAC) | платформа (кросс-tenant) |
| Tenant isolation | bypass tenant-фильтров под system-scope |
| Audit | обязателен, повышенный: deep audit + отдельная запись PII-erasure (кто, когда, основание LegalBasis) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | DELETE |
| Route | `/api/admin/v1/users/{userId}?mode={Archive |
| Success status | 200 |
| Idempotency header | Idempotency-Key (Guid; обязателен) |
| Correlation | OTel trace (traceparent); X-Admin-User-Id — обязателен |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | route | Guid | Да | != Guid.Empty | — |
Mode | query mode | UserDeletionMode (enum-string) | Да | ∈ {Archive, Purge} | — |
LegalBasis | body | string? | Обязателен при Mode = Purge | ≤ 512; основание удаления (запрос субъекта данных / регламент retention) | только audit |
Comment | body | string? | Нет | ≤ 1024 | только audit |
AdminUserId | header X-Admin-User-Id | Guid | Да | != Guid.Empty | audit-stamping |
IdempotencyKey | header Idempotency-Key | Guid | Да | != Guid.Empty | — |
Enum UserDeletionMode
| Значение | Семантика | Предусловие |
|---|---|---|
Archive | Soft-delete: Status → Archived, DeletedAt = now, отзыв сессий и назначений; PII сохраняется | Status ∈ {Active, Invited, Blocked} |
Purge | Анонимизация PII архивированной учётки (необратимо) | Status = Archived и DeletedAt <= now − 30 дней (регламентный карантин; исключение — явный LegalBasis запроса субъекта данных, тогда карантин не применяется) |
Правила анонимизации Purge (поле → значение)
Поле UserEntity | Значение после purge |
|---|---|
PhoneNumber | purged:{UserId} (уникальный плейсхолдер — уникальный индекс не нарушается; E.164-вход по нему невозможен) |
PhoneNumberConfirmedAt, Email, EmailConfirmedAt | null |
Name (owned FullName) | FirstName = "Удалённый", LastName = "Пользователь", MiddleName = null |
PasswordHash, PinCodeHash | null; IsTwoFactorEnabled = false |
ReferrerUrl | null |
LastSignInAt, LockoutUntil, FailedAccessAttempts | null / null / 0 |
| Связанные записи | SignInConfirmationRequest пользователя удаляются физически (содержат телефон/IP/UA); у RefreshSession затирается Device (UserAgent/IpAddress/DisplayName → null), статусы сохраняются для аудита |
Id, Type, CompanyId, Status = Archived, RegisteredVia, audit-штампы, Version —
сохраняются (неперсональные, нужны истории сделок).
Для SyncSource = TalentIdentity purge запрещён, пока Talent не прислал UserDeletedEvent
(источник PII — Talent; см. IDENTITY_USER_PURGE_SYNC_MANAGED ниже).
Модель ответа AdminDeleteUserResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
UserId | Guid | Да | UserEntity.Id | — | Пользователь. |
Mode | string (enum UserDeletionMode) | Да | команда | — | Применённый режим. |
Status | string (enum UserStatus) | Да | UserEntity.Status | — | Итоговый статус (Archived). |
RevokedSessionsCount | int | Да | батч-отзыв | — | Отозвано сессий (0 при Purge — уже отозваны архивом). |
RevokedAssignmentsCount | int | Да | батч-отзыв | — | Отозвано назначений ролей. |
PiiErased | bool | Да | вычисление | — | true только при Purge. |
Version | long | Да | UserEntity.Version | — | Версия после мутации. |
Алгоритм
- Контекст и доступ. Операцию выполняет система администрирования (m2m-клиент
pin-admin-system, policyAdminSystemOnly; приMode = PurgeдополнительноAdminPrivilegedOnly). Повтор с тем жеIdempotency-Key(admin:user-delete:{IdempotencyKey}) возвращает сохранённый результат. - Проверка входа и предусловий.
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 получит событие).
- Бизнес-правила и переходы.
Archive: учётка переводится вArchived, проставляетсяDeletedAt; актором удаления записывается служебный actor admin-API (Guid администратора — в audit); фиксируется вид изменения «архивирование».Purge: применяется таблица анонимизации PII выше; статус остаётсяArchived(перехода нет); фиксируется вид изменения «pii-erasure».
- Что меняется.
Archive: статус учётки →Archived; все активные сессии отзываются (Active → Revoked,RevokedReason = AdminBlock); все активные назначения ролей отзываются (Active → RevokedсRevokedAt), полная история назначений сохраняется (аудит доступов, role.md).Purge: одноразовые запросы кодовSignInConfirmationRequestудаляются физически; у сессий затирается слепок устройства (Device → пусто), статусы сохраняются для аудита; PII-поля учётки затираются по таблице анонимизации. Порядок изменения — учётка, затем дочерние записи (целостность связей). Правка не должна затирать конкурентное изменение (CONCURRENCY_CONFLICT).
- События.
Archive→UserUpsertedKafkaEventсоStatus = Archived(контракт) иUserAccessChangedKafkaEvent(ChangeReason = AssignmentRevoked,ActiveAssignments = [], контракт);Purge→UserUpsertedKafkaEventс анонимизированным снапшотом (Name = «Удалённый Пользователь»,PhoneNumber = purged:{UserId}) — потребители обязаны перезаписать свои PII-реплики (правило распространения erasure). Мутация учётки, дочерние изменения, отметка идемпотентности, audit (включая запись PII-erasure) и публикация событий выполняются в одном согласованном изменении.
- Результат.
AdminDeleteUserResult:UserId,Mode, итоговыйStatus = Archived,RevokedSessionsCount,RevokedAssignmentsCount,PiiErased,Version.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | UserUpsertedKafkaEvent → pin.identity.users.v1; при Archive также UserAccessChangedKafkaEvent → pin.identity.access.v1 (оба key = UserId) |
| External calls | none |
| Cache / projections / search | проекция UserSearchDocument (../../search/users.md): для company-user учётки документ вырезается (IsActive = false) при Archive и удаляется по UserId при Purge/выходе из компании; инвестор в проекции отсутствует. Кэши прав — событием access |
| Notifications | none (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, WhenDELETE ?mode=Archive, Then статус →Archived,DeletedAt = now, все активные сессии и назначения ролей отозваны, ответPiiErased = false, публикуютсяUserUpsertedKafkaEvent(Status = Archived) иUserAccessChangedKafkaEvent(ActiveAssignments = []); полная история назначений сохранена. - Given учётка уже
Archived, Whenmode=Archive, Then идемпотентный200без изменений. - Given учётка
ArchivedсDeletedAt45 дней назад, privileged-подтверждение иLegalBasis, Whenmode=Purge, Then PII затирается по таблице анонимизации, статус остаётсяArchived,PiiErased = true,SignInConfirmationRequestудаляются физически, у сессий затирается слепок устройства; публикуетсяUserUpsertedKafkaEventс анонимизированным снапшотом; потребители перезаписывают PII-реплики. - Given учётка
Active, Whenmode=Purge, ThenIDENTITY_USER_NOT_ARCHIVED(409) — сначала требуетсяArchive. - Given учётка
Archived5 дней назад без основания субъекта данных, Whenmode=Purge, ThenIDENTITY_USER_PURGE_QUARANTINE(409). - Given Talent-managed учётка (
SyncSource = TalentIdentity) безUserDeletedEventот Talent, Whenmode=Purge, ThenIDENTITY_USER_PURGE_SYNC_MANAGED(409). - Given
mode=PurgeбезLegalBasis, When запрос, ThenValidationError(400). - Given
mode=Purgeбез privileged-подтверждения на стороне системы администрирования, When запрос, ThenFORBIDDEN(403). - Given повтор с тем же
Idempotency-Key, When запрос по таймауту, Then сохранённый результат (replay), дочерние изменения не повторяются.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Mode вне enum / LegalBasis отсутствует при Purge | Ошибка валидации запроса | no retry |
FORBIDDEN | 403 | Нет policy (Purge без AdminPrivilegedOnly) | Доступ запрещён | no retry |
IDENTITY_USER_NOT_FOUND | 404 | Пользователь не существует | Пользователь не найден | no retry |
IDENTITY_USER_NOT_ARCHIVED | 409 | Purge не из Archived | Полное удаление возможно только из архива | no retry (сначала Archive) |
IDENTITY_USER_PURGE_QUARANTINE | 409 | Карантин 30 дней не истёк и нет основания субъекта | Не истёк карантин архивного удаления | no retry до истечения |
IDENTITY_USER_PURGE_SYNC_MANAGED | 409 | Talent-реплика без UserDeletedEvent от Talent | Источник данных — Talent Identity; удаление инициируется там | no retry |
CONCURRENCY_CONFLICT | 409 | Гонка версий | Повторите операцию | 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 и реплики потребителей» (контроль перезаписи снапшота).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): Identity Admin API — реестр методов и общие правила области, POST /api/admin/v1/users/{userId}/block — Блокировка пользователя (admin), POST /api/admin/v1/users/{userId}/security/reset — Ручной сброс факторов входа (admin), POST /api/admin/v1/users/{userId}/sessions/revoke — Отзыв сессий пользователя (admin)
- События (1): Event Contract: pin.identity.user-upserted (UserUpsertedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: user.md (soft-delete, «полное удаление PII — регламент системы администрирования»), user-session.md, role.md (история назначений не удаляется).
- События: user-upserted, user-access-changed.
- Соседние admin-операции: block-user, reset-user-security, revoke-user-sessions. ADR-0053; ADR-0052.