Entity / Aggregate: RoleDefinition (+ RolePermission, RoleAssignment / UserRole, ResourceScope)
Дом полей ролевой модели контекста identity: определение роли, связь роли с permissions,
назначение роли пользователю в scope (Scoped-RBAC) и правила наследования.
Назначение
Модель ролей и доступа: RoleDefinition (определение роли: ид/название/компания), RolePermission
(связь роли и прав, many-to-many) и RoleAssignment — назначение роли пользователю в scope (гибрид
Scoped-RBAC + record-level); плоская UserRole — частный случай RoleAssignment со scope
«организация»/«платформа», отдельной таблицы нет (один источник истины).
Record-level доступ (фиксации видят агент-владелец и его руководитель/группа) решается не
ролями, а полями владельца/группы на записях владеющих контекстов (crm) + permissions *.view-own /
*.view-team из permission.md.
Классификация
| Поле | Значение |
|---|---|
| Контекст | identity |
| Kind | RoleDefinition — AggregateRoot; RolePermission — child-Entity внутри агрегата роли; RoleAssignment — AggregateRoot (своя консистентность и события); ResourceScope — ValueObject-owned |
| Source of truth | identity (все четыре модели) |
| Таблицы | identity.role_definitions, identity.role_permissions, identity.role_assignments |
| Tenant isolation | RoleDefinition/RoleAssignment — TenantId = CompanyId (null — платформенные системные роли и назначения инвесторам) |
1. RoleDefinition — поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app; SEED — фиксированные Guid | Идентификатор роли. |
CompanyId | Guid? | Нет | FK → identity.companies(id), index ix_role_definitions_company_id | app | Компания-владелец роли. null — платформенная системная роль (SEED, 8 ролей). |
Code | string | Да | varchar(64), pattern ^[a-z][a-z0-9-]{1,62}$, unique ux_role_definitions_company_code (CompanyId NULLS NOT DISTINCT, Code) | app / SEED | Машинный код роли: investor, agent, agency-owner, developer-manager, pin-manager, care-operator, guest, admin + коды внутренних ролей компаний. |
Name | string | Да | varchar(128), min 1 | app / SEED | Отображаемое название («Старший менеджер»). |
Description | string? | Нет | varchar(512) | app | Назначение роли. |
IsSystem | bool | Да | default false | SEED | Системная роль: создана SEED-ом, компания не может её изменить/удалить; permissions меняем «только мы». |
Status | RoleStatus | Да | varchar(32) | app | Active / Archived. |
Permissions | IReadOnlyCollection<RolePermission> | Да (может быть пустой) | child-таблица | app | Состав прав роли (агрегат владеет). |
CreatedAt/CreatedByUserId/UpdatedAt/UpdatedByUserId/DeletedAt/DeletedByUserId | audit | — | — | app | Штампы аудита и soft-delete. |
Version | long | Да | >= 1 | app | Доменная версия. |
Enum RoleStatus: Active / Archived (архив запрещает новые назначения, действующие — отзываются миграционным job-ом или остаются до ExpiresAt; решение — отзываются немедленно, см. INV-R5).
Системные роли платформы (SEED, CompanyId = null, IsSystem = true) — 8 ролей (technical-assignment.md «Ролевая модель»)
| Code | Название | Дефолтный scope назначения | Комментарий |
|---|---|---|---|
guest | Гость | — (виртуальная: не назначается, применяется к неаутентифицированным запросам) | Публичный каталог, вход по реф-ссылке. |
investor | Инвестор | Platform (запись RoleAssignment создаётся при регистрации) | Самостоятельный пользователь. |
agent | Агент/Брокер | Organization (в агентстве — через AgentMembership; при мульти-членстве — по одному RoleAssignment(agent, Organization) на каждое агентство, ADR-0070) или Platform (независимый агент, ADR-0061) | Клиентская база, лиды, фиксации, сделки, комиссия. Роль сохраняется при выходе из агентства (scope Organization → Platform). Действующий organization-scope при мульти-членстве — scope выбранного активного агентства (см. ниже). |
agency-owner | Агентство (руководитель) | Organization | Всё, что агент, + команда, вся база/канбан, комиссионная политика. |
developer-manager | Девелопер (менеджер застройщика) | Organization, ProjectGroup или Project | Проекты/шахматка/фиксации/сделки застройщика. |
pin-manager | Менеджер ПИН | Platform | Сквозная видимость сделок, ручное управление этапами, авто-участие в чатах (US-MGR-01..06). |
care-operator | Служба заботы | Platform | Тикеты/арбитраж/вмешательства; уровни наблюдатель/оператор/супервизор — внутренние роли компании ПИН. |
admin | Администратор | — (вне identity, ADR-0053) | Каталогизируется для полноты; назначение — в админ-панели. |
Внутренние роли компаний (агентство: старший менеджер; девелопер: администратор проекта/маркетолог; забота:
наблюдатель/оператор/супервизор) — обычные RoleDefinition c CompanyId != null из каталога permissions.
Переходы роли инвестор ↔ агент (ADR-0061)
Один UserId на человека — переходы меняют роли/членство, не создают новую учётку (agent-membership.md):
- become-agent: инвестору выдаётся роль
agent; в агентстве —RoleAssignment(agent, Organization)+ActiveAgentMembership, независимому —RoleAssignment(agent, Platform)без членства (INV-R3).Investor → Agent. - leave-agency: активное membership закрывается; роль
agentсохраняется, перевыдаётсяOrganization → Platform(агент независим) — метрики целы. - become-investor: роль
agentснимается (Revoked), активное membership закрывается; рольinvestor(Platform) остаётся; исторические сделки/комиссии — у прежней компании (снапшоты crm иммутабельны).
Scope роли agent при мульти-агентстве (ADR-0070)
Один UserId может одновременно состоять в нескольких агентствах (несколько активных AgentMembership,
инвариант SingleActivePerCompany — не более одного активного членства на пару (agent, agency)). Ролевая
модель отражает это отдельным RoleAssignment(agent, Organization) на каждое активное членство
(Scope.OrganizationId = CompanyId соответствующего агентства, INV-R2). Роль agent — одна (общий
RoleDefinition), назначений в scope Organization — по числу агентств.
- Действующий organization-scope в моменте — scope выбранного активного агентства. Хотя эффективные
права аддитивны по всем Active-назначениям (правило
effectivePermissionsниже), CRM-ресурсы живут в tenant агентства (TenantId = CompanyId, ADR-0052), поэтому операции агентства выполняются в контексте активного агентства сессии (RefreshSession.ActiveCompanyId→ claimtenant_id, ADR-0070). Актуально назначениеagentтого агентства, чейCompanyIdсовпадает с активнымtenant_id. - Переключение активного агентства меняет действующий organization-scope. Смена активного агентства
(
POST /api/v1/me/active-agency) переиздаёт claimtenant_idпри refresh и, значит, переключает, какое изRoleAssignment(agent, Organization)действует в операциях агентства. Сами назначения при этом не трогаются — меняется только выбранный контекст; уже созданные ресурсы иммутабельны (AgencyCompanyId). - Независимый агент — scope
Platform. Ноль активных членств: единственное назначение —RoleAssignment(agent, Platform)(INV-R3), CRM агентства недоступен, выбор активного агентства не применяется.
2. RolePermission — поля (child внутри агрегата RoleDefinition)
| Поле | Тип | Обязательность | Ограничения | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор связи роль—право. |
RoleId | Guid | Да | FK → identity.role_definitions(id) on delete cascade, часть ux_role_permissions_role_permission | app | Роль. |
PermissionId | Guid | Да | FK → identity.permission_definitions(id) on delete restrict, часть unique ux_role_permissions_role_permission (role_id, permission_id) | app | Право из SEED-каталога (permission.md). |
CreatedAt | DateTimeOffset | Да | — | app | Когда право добавлено в роль. |
CreatedByUserId | Guid? | Нет | — | app | Кто добавил. |
Мутируется только методами агрегата RoleDefinition.GrantPermission(permissionId) /
RevokePermission(permissionId).
3. RoleAssignment — поля (назначение роли пользователю в scope)
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор назначения. |
UserId | Guid | Да | FK → identity.users(id), index ix_role_assignments_user_id | app | Кому назначена роль (UserRole.UserId). |
RoleId | Guid | Да | FK → identity.role_definitions(id) on delete restrict, index ix_role_assignments_role_id | app | Какая роль (UserRole.RoleId). |
CompanyId | Guid? | Нет | index; null для платформенных назначений (инвестор, роли ПИН) | app | Tenant назначения = компания, в чьём workspace действует роль. Для роли с RoleDefinition.CompanyId != null — обязан совпадать (INV-R2). |
Scope | ResourceScope (owned) | Да | см. VO ниже | app | Область действия: платформа / организация / группа проектов / проект. |
Status | RoleAssignmentStatus | Да | varchar(32) | app | Active / Revoked / Expired. |
GrantedByUserId | Guid? | Нет | — | app | Кто выдал (руководитель/админ/система при регистрации). |
GrantedAt | DateTimeOffset | Да | — | app | Когда выдано. |
ExpiresAt | DateTimeOffset? | Нет | > GrantedAt | app | Срок действия (для временных доступов, напр. арбитраж заботы). |
RevokedAt | DateTimeOffset? | Нет | заполняется вместе с Status = Revoked | app | Когда отозвано. |
RevokedByUserId | Guid? | Нет | — | app | Кто отозвал. |
CreatedAt/UpdatedAt/audit-поля | audit | — | — | app | Штампы аудита. |
Version | long | Да | >= 1 | app | Доменная версия. |
Уникальность: ux_role_assignments_user_role_scope unique (user_id, role_id, scope_type, coalesce
всех scope_*_id) where status = 'Active' — одно активное назначение одной роли в одном scope.
Enum RoleAssignmentStatus: Active / Revoked / Expired (переводится background-job-ом по ExpiresAt).
Value objects
ResourceScope (owned в RoleAssignment)
| Поле | Тип | Обязательность | Ограничения | Колонка |
|---|---|---|---|---|
Type | ResourceScopeType | Да | varchar(32) | scope_type |
OrganizationId | Guid? | Обязателен при Type ∈ {Organization, ProjectGroup, Project} | логическая ссылка на companies.id (FK не ставится: совпадает с CompanyId назначения) | scope_organization_id |
ProjectGroupId | Guid? | Обязателен при Type = ProjectGroup | без FK — id группы проектов из контекста chessboard (кросс-контекст, проверка существования — API chessboard на write-path) | scope_project_group_id |
ProjectId | Guid? | Обязателен при Type = Project | без FK — id проекта из chessboard | scope_project_id |
Enum ResourceScopeType и наследование
| Значение | Что покрывает | Наследование вниз |
|---|---|---|
Platform | Вся платформа (роли ПИН, инвестор) | → все организации → все группы → все проекты |
Organization | Один workspace компании | → все группы проектов компании → все проекты |
ProjectGroup | Именованная группа проектов застройщика | → все проекты группы |
Project | Один проект | только он |
Правило эффективных прав (identity отдаёт как алгоритм авторизации, кэшируемый потребителями):
effectivePermissions(user, resource) = ∪ permissions(role) для всех Active RoleAssignment, чей Scope
покрывает resource по иерархии Platform ⊃ Organization ⊃ ProjectGroup ⊃ Project — «все проекты» = роль в
scope Organization, «только проект X» = та же роль в scope Project(X). Deny-правил нет:
модель аддитивная; сужение — назначением в более узкий scope.
Назначение в scope: валидация и свежесть эффективных прав
Уточняет write-path назначения (RoleAssignment.Create/Grant) и окно eventual-consistency для потребителей
эффективных прав — дополнительно к INV-R2..R8.
- Scope-floor по
MinScopeType(permission.md). Назначить роль можно только в scope не ниже, чем требует самая «широкая» из её permissions (PermissionDefinition.MinScopeType). Платформенные права (MinScopeType = Platform,IsAssignableByCompanies = false) не входят в роли компаний (INV-P4), поэтому роль компании всегда назначима вOrganizationи ниже; роль сPlatform-правами назначается только в scopePlatformсистемным ролям (INV-R3) — нарушение →IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN.
INV-R4 задаёт форму ResourceScope; существование ProjectGroupId/ProjectId проверяется синхронным
обращением к chessboard на write-path: несуществующая группа/проект → ValidationError; chessboard
недоступен → fail-closed (назначение отклоняется, «висячий» scope не создаётся). Удаление/архив ресурса
после назначения — eventual: consumer
chessboard-project-deleted отзывает висячие
Project-назначения (окно ≤ 5 мин), enforcement владеющих контекстов дополнительно проверяет существование
ресурса.
Любое изменение назначений/состава прав публикует UserAccessChangedKafkaEvent (ниже); потребители
сбрасывают кэш авторизации в окне < 1 мин (gateway < 30 с). В этом окне только что отозванная роль может
оставаться эффективной, а только что выданная — ещё не действовать; снапшот активных назначений в событии
self-healing (последнее доставленное состояние применяется независимо от пропусков и относительного порядка
с fan-out по роли).
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|---|---|---|---|
RoleDefinition → RolePermission | owned-collection | role_permissions.role_id | include при GetById(roleId) | cascade |
RolePermission → PermissionDefinition | reference | permission_id | GetByIds при построении permission-set | restrict |
RoleAssignment → RoleDefinition | reference | role_id | GetByIds (батч по назначениям пользователя) | restrict |
RoleAssignment → UserEntity | reference | user_id | Filter(a => a.UserId == userId && a.Status == Active) | cascade при архиве пользователя |
RoleAssignment → CompanyEntity | reference | company_id | — | restrict |
ResourceScope → chessboard (ProjectGroupId/ProjectId) | cross-context reference | Guid без FK | валидация существования — запрос chessboard в момент назначения; далее — eventual (pin.chessboard.projects.v1 на удаление проекта → job отзывает висячие назначения) | soft |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|---|---|---|
| INV-R1 SystemRoleImmutable | IsSystem = true → компания не меняет Name/Code/Permissions/Status | guard в методах агрегата; admin-SEED — единственный писатель | IDENTITY_ROLE_SYSTEM_IMMUTABLE |
| INV-R2 RoleCompanyMatch | Роль с CompanyId != null назначается только в scope её компании (Scope.OrganizationId = RoleDefinition.CompanyId) | RoleAssignment.Create | IDENTITY_ASSIGNMENT_COMPANY_MISMATCH |
| INV-R3 PlatformScopeRestricted | Scope.Type = Platform допустим только для системных ролей investor, agent (независимый агент без активного членства, ADR-0061), pin-manager, care-operator | RoleAssignment.Create | IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN |
| INV-R4 ScopeShape | Комбинация nullable-полей ResourceScope строго соответствует Type (таблица VO) | конструктор VO ResourceScope.Of(...) | ValidationError |
| INV-R5 ArchiveRevokes | RoleDefinition.Archive() отзывает все Active назначения этой роли (в одном согласованном изменении) | операция архивации роли | — |
| INV-R6 UniqueActiveAssignment | Не более одного Active назначения (user, role, scope) | unique partial index (проверка до вставки) | IDENTITY_ASSIGNMENT_DUPLICATE |
| INV-R7 PermissionExists | GrantPermission — только для существующего PermissionDefinition | добавление права в роль | IDENTITY_PERMISSION_NOT_FOUND |
| INV-R8 GranterAuthority | Выдающий имеет identity.role-assignments.manage в scope, покрывающем выдаваемый scope | проверка прав выдающего | 403 FORBIDDEN |
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
RoleCreatedEvent / RoleUpdatedEvent / RoleArchivedEvent | доменное | CRUD роли | RoleId, CompanyId, дельта permissions |
RoleAssignmentGrantedEvent | доменное | Grant | AssignmentId, UserId, RoleId, Scope |
RoleAssignmentRevokedEvent | доменное | Revoke/Expire/каскад архива | то же + причина |
UserAccessChangedKafkaEvent | интеграционное (ContractVersion = 1, topic pin.identity.access.v1, key = UserId) | Любое изменение эффективных прав пользователя: grant/revoke/expire назначения, изменение состава permissions роли (fan-out по всем пользователям роли — background-job батчами) | UserId, CompanyId?, список активных назначений {RoleCode, ScopeType, ScopeIds}, Version; consumers (crm, catalog, chessboard, gateway) сбрасывают кэш авторизации |
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | role_definitions: PK; ux_role_definitions_company_code; ix_role_definitions_company_id. role_permissions: PK; ux_role_permissions_role_permission; ix_role_permissions_permission_id. role_assignments: PK; ux_role_assignments_user_role_scope (partial, Active); ix_role_assignments_user_id; ix_role_assignments_role_id; ix_role_assignments_scope btree (scope_type, scope_organization_id, scope_project_id); ix_role_assignments_expires_at btree where status = 'Active' and expires_at is not null (для job-а) |
| Партиционирование / retention | none; отозванные назначения хранятся бессрочно (аудит доступов) |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую); экспирация — фоновый job по ExpiresAt <= now |
| Permissions / PII | Управление ролями — identity.roles.manage (scope организации; системные роли — только admin-API); назначения — identity.role-assignments.manage; PII нет |
| Audit | Полная история назначений (INV: записи не удаляются, только Revoked); actor-stamping GrantedBy/RevokedBy |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (5): Entity / Aggregate: AgentMembershipEntity, Entity / Aggregate: CompanyEntity, Entity / Aggregate: CompanyInvitationEntity, Entity / Aggregate: UserEntity, Entity: PermissionDefinition + полный SEED-каталог permissions
- API (21): DELETE /api/admin/v1/users/{userId} — Удаление пользователя: архив vs полное удаление (admin), GET /api/v1/companies/my/users — Список пользователей компании, GET /api/v1/roles — Список ролей компании, GET /api/v1/users/{userId}/effective-permissions — Эффективные permissions пользователя, Identity Agent API — реестр методов и общие правила области, Identity Company-User API — реестр методов и общие правила области, Identity Internal API — реестр методов и общие правила области, POST /api/v1/companies/{companyId}/agents/{agentUserId}/terminate — Исключение агента из агентства, POST /api/v1/companies/my/agent-invitations — Приглашение агента по номеру телефона, POST /api/v1/companies/my/users/invitations — Приглашение пользователя в компанию, POST /api/v1/me/agency-membership/leave — Выход агента из агентства, POST /api/v1/me/agent-invitations/{invitationId}/accept — Принятие приглашения в агентство (+9)
- Use Cases (7): UC-IDN-001. Вход/регистрация по СМС (с заполнением имени и фиксацией реферальной ссылки), UC-IDN-005. Создание роли компании и назначение агенту в scope проекта, UC-IDN-006. Приглашение менеджера (агента) в компанию, UC-IDN-008. Проверка доступа сервисом (internal introspection эффективных прав), UC-IDN-009. Приглашение агента по телефону и его принятие, UC-IDN-010. Агент выходит из агентства, сохраняя метрики, UC-IDN-011. Инвестор становится агентом
- События (1): Event Contract: pin.identity.user-access-changed (UserAccessChangedKafkaEvent)
- Консюмеры (2): Consumer: Chessboard Project Deleted (pin-identity-chessboard-projects), Consumer: Talent Identity Sync (pin-identity-talent-sync)
- Фоновые задачи (4): Background Job: AccessFanOutJob — веерная рассылка UserAccessChangedKafkaEvent при смене состава прав роли, Background Job: ExpireRoleAssignmentsJob, Background Job: ExpireSessionsJob, Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Identity)
- adr (3): ADR-0061: Независимый агент, отвязка от агентства и приглашения по номеру телефона, ADR-0070: Мульти-агентство и контекст активного агентства, ADR-0072: Увольнение/исключение агента и каскад доступа
- index.md (1): Identity Service (Pin.Identity)
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- permission.md — SEED-каталог permissions (139 позиций).
- user.md, company.md, agent-membership.md (роль
agent:Organization ↔ Platform). - ADR-0052 («tenant — граница данных, scope — граница прав»), ADR-0053, ADR-0061 (независимый агент, переходы ролей), ADR-0070 (мульти-агентство:
RoleAssignment(agent, Organization)на каждое членство, действующий scope — активное агентство), ADR-0025 (роли/permissions/scope). - Record-level реализация фиксаций/лидов —
docs/06-services/crm/(владелец +view-own/view-team).