GET /api/v1/users/{userId}/effective-permissions — Эффективные permissions пользователя
Вычисление действующего набора прав пользователя с учётом наследования scope
(Platform ⊃ Organization ⊃ ProjectGroup ⊃ Project, role.md): объединение permissions всех Active
RoleAssignment, чей scope покрывает запрошенный ресурс. Используется UI («что мне доступно» —
скрытие кнопок) и админ-экранами компании («что может сотрудник»).
Назначение
Два сценария одного контракта: userId = self — свои права (любой аутентифицированный);
userId != self — права сотрудника своей компании (требует identity.role-assignments.view).
Owning-контекст — identity. Feature group — permissions.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | public |
| Feature group | permissions |
| Статус | draft |
| Документ | docs/06-services/identity/api/company-user/get-user-effective-permissions.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User |
| Auth policy / Permissions | self: authenticated (без permission); чужой: identity.role-assignments.view в scope Organization(CompanyId) (дефолт: agency-owner, developer-manager, care-operator) |
| Scope (RBAC) | организация — при запросе чужого пользователя |
| Record-level | чужой userId обязан принадлежать компании актора (target.CompanyId == CompanyId); кросс-компания — только платформенные роли ПИН через identity.users.view-all |
| Tenant isolation | TenantId = CompanyId; инвестора (CompanyId = null) может запросить только он сам |
| Audit | read-sensitive при userId != self: лог доступа с ActorUserId/UserId |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/v1/users/{userId}/effective-permissions (для self допустим alias /api/v1/users/my/effective-permissions) |
| Success status | 200 |
| Idempotency header | не применяется |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims | Guid | Да | Required | — |
CompanyId | identity claims (tenant_id) | Guid? | Нет (нет у инвестора) | — | Tenant isolation |
UserId | route {userId} | Guid | Да | != Guid.Empty; my → ActorUserId | record-level guard |
ResourceOrganizationId | query | Guid? | Нет | ресурс-координаты для среза прав; без них возвращается полный набор по всем scope | — |
ResourceProjectGroupId | query | Guid? | Нет | допустим только вместе с ResourceOrganizationId | — |
ResourceProjectId | query | Guid? | Нет | допустим только вместе с ResourceOrganizationId; цепочку project → group → organization резолвит вызывающий (identity не владеет данными chessboard) | — |
Модель ответа GetUserEffectivePermissionsQueryResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
UserId | Guid | Да | echo | — | Чьи права. |
PermissionCodes | IReadOnlyList<string> | Да | computed (шаг 4) | — | Отсортированное объединение кодов; при ресурс-срезе — только покрывающие ресурс. |
Assignments | IReadOnlyList<EffectiveAssignmentModel> | Да | Active RoleAssignment | — | Из чего складываются права (для отладки/UI). |
ComputedAt | DateTimeOffset | Да | now | — | Момент вычисления (кэш-метка). |
EffectiveAssignmentModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
AssignmentId | Guid | Да | RoleAssignment.Id | — | Id назначения. |
RoleCode | string | Да | RoleDefinition.Code | — | Код роли. |
RoleName | string | Да | RoleDefinition.Name | — | Название. |
ScopeType | string (enum ResourceScopeType) | Да | ResourceScope.Type | — | Scope назначения. |
ScopeOrganizationId | Guid? | Нет | ResourceScope.OrganizationId | — | — |
ScopeProjectGroupId | Guid? | Нет | ResourceScope.ProjectGroupId | — | — |
ScopeProjectId | Guid? | Нет | ResourceScope.ProjectId | — | — |
ExpiresAt | DateTimeOffset? | Нет | RoleAssignment.ExpiresAt | — | Срок действия. |
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь;
CompanyId— из claimtenant_id(у инвестора отсутствует). Два режима: self (UserId == ActorUserId) — без permission; чужой пользователь — требуетсяidentity.role-assignments.viewв scopeOrganization(CompanyId), иначе —FORBIDDEN. Aliasmyв маршруте трактуется какUserId = ActorUserId. - Проверка входа и предусловий.
UserIdнепустой; ресурс-поля среза (ResourceProjectGroupId/ResourceProjectId) допустимы только вместе сResourceOrganizationId; иначе —ValidationError.- Целевой пользователь существует; иначе —
IDENTITY_USER_NOT_FOUND. - При запросе чужого — принадлежность компании актора (
target.CompanyId == CompanyId); иначе —IDENTITY_USER_NOT_FOUND(кросс-tenant видимость — толькоidentity.users.view-allу платформенных ролей ПИН). - Если пользователь
Blocked/Archived— права пустые (не ошибка): возвращается пустой набор сAssignments = []; enforcement блокировки происходит на входе.
- Что читаем. Активные назначения пользователя (
RoleAssignmentсоStatus = Active, не истёкшие поExpiresAt); роли этих назначений с их правами; коды прав — из in-memory SEED-каталога. Self-режим сначала пробует кэшperm:{UserId}(TTL 5 мин; инвалидация — событиеpin.identity.access.v1и мутации назначений). - Бизнес-правила и маппинг. По формуле role.md эффективные права — объединение прав всех активных
назначений, чей scope покрывает запрошенный ресурс (
Covers(assignmentScope, resource)):Platform→ покрывает всё;Organization(o)→ ресурс сResourceOrganizationId == o(и любые его группы/проекты);ProjectGroup(g)→ ресурс сResourceProjectGroupId == gилиResourceProjectId, принадлежащий группеg(принадлежность — из query-цепочки вызывающего);Project(p)→ только ресурс сResourceProjectId == p. Без ресурс-параметров покрытие не фильтруется — берутся все назначения (полный набор «где-либо»). Deny-правил нет — модель аддитивная.PermissionCodes— отсортированное объединение кодов покрывающих назначений;Assignments— сами покрывающие назначения.
- Результат. Набор кодов прав, состав назначений и момент вычисления (
200). Self-результат кладётся в кэшperm:{UserId}(TTL 5 мин). Консистентность — eventual относительно недавних grant/revoke на других инстансах (≤ 5 мин или до события инвалидации).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Проекции / кэш | кэш perm:{userId} — запись self-результата, TTL 5 мин |
Кросс-сервисные контракты и согласованность
- Операция — человеко-читаемый ответ на вопрос «что мне/сотруднику доступно» (скрытие кнопок в UI, экран
«что может сотрудник»); машинную проверку для сервисов даёт internal-контракт
check-permission.md по той же формуле покрытия scope. Оба разделяют
один источник — активные
RoleAssignmentи SEED-каталог прав; расхождения между ними быть не должно. - Свежесть относительно grant/revoke: после
grant-role-assignment/revoke-role-assignment/set-role-permissionsidentity инвалидируетperm:{UserId}— self-режим отражает изменение сразу (read-your-writes) либо не позже TTL 5 мин, если инвалидация не сработала. Потребители прав в других сервисах (crm,catalog,chessboard, gateway) держат свою копию, которую сбрасывает событиеpin.identity.access.v1; в окне до его обработки downstream-энфорсмент может краткий промежуток видеть прежний набор — этот endpoint показывает уже актуальное состояние из источника. - Модель аддитивна (deny-правил нет): клиент обязан игнорировать неизвестные коды прав (deny by default на enforcement-стороне) — добавление нового кода в каталог не ломает существующие клиенты.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Ресурс-поля без ResourceOrganizationId | Список ошибок | no retry |
IDENTITY_USER_NOT_FOUND | 404 | Пользователь не существует / чужой компании | «Пользователь не найден» | no retry |
FORBIDDEN | 403 | Чужой пользователь без identity.role-assignments.view | «Недостаточно прав» | no retry |
UNAUTHORIZED | 401 | Нет токена | — | retry после refresh |
Деградация при недоступности кэша
Redis недоступен → вычисление из PostgreSQL без кэша (README identity «Failure mode»).
Совместимость и наблюдаемость
PermissionCodes— стабильные коды SEED-каталога (INV-P2); клиент обязан игнорировать неизвестные коды (deny by default на enforcement-стороне — permission.md «Совместимость»).- Logs:
ActorUserId,UserId, режим self/foreign, cache hit/miss; metrics:identity_effective_permissions_total{mode, cache}, p99 latency (SLO < 50 мс с кэшом — README); traces: OTel; dashboardidentity-overview; runbook — не применяется.
Acceptance Criteria
- Given пользователь с назначениями роли в scope
OrganizationиProject, WhenGET /users/my/effective-permissionsбез ресурс-параметров, Then200с объединением кодов всех активных назначений;Assignmentsперечисляет источники; результат кладётся в кэшperm:{UserId}. - Given пользователь имеет роль в scope
Project(P1)и роль в scopeOrganization, When запрос сResourceProjectId = P2(не входит вP1), Then в наборе — только права организационного назначения (покрывает любой проект), праваProject(P1)не попадают. - Given актор с
identity.role-assignments.viewи целевой пользователь его компании, WhenGET /users/{id}/effective-permissions, Then200с правами сотрудника; факт доступа логируется (read-sensitive). - Given актор без
identity.role-assignments.view, When запрос чужогоuserId, ThenFORBIDDEN(403). - Given целевой пользователь другой компании, When запрос, Then
IDENTITY_USER_NOT_FOUND(404) — кросс-tenant существование не раскрывается. - Given целевой пользователь
Blocked/Archived, When запрос, Then200сPermissionCodes = [],Assignments = [](не ошибка). - Given
ResourceProjectIdзадан безResourceOrganizationId, When запрос, ThenValidationError(400). - Given у пользователя есть назначение с
ExpiresAt < now, When запрос, Then права этого назначения в набор не входят. - Given кэш недоступен, When self-запрос, Then результат вычисляется из PostgreSQL без кэша
(
200, деградация без ошибки).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): Identity Company-User API — реестр методов и общие правила области, Identity Internal API — реестр методов и общие правила области, POST /internal/v1/authorize/check-permission — Проверка доступа (internal)
- Use Cases (1): UC-IDN-008. Проверка доступа сервисом (internal introspection эффективных прав)
- registries (1): Реестр API-методов PIN
Связанные документы
- Формула и иерархия scope: domain/role.md; каталог: domain/permission.md.
- Машинная проверка для сервисов: check-permission.md (internal).