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

Consumer: Chessboard Project Deleted (pin-identity-chessboard-projects)

Consumer контекста identity, отзывающий висячие назначения ролей (RoleAssignment), чей ResourceScope ссылается на проект контекста chessboard, удалённый/архивированный в источнике. Отдельный consumer (НЕ входит в group синка Talent — talent-identity-sync.md, там только топики Talent). Дом полей RoleAssignment/ResourceScope и переходов — domain/role.md (связь «ResourceScope → chessboard», eventual-консистентность).

Назначение

Удаление проекта или группы проектов в chessboard не имеет FK на identity.role_assignments (кросс-контекст без FK, role.md — связь ResourceScope → chessboard): назначения роли developer-manager в scope Project/ProjectGroup остаются «висячими» — ссылаются на несуществующий ресурс. Consumer подписан на upsert-событие проекта chessboard и реагирует только на переход в Status = Archived (архивация = логическое удаление ресурса для целей scope-очистки; отдельного delete-топика у chessboard нет — project-upserted.md), находит по scope-id все активные RoleAssignment, ссылающиеся на архивированный ресурс (индекс ix_role_assignments_scope), и отзывает их (причина ScopeResourceRemoved). Для каждого затронутого пользователя публикуется UserAccessChangedKafkaEvent (pin.identity.access.v1, key = UserId) — потребители авторизации (crm, catalog, chessboard, gateway) сбрасывают кэш эффективных прав. Актор — система (фоновый fan-out, не пользовательский контекст); INV-R8 (GranterAuthority) к системному отзыву не применяется.

Owning-сервис и триггер

ПолеЗначение
Сервис/контекстidentity
Статус документаdraft · Owner: PIN Platform Core
Обрабатываемое событиеProjectUpsertedKafkaEvent — upsert проекта; consumer обрабатывает только записи с Status = Archived (архивация = удаление ресурса для scope-очистки)
Topicpin.chessboard.project-upserted.v1 (внешний, владелец chessboard; фильтр Status = Archived; контракт — project-upserted.md)
ContractVersionожидается 1; при несовпадении версии события — warn-and-proceed (метрика chessboard_scope_contract_mismatch_total)
Consumer grouppin-identity-chessboard-projects
Producerchessboard
Эффект (событие → что меняется)активные RoleAssignment в scope архивированного проекта → Revoked (причина ScopeResourceRemoved); на каждого затронутого пользователя — UserAccessChangedKafkaEvent
Порядокkey события = ProjectId → события одного проекта в одной партиции

Контекст события

  • Из метаданных события доступны: EventId (для дедупликации, Guid v7), CreatedAt (= момент архивации), Version (= ContractVersion producer-а), идентификатор и тип агрегата-источника (ProjectId / Project).
  • Актор — система (RevokedByUserId = null); операция исполняется в tenant-scope застройщика (TenantId = DeveloperId события) — назначения Project/ProjectGroup всегда имеют CompanyId = DeveloperId (INV-R2 role.md).
  • Корреляция сквозного трейса восстанавливается из метаданных события и пробрасывается в исходящие pin.identity.access.v1 события.
  • Ключ бизнес-идемпотентности: identity-chessboard-project-deleted:{EventId}; бизнес-идемпотентность — status-guard Active → Revoked (повторная доставка находит 0 активных назначений → no-op).

Входные данные / payload события

Контракт внешний (контекст chessboard); ниже — поля, которые consumer реально читает. Дом полей RoleAssignment/ResourceScope, на которые маппится scope-id, — role.md; полная таблица payload — project-upserted.md.

Поле payloadТипОбязательностьКак используется (маппинг)
IdGuidДаProjectId — ключ отзыва → RoleAssignment.Scope.ProjectId (scope Project).
Statusstring (enum ProjectStatus)ДаФильтр consumer-а: обрабатывается только Archived; прочие статусы (Draft/Published/Hidden) → skip без изменений.
DeveloperIdGuidДаTenantId операции (= CompanyId затронутых назначений, INV-R2).
VersionlongДаДоменная версия проекта (информационно, лог/трейс; guard порядка).
Область отзыва: только Project-scope

