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

UC-IDN-005. Создание роли компании и назначение агенту в scope проекта

Назначение

Реализует модель Scoped-RBAC: руководитель компании создаёт внутреннюю роль из каталога permissions и назначает её сотруднику в узком scope (конкретный проект), получая поведение «редактирует только проект X». Вызывает руководитель агентства/застройщика с правами администратора компании; на выходе — созданная роль и активное назначение, после чего эффективные права сотрудника ограничены выбранным ресурсом. Связан с контекстом identity.

Metadata

ПолеЗначение
IDUC-IDN-005
Контекстidentity (см. docs/06-services/identity/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольАгентство (руководитель, agency-owner) или Девелопер (developer-manager с правами администратора компании)
Permissions / scopeidentity.roles.manage (создание роли) + identity.role-assignments.manage (назначение) в scope Organization(CompanyId), покрывающем целевой scope (INV-R8)
TenantTenantId = CompanyId актора; роль и назначение создаются в этом же tenant (ADR-0052)
Record-levelне применяется (управление ролями — tenant-scope, не record-level)

Предусловие

  • Компания актора Status = Active; актор Status = Active.
  • Назначаемый пользователь принадлежит той же компании (user.CompanyId = actor.CompanyId) и не архивен.
  • Целевой проект существует в chessboard и принадлежит организации scope (кросс-контекстная проверка на write-path — role.md «ResourceScope»).
  • SEED-каталог permissions загружен (139 позиций).

Триггер

ПолеЗначение
ИсточникUI button
Триггерящий элементЛК компании → «Команда» → «Роли» → «Создать роль»; карточка сотрудника → «Назначить роль»

Основной Поток

  1. Руководитель открывает раздел «Роли», нажимает «Создать роль», вводит название («Менеджер проекта X»), код и отмечает permissions из каталога (каталог получен через list-permission-catalog.md, доступен любому аутентифицированному пользователю).
  2. Система принимает запрос создания роли (контракт — create-company-role.md; CompanyId берётся из claims tenant_id, не из route/body; передаётся Idempotency-Key) с телом { code (^[a-z][a-z0-9-]{1,62}$), name (1..128), description? (≤512), permissionIds: Guid[] (без дубликатов) }.
  3. Проверить доступ и вход: у актора есть identity.roles.manage в scope Organization(CompanyId) (иначе FORBIDDEN); повторный запрос с тем же Idempotency-Key возвращает прежний результат; компания Status = Active (иначе IDENTITY_COMPANY_SUSPENDED); пара (CompanyId, Code) уникальна (иначе IDENTITY_ROLE_CODE_TAKEN); все permissionIds существуют (INV-R7, иначе IDENTITY_PERMISSION_NOT_FOUND), каждый IsAssignableByCompanies = true, не IsDeprecated и MinScopeType != Platform (INV-P4, иначе IDENTITY_PERMISSION_NOT_ASSIGNABLE).
  4. Создать роль RoleDefinition (CompanyId, code, name, description, IsSystem = false, Status = Active) с дочерними RolePermission по каждому permissionId. Роль и её permissions фиксируются в одном согласованном изменении.
  5. Зафиксировать факт: событие RoleCreatedEvent. Интеграционное событие не публикуется — права пользователей ещё не изменились (роль никому не назначена).
  6. Система возвращает { id, code, status = Active, permissionCodes[], version } (201, Location: /api/v1/roles/{id}).
  7. Руководитель открывает карточку агента, выбирает «Назначить роль» → роль «Менеджер проекта X», scope «Проект» → выбирает проект из списка проектов организации.
  8. Система принимает запрос назначения (контракт — grant-role-assignment.md; передаётся Idempotency-Key) с телом { userId, roleId, scopeType: "Project", projectId, expiresAt? }organizationId в теле отсутствует, всегда берётся из claims (CompanyId), что исключает кросс-tenant подмену.
  9. Проверить доступ и вход: у актора есть identity.role-assignments.manage в scope, покрывающем Project(projectId) (scope OrganizationProject — INV-R8, иначе FORBIDDEN); пользователь существует, Status ∈ {Invited, Active} и принадлежит компании актора (иначе IDENTITY_USER_NOT_FOUND); роль существует, Status = Active и принадлежит компании актора (INV-R2, иначе IDENTITY_ROLE_NOT_FOUND / IDENTITY_ASSIGNMENT_COMPANY_MISMATCH); форма scope корректна (INV-R4, иначе ValidationError); scopeType != Platform (INV-R3, иначе IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN); проект существует и принадлежит компании (проверка в chessboard, иначе IDENTITY_SCOPE_RESOURCE_NOT_FOUND / SERVICE_UNAVAILABLE); нет активного дубля назначения (user, role, scope) (INV-R6, иначе IDENTITY_ASSIGNMENT_DUPLICATE).
  10. Создать назначение RoleAssignment (userId, roleId, CompanyId, Scope = Project(CompanyId, projectId), Status = Active, GrantedByUserId = actor, GrantedAt = now, ExpiresAt?).
  11. Зафиксировать факты: событие RoleAssignmentGrantedEvent и интеграционное UserAccessChangedKafkaEventpin.identity.access.v1 (key = UserId; payload: UserId, CompanyId, полный список активных назначений {RoleCode, ScopeType, ScopeIds}, Version). Назначение и публикация — в одном согласованном изменении.
  12. Система возвращает { assignmentId, userId, roleCode, scopeType, scopeOrganizationId, scopeProjectId, expiresAt?, grantedAt } (201).
  13. Consumers (crm, chessboard, catalog, gateway) по pin.identity.access.v1 сбрасывают кэш эффективных прав пользователя; при следующем запросе агент видит/редактирует только проект X (наследование: Project не поднимается вверх — role.md «effectivePermissions»).

Альтернативные Потоки

A1. Изменение состава прав существующей роли

  1. Операция изменения permissions роли (set-role-permissions.md) загружает роль, проверяет INV-R1 (IsSystem = false) и применяет дельту (добавление/удаление permissions).
  2. Так как эффективные права меняются у ВСЕХ пользователей роли, публикация UserAccessChangedKafkaEvent выполняется fan-out background-job-ом батчами по назначениям роли (role.md «События»). Завершается шагом 13.

A2. Назначение в scope организации («редактирует все проекты»)

  1. Шаг 8 с scopeType: "Organization" (без projectGroupId/projectId; организация — из claims) — та же операция; наследование даёт доступ ко всем группам и проектам компании. Проверка chessboard не нужна.

A3. Отзыв назначения

  1. Операция отзыва (revoke-role-assignment.md) проверяет scope актора (INV-R8), переводит назначение Active → Revoked (заполняет RevokedAt/ByUserId), поднимает RoleAssignmentRevokedEvent + UserAccessChangedKafkaEvent. Запись не удаляется (аудит).

A4. Попытка изменить системную роль

  1. roleId указывает на SEED-роль (IsSystem = true) → guard INV-R1 → IDENTITY_ROLE_SYSTEM_IMMUTABLE (403). Системные роли меняет только релиз-миграция.

A5. Архив роли

  1. Архивация роли переводит её Status = Archived; в том же согласованном изменении отзываются все Active назначения роли (INV-R5) + fan-out UserAccessChangedKafkaEvent. Новые назначения архивной роли отклоняются.

A6. Проект удалён/выведен в chessboard (висячее назначение)

  1. Назначение выдано в scope Project(X); позже проект X удаляется или выводится из витрины в chessboard. Назначение остаётся, но его scope указывает на несуществующий ресурс.
  2. По событию chessboard об удалении проекта identity eventual-но отзывает висячие назначения этого scope (Active → Revoked, причина — ресурс удалён) и публикует UserAccessChangedKafkaEvent. До обработки события проверка доступа (UC-IDN-008) на удалённый ресурс всё равно вернёт deny — покрывающего живого ресурса нет, а Organization-назначения на удалённый проект не распространяются вниз к отсутствующему id.

Постусловие

  • Создана RoleDefinition (CompanyId = компания актора, IsSystem = false) с RolePermission[].
  • Создано RoleAssignment (Active, Scope = Project(X)); полная история назначений сохраняется.
  • Кэши авторизации потребителей инвалидированы; effectivePermissions агента включают permissions роли только для ресурсов проекта X.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
FORBIDDEN403Нет identity.roles.manage / identity.role-assignments.manage в покрывающем scope (INV-R8)Скрыть/задизейблить действия
ValidationError400Код роли/форма scope (INV-R4)Подсветить поле
IDENTITY_ROLE_CODE_TAKEN409Дубль (CompanyId, Code)Предложить другой код
IDENTITY_PERMISSION_NOT_FOUND400Несуществующий permissionId (INV-R7)Перезагрузить каталог
IDENTITY_PERMISSION_NOT_ASSIGNABLE409IsAssignableByCompanies = false / deprecated (INV-P4)Убрать позицию из выбора
IDENTITY_COMPANY_SUSPENDED409Компания не Active«Компания заблокирована»
IDENTITY_ROLE_SYSTEM_IMMUTABLE403Мутация SEED-роли (INV-R1, A4)Показать пояснение
IDENTITY_USER_NOT_FOUND / IDENTITY_ROLE_NOT_FOUND404Пользователь/роль не существуют или чужиеОбновить данные формы
IDENTITY_ROLE_ARCHIVED409Роль Archived (A5)Обновить список ролей
IDENTITY_ASSIGNMENT_COMPANY_MISMATCH409Роль чужой компании / несовместимая системная (INV-R2)Ошибка формы
IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN403Platform-scope для роли компании (INV-R3)Ошибка формы
IDENTITY_ASSIGNMENT_DUPLICATE409Активный дубль (user, role, scope) (INV-R6)Показать существующее назначение
IDENTITY_SCOPE_RESOURCE_NOT_FOUND400Проект/группа отсутствует в chessboard / чужой организацииОбновить список проектов
SERVICE_UNAVAILABLE503chessboard недоступен на валидации scopeПовторить позже

Доменные События

СобытиеКогда поднимаетсяSubscribers
RoleCreatedEvent / RoleUpdatedEvent / RoleArchivedEventCRUD ролиin-proc; A1/A5 — fan-out job
RoleAssignmentGrantedEventGrant (шаг 11)in-proc аудит
RoleAssignmentRevokedEventRevoke/Expire/каскад архиваin-proc аудит

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
UserAccessChangedKafkaEvent.v1pin.identity.access.v1Grant/Revoke/Expire назначения; изменение permissions роли (fan-out по пользователям)crm, catalog, chessboard, gateway — сброс кэшей авторизации
Гарантии доставки и окно согласованности

UserAccessChangedKafkaEvent публикуется с ключом партиционирования UserId (порядок изменений прав на одного пользователя сохраняется), несёт Version и полный список активных назначений — потребители применяют снапшот идемпотентно и игнорируют более старую Version. Инвалидация кэшей прав у потребителей и в самом identity — eventual: до неё эффективные права агента вычисляются по прежнему кэшу (окно ≤ TTL кэша прав, UC-IDN-008). Практический эффект: после grant агент получает доступ к проекту X не мгновенно, а в пределах окна; после revoke — так же теряет его в пределах окна. Что синк ожидает от chessboard на write-path: синхронное подтверждение существования и принадлежности проекта организации (недоступность → SERVICE_UNAVAILABLE, устаревание → висячее назначение и eventual-отзыв, A6). Idempotency-Key на обоих write-path (создание роли, назначение) гарантирует, что повтор запроса не создаёт дубль, а возвращает прежний результат.

Acceptance Criteria

#КритерийПроверка
AC1Роль создаётся только из assignable permissions каталога (INV-P4/R7)тест с платформенным permission → 400
AC2Назначение в scope Project даёт доступ только к проекту X; scope Organization — ко всем проектаминтеграционный тест effectivePermissions (UC-IDN-008)
AC3Дубль активного назначения невозможен (INV-R6)повторный grant → 409
AC4Grant публикует UserAccessChangedKafkaEvent в том же согласованном измененииконтракт-тест + проверка публикации
AC5Актор со scope Project(Y) не может выдать роль в Project(X) (INV-R8)тест покрытия scope
AC6Архив роли отзывает все её назначения (INV-R5)тест A5
AC7История назначений не удаляется при revokeпроверка записи Revoked
AC8Удаление проекта в chessboard eventual-но отзывает висячее Project(X)-назначение (A6)тест: удалить проект → назначение Revoked + событие

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

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

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