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

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
KindRoleDefinitionAggregateRoot; RolePermission — child-Entity внутри агрегата роли; RoleAssignmentAggregateRoot (своя консистентность и события); ResourceScopeValueObject-owned
Source of truthidentity (все четыре модели)
Таблицыidentity.role_definitions, identity.role_permissions, identity.role_assignments
Tenant isolationRoleDefinition/RoleAssignmentTenantId = CompanyId (null — платформенные системные роли и назначения инвесторам)

1. RoleDefinition — поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKapp; SEED — фиксированные GuidИдентификатор роли.
CompanyIdGuid?НетFK → identity.companies(id), index ix_role_definitions_company_idappКомпания-владелец роли. null — платформенная системная роль (SEED, 8 ролей).
CodestringДа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 + коды внутренних ролей компаний.
NamestringДаvarchar(128), min 1app / SEEDОтображаемое название («Старший менеджер»).
Descriptionstring?Нетvarchar(512)appНазначение роли.
IsSystemboolДаdefault falseSEEDСистемная роль: создана SEED-ом, компания не может её изменить/удалить; permissions меняем «только мы».
StatusRoleStatusДаvarchar(32)appActive / Archived.
PermissionsIReadOnlyCollection<RolePermission>Да (может быть пустой)child-таблицаappСостав прав роли (агрегат владеет).
CreatedAt/CreatedByUserId/UpdatedAt/UpdatedByUserId/DeletedAt/DeletedByUserIdauditappШтампы аудита и soft-delete.
VersionlongДа>= 1appДоменная версия.

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) + Active AgentMembership, независимому — 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 — по числу агентств.

Действующий scope в моменте
  • Действующий organization-scope в моменте — scope выбранного активного агентства. Хотя эффективные права аддитивны по всем Active-назначениям (правило effectivePermissions ниже), CRM-ресурсы живут в tenant агентства (TenantId = CompanyId, ADR-0052), поэтому операции агентства выполняются в контексте активного агентства сессии (RefreshSession.ActiveCompanyId → claim tenant_id, ADR-0070). Актуально назначение agent того агентства, чей CompanyId совпадает с активным tenant_id.
  • Переключение активного агентства меняет действующий organization-scope. Смена активного агентства (POST /api/v1/me/active-agency) переиздаёт claim tenant_id при refresh и, значит, переключает, какое из RoleAssignment(agent, Organization) действует в операциях агентства. Сами назначения при этом не трогаются — меняется только выбранный контекст; уже созданные ресурсы иммутабельны (AgencyCompanyId).
  • Независимый агент — scope Platform. Ноль активных членств: единственное назначение — RoleAssignment(agent, Platform) (INV-R3), CRM агентства недоступен, выбор активного агентства не применяется.

2. RolePermission — поля (child внутри агрегата RoleDefinition)

ПолеТипОбязательностьОграниченияИсточникОписание
IdGuidДаPKappИдентификатор связи роль—право.
RoleIdGuidДаFK → identity.role_definitions(id) on delete cascade, часть ux_role_permissions_role_permissionappРоль.
PermissionIdGuidДаFK → identity.permission_definitions(id) on delete restrict, часть unique ux_role_permissions_role_permission (role_id, permission_id)appПраво из SEED-каталога (permission.md).
CreatedAtDateTimeOffsetДаappКогда право добавлено в роль.
CreatedByUserIdGuid?НетappКто добавил.

Мутируется только методами агрегата RoleDefinition.GrantPermission(permissionId) / RevokePermission(permissionId).