Consumer отзывает ТОЛЬКО Project-scoped назначения ровно этого ProjectId (родительские ProjectGroup- и Organization-назначения не затрагиваются — покрывают существующие ресурсы). Очистка ProjectGroup-scope (удаление группы проектов) — вне этого consumer-а: у chessboard нет события удаления группы в project-upserted; при появлении group-lifecycle-события заводится отдельный consumer (будущее).

Алгоритм

  1. Контекст события. Восстановить метаданные (EventId, CreatedAt, Version, ProjectId); актор = система; сверить Version с ожидаемым ContractVersion = 1 — при несовпадении warn-and-proceed; идемпотентность по ключу identity-chessboard-project-deleted:{EventId} — дубль → лог skipped-duplicate, ack. Фильтр Archived: если Status != Archived — запись не относится к очистке scope (обычный upsert), лог skipped-non-archived, ack без изменений.
  2. Проверка входа. Id (ProjectId) и DeveloperId заданы — иначе отказ IDENTITY_CHESSBOARD_SCOPE_MISSING (non-retriable → DLQ, нарушение контракта). Причина отзыва фиксирована — ScopeResourceRemoved (архивация ресурса). Существование ресурса/назначений НЕ проверяется — отсутствие активных назначений допустимо (успешный no-op, устойчивость к висячим/отсутствующим ссылкам).
  3. Что читаем. Активные назначения только Project-scope этого проекта: Status = Active, Scope.Type = Project, Scope.ProjectId = payload.Id — по индексу ix_role_assignments_scope, страницами до исчерпания. Пустая первая страница → изменений/событий нет, только отметка идемпотентности.
  4. Правила и переходы. Для каждого назначения — отзыв (Reason = ScopeResourceRemoved, RevokedAt = момент архивации): guard перехода Active → Revoked (role.md); уже Revoked/Expired → доменный no-op (бизнес-идемпотентность). Ставится Status = Revoked, RevokedAt, инкремент Version, поднимается доменное RoleAssignmentRevokedEvent(AssignmentId, UserId, RoleId, Scope, Reason). Накопить affectedUserIds (уникальные UserId).
  5. Что меняется. Отозванные назначения (переход статуса; конкурентный доступ контролируется версией: повторная доставка не находит активных строк). Прямых удалений нет — отзыв это soft-delete статусом (записи назначений хранятся бессрочно, аудит доступов, role.md).
  6. События. Для каждого userId ∈ affectedUserIds: перечитать его остаточные активные назначения, коды ролей; собрать UserAccessChangedKafkaEvent → топик pin.identity.access.v1, key = userId, AggregateId = userId, AggregateType = "User", ContractVersion = 1, payload (UserId, CompanyId = DeveloperId, список ActiveAssignmentSnapshotModel {RoleCode, ScopeType, ScopeIds}, Version) — дом контракта user-access-changed.md. affectedUserIds пуст → исходящих событий нет.
  7. Согласованность. Отозванные назначения + отметка идемпотентности + исходящие UserAccessChangedKafkaEventатомарно. Offset подтверждается только после успешной фиксации. Многостраничный отзыв — в одном согласованном изменении (весь набор висячих назначений одного ресурса атомарен).
  8. Результат. Успех → ack. Контролируемая ошибка с retriable-кодом → retry по политике (таблица ниже); non-retriable → DLQ + алерт. Метрики/логи — раздел «Наблюдаемость».

Диаграмма

Идемпотентность и replay

  • По событию: дедупликация по EventId (ключ identity-chessboard-project-deleted:{EventId}) — повторная доставка after-commit-crash не меняет состояние.
  • По бизнес-правилу: status-guard Active → Revoked — при повторе активных назначений не находится → no-op, исходящих событий нет. Отсутствие назначений на удалённый ресурс — успешный no-op (не ошибка).
  • Replay: да. Повторное чтение топика безопасно (идемпотентно по обоим уровням). Порядок per-resource гарантирован ключом; событие с уже отозванными назначениями — no-op.

Retry и DLQ

DLQ-топик: pin.identity.chessboard-scope-dlq.v1 (исходное событие + заголовки x-dlq-reason, x-dlq-error-code, x-source-topic, x-original-offset).

