UC-IDN-005. Создание роли компании и назначение агенту в scope проекта
Назначение
Реализует модель Scoped-RBAC: руководитель компании создаёт внутреннюю роль из каталога permissions и
назначает её сотруднику в узком scope (конкретный проект), получая поведение «редактирует только проект X».
Вызывает руководитель агентства/застройщика с правами администратора компании; на выходе — созданная роль и
активное назначение, после чего эффективные права сотрудника ограничены выбранным ресурсом. Связан с
контекстом identity.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-IDN-005 |
| Контекст | identity (см. docs/06-services/identity/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Агентство (руководитель, agency-owner) или Девелопер (developer-manager с правами администратора компании) |
| Permissions / scope | identity.roles.manage (создание роли) + identity.role-assignments.manage (назначение) в scope Organization(CompanyId), покрывающем целевой scope (INV-R8) |
| Tenant | TenantId = 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 |
| Триггерящий элемент | ЛК компании → «Команда» → «Роли» → «Создать роль»; карточка сотрудника → «Назначить роль» |
Основной Поток
- Руководитель открывает раздел «Роли», нажимает «Создать роль», вводит название («Менеджер проекта X»), код и отмечает permissions из каталога (каталог получен через list-permission-catalog.md, доступен любому аутентифицированному пользователю).
- Система принимает запрос создания роли (контракт —
create-company-role.md;
CompanyIdберётся из claimstenant_id, не из route/body; передаётсяIdempotency-Key) с телом{ code (^[a-z][a-z0-9-]{1,62}$), name (1..128), description? (≤512), permissionIds: Guid[] (без дубликатов) }. - Проверить доступ и вход: у актора есть
identity.roles.manageв scopeOrganization(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). - Создать роль
RoleDefinition (CompanyId, code, name, description, IsSystem = false, Status = Active)с дочернимиRolePermissionпо каждомуpermissionId. Роль и её permissions фиксируются в одном согласованном изменении. - Зафиксировать факт: событие
RoleCreatedEvent. Интеграционное событие не публикуется — права пользователей ещё не изменились (роль никому не назначена). - Система возвращает
{ id, code, status = Active, permissionCodes[], version }(201,Location: /api/v1/roles/{id}). - Руководитель открывает карточку агента, выбирает «Назначить роль» → роль «Менеджер проекта X», scope «Проект» → выбирает проект из списка проектов организации.
- Система принимает запрос назначения (контракт —
grant-role-assignment.md; передаётся
Idempotency-Key) с телом{ userId, roleId, scopeType: "Project", projectId, expiresAt? }—organizationIdв теле отсутствует, всегда берётся из claims (CompanyId), что исключает кросс-tenant подмену. - Проверить доступ и вход: у актора есть
identity.role-assignments.manageв scope, покрывающемProject(projectId)(scopeOrganization⊃Project— 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). - Создать назначение
RoleAssignment (userId, roleId, CompanyId, Scope = Project(CompanyId, projectId), Status = Active, GrantedByUserId = actor, GrantedAt = now, ExpiresAt?). - Зафиксировать факты: событие
RoleAssignmentGrantedEventи интеграционноеUserAccessChangedKafkaEvent→pin.identity.access.v1(key =UserId; payload:UserId,CompanyId, полный список активных назначений{RoleCode, ScopeType, ScopeIds},Version). Назначение и публикация — в одном согласованном изменении. - Система возвращает
{ assignmentId, userId, roleCode, scopeType, scopeOrganizationId, scopeProjectId, expiresAt?, grantedAt }(201). - Consumers (
crm,chessboard,catalog, gateway) поpin.identity.access.v1сбрасывают кэш эффективных прав пользователя; при следующем запросе агент видит/редактирует только проект X (наследование:Projectне поднимается вверх — role.md «effectivePermissions»).
Альтернативные Потоки
A1. Изменение состава прав существующей роли
- Операция изменения permissions роли (set-role-permissions.md)
загружает роль, проверяет INV-R1 (
IsSystem = false) и применяет дельту (добавление/удаление permissions). - Так как эффективные права меняются у ВСЕХ пользователей роли, публикация
UserAccessChangedKafkaEventвыполняется fan-out background-job-ом батчами по назначениям роли (role.md «События»). Завершается шагом 13.
A2. Назначение в scope организации («редактирует все проекты»)
- Шаг 8 с
scopeType: "Organization"(безprojectGroupId/projectId; организация — из claims) — та же операция; наследование даёт доступ ко всем группам и проектам компании. Проверка chessboard не нужна.
A3. Отзыв назначения
- Операция отзыва (revoke-role-assignment.md) проверяет
scope актора (INV-R8), переводит назначение
Active → Revoked(заполняетRevokedAt/ByUserId), поднимаетRoleAssignmentRevokedEvent+UserAccessChangedKafkaEvent. Запись не удаляется (аудит).
A4. Попытка изменить системную роль
roleIdуказывает на SEED-роль (IsSystem = true) → guard INV-R1 →IDENTITY_ROLE_SYSTEM_IMMUTABLE(403). Системные роли меняет только релиз-миграция.
A5. Архив роли
- Архивация роли переводит её
Status = Archived; в том же согласованном изменении отзываются все Active назначения роли (INV-R5) + fan-outUserAccessChangedKafkaEvent. Новые назначения архивной роли отклоняются.
A6. Проект удалён/выведен в chessboard (висячее назначение)
- Назначение выдано в scope
Project(X); позже проект X удаляется или выводится из витрины вchessboard. Назначение остаётся, но его scope указывает на несуществующий ресурс. - По событию 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 | Условие | Поведение клиента |
|---|---|---|---|
FORBIDDEN | 403 | Нет identity.roles.manage / identity.role-assignments.manage в покрывающем scope (INV-R8) | Скрыть/задизейблить действия |
ValidationError | 400 | Код роли/форма scope (INV-R4) | Подсветить поле |
IDENTITY_ROLE_CODE_TAKEN | 409 | Дубль (CompanyId, Code) | Предложить другой код |
IDENTITY_PERMISSION_NOT_FOUND | 400 | Несуществующий permissionId (INV-R7) | Перезагрузить каталог |
IDENTITY_PERMISSION_NOT_ASSIGNABLE | 409 | IsAssignableByCompanies = false / deprecated (INV-P4) | Убрать позицию из выбора |
IDENTITY_COMPANY_SUSPENDED | 409 | Компания не Active | «Компания заблокирована» |
IDENTITY_ROLE_SYSTEM_IMMUTABLE | 403 | Мутация SEED-роли (INV-R1, A4) | Показать пояснение |
IDENTITY_USER_NOT_FOUND / IDENTITY_ROLE_NOT_FOUND | 404 | Пользователь/роль не существуют или чужие | Обновить данные формы |
IDENTITY_ROLE_ARCHIVED | 409 | Роль Archived (A5) | Обновить список ролей |
IDENTITY_ASSIGNMENT_COMPANY_MISMATCH | 409 | Роль чужой компании / несовместимая системная (INV-R2) | Ошибка формы |
IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN | 403 | Platform-scope для роли компании (INV-R3) | Ошибка формы |
IDENTITY_ASSIGNMENT_DUPLICATE | 409 | Активный дубль (user, role, scope) (INV-R6) | Показать существующее назначение |
IDENTITY_SCOPE_RESOURCE_NOT_FOUND | 400 | Проект/группа отсутствует в chessboard / чужой организации | Обновить список проектов |
SERVICE_UNAVAILABLE | 503 | chessboard недоступен на валидации scope | Повторить позже |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
RoleCreatedEvent / RoleUpdatedEvent / RoleArchivedEvent | CRUD роли | in-proc; A1/A5 — fan-out job |
RoleAssignmentGrantedEvent | Grant (шаг 11) | in-proc аудит |
RoleAssignmentRevokedEvent | Revoke/Expire/каскад архива | in-proc аудит |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
UserAccessChangedKafkaEvent.v1 | pin.identity.access.v1 | Grant/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 |
| AC4 | Grant публикует UserAccessChangedKafkaEvent в том же согласованном изменении | контракт-тест + проверка публикации |
| AC5 | Актор со scope Project(Y) не может выдать роль в Project(X) (INV-R8) | тест покрытия scope |
| AC6 | Архив роли отзывает все её назначения (INV-R5) | тест A5 |
| AC7 | История назначений не удаляется при revoke | проверка записи Revoked |
| AC8 | Удаление проекта в chessboard eventual-но отзывает висячее Project(X)-назначение (A6) | тест: удалить проект → назначение Revoked + событие |
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): Identity Company-User API — реестр методов и общие правила области
- index.md (1): Identity Service (Pin.Identity)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы: ../api/company-user/create-company-role.md, ../api/company-user/set-role-permissions.md, ../api/company-user/grant-role-assignment.md, ../api/company-user/revoke-role-assignment.md, ../api/company-user/list-permission-catalog.md; реестр — ../api/README.md.
- Domain entity: ../domain/role.md (INV-R1..R8), ../domain/permission.md (SEED 115, INV-P4).
- Кросс-контекст:
docs/06-services/chessboard/(проекты/группы проектов; eventual-отзыв висячих назначений). - Связанные UC: UC-IDN-006 (пользователь появляется в компании до назначения), UC-IDN-008 (enforcement).