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

POST /api/v1/roles — Создание роли компании

Создание внутренней роли компании (RoleDefinition с CompanyId != null) с начальным составом прав из SEED-каталога. Компания сама заполняет таблицу определения ролей под свою оргструктуру.

Назначение

Руководитель агентства/менеджер застройщика создаёт роль под свою оргструктуру («Старший менеджер», «Маркетолог», «Администратор проекта») и наделяет её правами из каталога. Owning-контекст — identity (дом полей — domain/role.md). Feature group — roles.

Metadata

ПолеЗначение
Сервис/контекстidentity
API areapublic
Feature grouproles
Статусdraft
Документdocs/06-services/identity/api/company-user/create-company-role.md

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

ПроверкаЗначение
Actor typeUser (пользователь компании)
Auth policy / Permissionsidentity.roles.manage (дефолт: agency-owner, developer-manager)
Scope (RBAC)организация: Active RoleAssignment со scope Platform или Organization(CompanyId)
Record-levelне применяется
Tenant isolationTenantId = CompanyId: роль создаётся только в своей компании (CompanyId из claims, не из body)
Auditcreate: CreatedByUserId, Version = 1, доменное событие RoleCreatedEvent
MFA/approvalне требуется; системные роли этим методом не создаются (только SEED, INV-R1)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/roles
Success status201
Idempotency headerIdempotency-Key (Guid)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДаRequired
CompanyIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
IdempotencyKeyheader Idempotency-KeyGuidДаRequired
CodebodystringДаpattern ^[a-z][a-z0-9-]{1,62}$; уникальность в паре (CompanyId, Code) — ux_role_definitions_company_code
NamebodystringДа1..128
Descriptionbodystring?Нет<= 512
PermissionIdsbodyIReadOnlyList<Guid>Да (может быть пустым)<= 113 элементов, без дубликатов; каждый — существующий PermissionDefinition с IsAssignableByCompanies = true и IsDeprecated = false (INV-P4)

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаRoleDefinition.IdId созданной роли.
CodestringДаRoleDefinition.CodeКод.
Statusstring (enum RoleStatus)ДаRoleDefinition.StatusВсегда Active.
PermissionCodesIReadOnlyList<string>Даjoin PermissionDefinition.CodeФактический состав прав.
VersionlongДаRoleDefinition.Version1.

Предусловия и постусловия

Предусловия. Актор аутентифицирован и имеет identity.roles.manage в scope Platform или Organization(CompanyId); компания актора существует и в статусе Active; каталог прав (SEED) прогрет.

Постусловия при успехе. В компании актора появилась роль Status = Active, Version = 1, IsSystem = false, с составом прав ровно из PermissionIds; зафиксирован доменный факт RoleCreatedEvent. Отметка идемпотентности по Idempotency-Key сохранена: повтор вернёт тот же Id.

Постусловия при отказе. Состояние не изменилось: роль не создана, событий нет.

Интеграционных событий нет