3. RoleAssignment — поля (назначение роли пользователю в scope)

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор назначения.
UserIdGuidДаFK → identity.users(id), index ix_role_assignments_user_idappКому назначена роль (UserRole.UserId).
RoleIdGuidДаFK → identity.role_definitions(id) on delete restrict, index ix_role_assignments_role_idappКакая роль (UserRole.RoleId).
CompanyIdGuid?Нетindex; null для платформенных назначений (инвестор, роли ПИН)appTenant назначения = компания, в чьём workspace действует роль. Для роли с RoleDefinition.CompanyId != null — обязан совпадать (INV-R2).
ScopeResourceScope (owned)Дасм. VO нижеappОбласть действия: платформа / организация / группа проектов / проект.
StatusRoleAssignmentStatusДаvarchar(32)appActive / Revoked / Expired.
GrantedByUserIdGuid?НетappКто выдал (руководитель/админ/система при регистрации).
GrantedAtDateTimeOffsetДаappКогда выдано.
ExpiresAtDateTimeOffset?Нет> GrantedAtappСрок действия (для временных доступов, напр. арбитраж заботы).
RevokedAtDateTimeOffset?Нетзаполняется вместе с Status = RevokedappКогда отозвано.
RevokedByUserIdGuid?НетappКто отозвал.
CreatedAt/UpdatedAt/audit-поляauditappШтампы аудита.
VersionlongДа>= 1appДоменная версия.

Уникальность: 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)

ПолеТипОбязательностьОграниченияКолонка
TypeResourceScopeTypeДаvarchar(32)scope_type
OrganizationIdGuid?Обязателен при Type ∈ {Organization, ProjectGroup, Project}логическая ссылка на companies.id (FK не ставится: совпадает с CompanyId назначения)scope_organization_id
ProjectGroupIdGuid?Обязателен при Type = ProjectGroupбез FK — id группы проектов из контекста chessboard (кросс-контекст, проверка существования — API chessboard на write-path)scope_project_group_id
ProjectIdGuid?Обязателен при Type = Projectбез FK — id проекта из chessboardscope_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-правами назначается только в scope Platform системным ролям (INV-R3) — нарушение → IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN.
Кросс-контекстная проверка существования scope (chessboard)

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 → RolePermissionowned-collectionrole_permissions.role_idinclude при GetById(roleId)cascade
RolePermission → PermissionDefinitionreferencepermission_idGetByIds при построении permission-setrestrict
RoleAssignment → RoleDefinitionreferencerole_idGetByIds (батч по назначениям пользователя)restrict
RoleAssignment → UserEntityreferenceuser_idFilter(a => a.UserId == userId && a.Status == Active)cascade при архиве пользователя
RoleAssignment → CompanyEntityreferencecompany_idrestrict
ResourceScope → chessboard (ProjectGroupId/ProjectId)cross-context referenceGuid без FKвалидация существования — запрос chessboard в момент назначения; далее — eventual (pin.chessboard.projects.v1 на удаление проекта → job отзывает висячие назначения)soft

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-R1 SystemRoleImmutableIsSystem = true → компания не меняет Name/Code/Permissions/Statusguard в методах агрегата; admin-SEED — единственный писательIDENTITY_ROLE_SYSTEM_IMMUTABLE
INV-R2 RoleCompanyMatchРоль с CompanyId != null назначается только в scope её компании (Scope.OrganizationId = RoleDefinition.CompanyId)RoleAssignment.CreateIDENTITY_ASSIGNMENT_COMPANY_MISMATCH
INV-R3 PlatformScopeRestrictedScope.Type = Platform допустим только для системных ролей investor, agent (независимый агент без активного членства, ADR-0061), pin-manager, care-operatorRoleAssignment.CreateIDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN
INV-R4 ScopeShapeКомбинация nullable-полей ResourceScope строго соответствует Type (таблица VO)конструктор VO ResourceScope.Of(...)ValidationError
INV-R5 ArchiveRevokesRoleDefinition.Archive() отзывает все Active назначения этой роли (в одном согласованном изменении)операция архивации роли
INV-R6 UniqueActiveAssignmentНе более одного Active назначения (user, role, scope)unique partial index (проверка до вставки)IDENTITY_ASSIGNMENT_DUPLICATE
INV-R7 PermissionExistsGrantPermission — только для существующего 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доменноеGrantAssignmentId, 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-а)
Партиционирование / retentionnone; отозванные назначения хранятся бессрочно (аудит доступов)
Concurrencyоптимистическая блокировка (правка не должна затирать чужую); экспирация — фоновый job по ExpiresAt <= now
Permissions / PIIУправление ролями — identity.roles.manage (scope организации; системные роли — только admin-API); назначения — identity.role-assignments.manage; PII нет
AuditПолная история назначений (INV: записи не удаляются, только Revoked); actor-stamping GrantedBy/RevokedBy

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

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

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

  • 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).