СбойКодRetryDLQРучное действие
Инфраструктурный (PostgreSQL/шина недоступны, timeout)Бесконечный exponential backoff 1s→60s с паузой партицииНетRunbook identity-overview: восстановить зависимость
Отсутствует scope-id (нарушение контракта)IDENTITY_CHESSBOARD_SCOPE_MISSINGНет (non-retriable)Да + алертРазбор контракта с chessboard; фикс producer → replay
Конфликт версий при гонке с ручным отзывом/grant3 попытки немедленно (перечитать страницу)После исчерпанияРасследование гонки

Отсутствие активных назначений — НЕ сбой: успешный no-op.

Наблюдаемость и безопасность

  • Logs (structured): consumed / skipped-duplicate / revokedEventId, scope-id, revokedCount, affectedUsers) / no-op / dead-lettered. PII нет (scope-id и UserId — не PII).
  • Metrics: chessboard_scope_consume_duration_ms, chessboard_scope_revoked_assignments_total, chessboard_scope_access_events_total, chessboard_scope_noop_total, chessboard_scope_contract_mismatch_total, chessboard_scope_dlq_total.
  • Traces: consume-span → span обработки → spans хранилища; correlation из метаданных события пробрасывается в исходящие pin.identity.access.v1.
  • Alerts: DLQ > 0; всплеск chessboard_scope_revoked_assignments_total (массовый отзыв — возможна ошибочная зачистка группы).
  • Безопасность: consumer под system-actor (не пользовательский контекст); tenant isolation — операция в scope TenantId = DeveloperId (назначения Project/ProjectGroup кросс-tenant не затрагиваются, INV-R2); доступ к топику chessboard — m2m ACL Kafka. INV-R8 (granter authority) к системному отзыву не применяется (см. role.md).

Acceptance (Given/When/Then)

#GivenWhenThen
AC-1Проект chessboard с 3 активными Project-назначениями роли developer-managerПришёл ProjectUpsertedKafkaEvent со Status = ArchivedВсе 3 → Revoked(ScopeResourceRemoved); на каждого уникального UserId — один UserAccessChangedKafkaEvent; offset ack после фиксации
AC-2Событие AC-1 доставлено повторно (тот же EventId)Повторная обработкаNo-op: 0 новых отзывов, 0 событий; ack (skipped-duplicate)
AC-3Событие проекта со Status = Published (не Archived)ОбработкаSkip без изменений (skipped-non-archived), ack
AC-4Архивируется проект без активных Project-назначенийОбработкаУспешный no-op: 0 отозвано, 0 событий; ack
AC-5Payload без Id или DeveloperIdОбработкаIDENTITY_CHESSBOARD_SCOPE_MISSING → DLQ + алерт (non-retriable)
AC-6У пользователя, кроме Project-назначения, есть Organization/ProjectGroup-назначение родителяАрхив одиночного проектаОтзывается только Project-назначение; родительские не затронуты; снапшот пользователя содержит оставшиеся активные
AC-7PostgreSQL/шина недоступны в момент обработкиОбработкаБесконечный exponential backoff с паузой партиции; offset не двигается; после восстановления — отзыв фиксируется, событий-дублей у потребителя нет ((UserId, Version)-guard)

Известные ограничения и assumptions

  • Топик выровнен с контрактом chessboard. У chessboard нет выделенного топика удаления проекта: архивация — это ProjectUpsertedKafkaEvent со Status = Archived на топике pin.chessboard.project-upserted.v1 (project-upserted.md, project.md — soft-delete Archived). Consumer триггерится переходом * → Archived (ProjectId = payload.Id). Связь зафиксирована в role.md «ResourceScope → chessboard».
  • Архивация одиночного проекта НЕ отзывает ProjectGroup/Organization-назначения родителя (ресурсы существуют); очистка ProjectGroup-scope — отдельным consumer-ом при появлении group-lifecycle-события.
  • Консистентность eventual: между удалением ресурса в chessboard и отзывом назначений возможен лаг (SLA потребителя identity < 5 мин, project-upserted.md) — в окне запрос прав может вернуть висячее назначение; enforcement record-level у владеющих контекстов дополнительно проверяет существование ресурса.

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

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

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