Свежесозданная роль ещё никому не назначена, поэтому ничьи эффективные права не изменились — UserAccessChangedKafkaEvent уйдёт только при последующем grant/set-permissions.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь компании; tenant — его компания (CompanyId); требуется право identity.roles.manage в scope Organization(CompanyId). Повтор с тем же Idempotency-Key возвращает ранее сохранённый результат.
  2. Проверка входа и предусловий.
    • Синтаксис по таблице входных данных: Code соответствует паттерну, в PermissionIds нет дубликатов; иначе — ValidationError.
    • Компания актора существует и активна; Status != ActiveIDENTITY_COMPANY_SUSPENDED; отсутствие claim tenant_idIDENTITY_COMPANY_NOT_FOUND.
    • Код роли уникален в паре (CompanyId, Code) среди неудалённых ролей; занят → IDENTITY_ROLE_CODE_TAKEN; коллизия с кодом системной роли (CompanyId = null) допустима — уникальность по паре.
    • Все переданные права существуют в SEED-каталоге (INV-R7); отсутствующие → IDENTITY_PERMISSION_NOT_FOUND.
    • Каждое право назначимо компаниями (INV-P4): IsAssignableByCompanies = true и IsDeprecated = false; иначе — IDENTITY_PERMISSION_NOT_ASSIGNABLE с перечислением кодов-нарушителей.
  3. Что читаем. Компанию актора (для проверки статуса) и определения прав по переданным id из каталога прав (in-memory SEED-каталог). Других чтений нет.
  4. Бизнес-правила и маппинг. Создаётся роль: CompanyId берётся из claims (не из body), Code/Name/Description — из входа, IsSystem = false, Status = Active, Version = 1, фиксируется автор создания. К роли привязывается каждое переданное право (RolePermission). Поднимается доменный факт RoleCreatedEvent.
  5. Что меняется. Появляется роль с дочерней коллекцией привязок прав.
  6. Какие факты/события фиксируются. Интеграционных событий нет: роль без назначений не меняет ничьих эффективных прав (UserAccessChangedKafkaEvent публикуется только при grant/revoke назначений и изменении состава прав уже назначенной роли — role.md «События»). Доменный факт RoleCreatedEvent фиксируется вместе с ролью.
  7. Согласованность. Роль, её привязки прав и отметка идемпотентности фиксируются в одном согласованном изменении. Гонка одинакового кода разрешается уникальным индексом ux_role_definitions_company_codeIDENTITY_ROLE_CODE_TAKEN.
  8. Результат. Идентификатор и код роли, статус Active, фактический состав прав (коды), версия 1; ответ 201 с заголовком Location: /api/v1/roles/{id}.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400pattern Code / длины / дубликаты PermissionIdsСписок ошибокno retry
IDENTITY_ROLE_CODE_TAKEN409Код занят в компании«Код роли уже используется»no retry
IDENTITY_PERMISSION_NOT_FOUND400Id не из SEED-каталога (INV-R7)«Право не найдено»no retry
IDENTITY_PERMISSION_NOT_ASSIGNABLE409Право платформенное/deprecated (INV-P4)«Право недоступно для ролей компании»no retry
IDENTITY_COMPANY_SUSPENDED409Компания не Active«Компания заблокирована»no retry
IDENTITY_COMPANY_NOT_FOUND404Нет claim tenant_id«Компания не найдена»no retry
FORBIDDEN403Нет identity.roles.manage«Недостаточно прав»no retry

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

  • Новые optional поля формы — additive. Code роли — стабильный контракт компании: переименование кода = новая роль + перенос назначений (по аналогии INV-P2).
  • Logs: CompanyId, ActorUserId, RoleId, число permissions; metrics: identity_role_create_total{result}; traces: OTel; dashboard identity-overview; runbook — не применяется.

Acceptance Criteria

  • Given компания актора Active и актор с identity.roles.manage, When POST с валидными Code/Name и списком назначимых прав, Then роль создаётся Status = Active, Version = 1; ответ 201, PermissionCodes = переданный состав, Location указывает на созданную роль; зафиксирован RoleCreatedEvent; интеграционных событий нет (роль ещё не назначена).
  • Given PermissionIds = [], When POST, Then роль создаётся без прав (валидно), PermissionCodes = [].
  • Given в компании уже есть неудалённая роль с Code = X, When создаём роль с тем же Code = X, Then IDENTITY_ROLE_CODE_TAKEN (409), новая роль не создана.
  • Given системная роль платформы имеет Code = agent (CompanyId = null), When компания создаёт свою роль с Code = agent, Then роль создаётся (уникальность по паре CompanyId+Code, системный код не конфликтует).
  • Given в PermissionIds есть id вне SEED-каталога, When POST, Then IDENTITY_PERMISSION_NOT_FOUND (400).
  • Given в PermissionIds есть право с IsAssignableByCompanies = false или IsDeprecated = true, When POST, Then IDENTITY_PERMISSION_NOT_ASSIGNABLE (409) с перечислением кодов-нарушителей.
  • Given первый вызов создал роль по ключу K, When повтор с тем же Idempotency-Key = K, Then возвращается тот же Id, вторая роль не создаётся.
  • Given два параллельных запроса с одинаковым Code, When оба прошли предпроверку, Then ровно один создаёт роль, второй → IDENTITY_ROLE_CODE_TAKEN (409) на уникальном индексе.
  • Given компания актора Suspended, When POST, Then IDENTITY_COMPANY_SUSPENDED (409).
  • Given актор без identity.roles.manage, When POST, Then FORBIDDEN (403).

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

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

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