POST /api/v1/roles — Создание роли компании
Создание внутренней роли компании (RoleDefinition с CompanyId != null) с начальным составом прав из
SEED-каталога. Компания сама заполняет таблицу определения ролей под свою оргструктуру.
Назначение
Руководитель агентства/менеджер застройщика создаёт роль под свою оргструктуру («Старший менеджер»,
«Маркетолог», «Администратор проекта») и наделяет её правами из каталога. Owning-контекст — identity
(дом полей — domain/role.md). Feature group — roles.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | public |
| Feature group | roles |
| Статус | draft |
| Документ | docs/06-services/identity/api/company-user/create-company-role.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (пользователь компании) |
| Auth policy / Permissions | identity.roles.manage (дефолт: agency-owner, developer-manager) |
| Scope (RBAC) | организация: Active RoleAssignment со scope Platform или Organization(CompanyId) |
| Record-level | не применяется |
| Tenant isolation | TenantId = CompanyId: роль создаётся только в своей компании (CompanyId из claims, не из body) |
| Audit | create: CreatedByUserId, Version = 1, доменное событие RoleCreatedEvent |
| MFA/approval | не требуется; системные роли этим методом не создаются (только SEED, INV-R1) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/roles |
| Success status | 201 |
| Idempotency header | Idempotency-Key (Guid) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims | Guid | Да | Required | — |
CompanyId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
IdempotencyKey | header Idempotency-Key | Guid | Да | Required | — |
Code | body | string | Да | pattern ^[a-z][a-z0-9-]{1,62}$; уникальность в паре (CompanyId, Code) — ux_role_definitions_company_code | — |
Name | body | string | Да | 1..128 | — |
Description | body | string? | Нет | <= 512 | — |
PermissionIds | body | IReadOnlyList<Guid> | Да (может быть пустым) | <= 113 элементов, без дубликатов; каждый — существующий PermissionDefinition с IsAssignableByCompanies = true и IsDeprecated = false (INV-P4) | — |
Модель ответа CreateRoleCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | RoleDefinition.Id | — | Id созданной роли. |
Code | string | Да | RoleDefinition.Code | — | Код. |
Status | string (enum RoleStatus) | Да | RoleDefinition.Status | — | Всегда Active. |
PermissionCodes | IReadOnlyList<string> | Да | join PermissionDefinition.Code | — | Фактический состав прав. |
Version | long | Да | RoleDefinition.Version | — | 1. |
Предусловия и постусловия
Предусловия. Актор аутентифицирован и имеет identity.roles.manage в scope Platform или
Organization(CompanyId); компания актора существует и в статусе Active; каталог прав (SEED) прогрет.
Постусловия при успехе. В компании актора появилась роль Status = Active, Version = 1,
IsSystem = false, с составом прав ровно из PermissionIds; зафиксирован доменный факт
RoleCreatedEvent. Отметка идемпотентности по Idempotency-Key сохранена: повтор вернёт тот же Id.
Постусловия при отказе. Состояние не изменилось: роль не создана, событий нет.
Свежесозданная роль ещё никому не назначена, поэтому ничьи эффективные права не изменились —
UserAccessChangedKafkaEvent уйдёт только при последующем grant/set-permissions.
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь компании; tenant — его компания (
CompanyId); требуется правоidentity.roles.manageв scopeOrganization(CompanyId). Повтор с тем жеIdempotency-Keyвозвращает ранее сохранённый результат. - Проверка входа и предусловий.
- Синтаксис по таблице входных данных:
Codeсоответствует паттерну, вPermissionIdsнет дубликатов; иначе —ValidationError. - Компания актора существует и активна;
Status != Active→IDENTITY_COMPANY_SUSPENDED; отсутствие claimtenant_id→IDENTITY_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с перечислением кодов-нарушителей.
- Синтаксис по таблице входных данных:
- Что читаем. Компанию актора (для проверки статуса) и определения прав по переданным id из каталога прав (in-memory SEED-каталог). Других чтений нет.
- Бизнес-правила и маппинг. Создаётся роль:
CompanyIdберётся из claims (не из body),Code/Name/Description— из входа,IsSystem = false,Status = Active,Version = 1, фиксируется автор создания. К роли привязывается каждое переданное право (RolePermission). Поднимается доменный фактRoleCreatedEvent. - Что меняется. Появляется роль с дочерней коллекцией привязок прав.
- Какие факты/события фиксируются. Интеграционных событий нет: роль без назначений не меняет ничьих
эффективных прав (
UserAccessChangedKafkaEventпубликуется только при grant/revoke назначений и изменении состава прав уже назначенной роли — role.md «События»). Доменный фактRoleCreatedEventфиксируется вместе с ролью. - Согласованность. Роль, её привязки прав и отметка идемпотентности фиксируются в одном согласованном
изменении. Гонка одинакового кода разрешается уникальным индексом
ux_role_definitions_company_code→IDENTITY_ROLE_CODE_TAKEN. - Результат. Идентификатор и код роли, статус
Active, фактический состав прав (коды), версия1; ответ201с заголовкомLocation: /api/v1/roles/{id}.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | pattern Code / длины / дубликаты PermissionIds | Список ошибок | no retry |
IDENTITY_ROLE_CODE_TAKEN | 409 | Код занят в компании | «Код роли уже используется» | no retry |
IDENTITY_PERMISSION_NOT_FOUND | 400 | Id не из SEED-каталога (INV-R7) | «Право не найдено» | no retry |
IDENTITY_PERMISSION_NOT_ASSIGNABLE | 409 | Право платформенное/deprecated (INV-P4) | «Право недоступно для ролей компании» | no retry |
IDENTITY_COMPANY_SUSPENDED | 409 | Компания не Active | «Компания заблокирована» | no retry |
IDENTITY_COMPANY_NOT_FOUND | 404 | Нет claim tenant_id | «Компания не найдена» | no retry |
FORBIDDEN | 403 | Нет identity.roles.manage | «Недостаточно прав» | no retry |
Совместимость и наблюдаемость
- Новые optional поля формы — additive.
Codeроли — стабильный контракт компании: переименование кода = новая роль + перенос назначений (по аналогии INV-P2). - Logs:
CompanyId,ActorUserId,RoleId, число permissions; metrics:identity_role_create_total{result}; traces: OTel; dashboardidentity-overview; runbook — не применяется.
Acceptance Criteria
- Given компания актора
Activeи актор сidentity.roles.manage, WhenPOSTс валиднымиCode/Nameи списком назначимых прав, Then роль создаётсяStatus = Active,Version = 1; ответ201,PermissionCodes= переданный состав,Locationуказывает на созданную роль; зафиксированRoleCreatedEvent; интеграционных событий нет (роль ещё не назначена). - Given
PermissionIds = [], WhenPOST, Then роль создаётся без прав (валидно),PermissionCodes = []. - Given в компании уже есть неудалённая роль с
Code = X, When создаём роль с тем жеCode = X, ThenIDENTITY_ROLE_CODE_TAKEN(409), новая роль не создана. - Given системная роль платформы имеет
Code = agent(CompanyId = null), When компания создаёт свою роль сCode = agent, Then роль создаётся (уникальность по пареCompanyId+Code, системный код не конфликтует). - Given в
PermissionIdsесть id вне SEED-каталога, WhenPOST, ThenIDENTITY_PERMISSION_NOT_FOUND(400). - Given в
PermissionIdsесть право сIsAssignableByCompanies = falseилиIsDeprecated = true, WhenPOST, ThenIDENTITY_PERMISSION_NOT_ASSIGNABLE(409) с перечислением кодов-нарушителей. - Given первый вызов создал роль по ключу
K, When повтор с тем жеIdempotency-Key = K, Then возвращается тот жеId, вторая роль не создаётся. - Given два параллельных запроса с одинаковым
Code, When оба прошли предпроверку, Then ровно один создаёт роль, второй →IDENTITY_ROLE_CODE_TAKEN(409) на уникальном индексе. - Given компания актора
Suspended, WhenPOST, ThenIDENTITY_COMPANY_SUSPENDED(409). - Given актор без
identity.roles.manage, WhenPOST, ThenFORBIDDEN(403).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): GET /api/v1/permissions — Каталог permissions (SEED), GET /api/v1/roles — Список ролей компании, Identity Company-User API — реестр методов и общие правила области
- Use Cases (1): UC-IDN-005. Создание роли компании и назначение агенту в scope проекта
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: domain/role.md (INV-R1/R7), domain/permission.md (INV-P4, каталог 113).
- Состав прав: set-role-permissions.md; назначение: grant-role-assignment.md.