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

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 areapublic
Feature grouppermissions
Статусdraft
Документdocs/06-services/identity/api/company-user/get-user-effective-permissions.md

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

ПроверкаЗначение
Actor typeUser
Auth policy / Permissionsself: 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 isolationTenantId = CompanyId; инвестора (CompanyId = null) может запросить только он сам
Auditread-sensitive при userId != self: лог доступа с ActorUserId/UserId

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/users/{userId}/effective-permissions (для self допустим alias /api/v1/users/my/effective-permissions)
Success status200
Idempotency headerне применяется
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДаRequired
CompanyIdidentity claims (tenant_id)Guid?Нет (нет у инвестора)Tenant isolation
UserIdroute {userId}GuidДа!= Guid.Empty; myActorUserIdrecord-level guard
ResourceOrganizationIdqueryGuid?Нетресурс-координаты для среза прав; без них возвращается полный набор по всем scope
ResourceProjectGroupIdqueryGuid?Нетдопустим только вместе с ResourceOrganizationId
ResourceProjectIdqueryGuid?Нетдопустим только вместе с ResourceOrganizationId; цепочку project → group → organization резолвит вызывающий (identity не владеет данными chessboard)

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

ПолеТипОбязательностьИсточникMaskingОписание
UserIdGuidДаechoЧьи права.
PermissionCodesIReadOnlyList<string>Даcomputed (шаг 4)Отсортированное объединение кодов; при ресурс-срезе — только покрывающие ресурс.
AssignmentsIReadOnlyList<EffectiveAssignmentModel>ДаActive RoleAssignmentИз чего складываются права (для отладки/UI).
ComputedAtDateTimeOffsetДаnowМомент вычисления (кэш-метка).

EffectiveAssignmentModel

ПолеТипОбязательностьИсточникMaskingОписание
AssignmentIdGuidДаRoleAssignment.IdId назначения.
RoleCodestringДаRoleDefinition.CodeКод роли.
RoleNamestringДаRoleDefinition.NameНазвание.
ScopeTypestring (enum ResourceScopeType)ДаResourceScope.TypeScope назначения.
ScopeOrganizationIdGuid?НетResourceScope.OrganizationId
ScopeProjectGroupIdGuid?НетResourceScope.ProjectGroupId
ScopeProjectIdGuid?НетResourceScope.ProjectId
ExpiresAtDateTimeOffset?НетRoleAssignment.ExpiresAtСрок действия.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь; CompanyId — из claim tenant_id (у инвестора отсутствует). Два режима: self (UserId == ActorUserId) — без permission; чужой пользователь — требуется identity.role-assignments.view в scope Organization(CompanyId), иначе — FORBIDDEN. Alias my в маршруте трактуется как UserId = ActorUserId.
  2. Проверка входа и предусловий.
    • 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 блокировки происходит на входе.
  3. Что читаем. Активные назначения пользователя (RoleAssignment со Status = Active, не истёкшие по ExpiresAt); роли этих назначений с их правами; коды прав — из in-memory SEED-каталога. Self-режим сначала пробует кэш perm:{UserId} (TTL 5 мин; инвалидация — событие pin.identity.access.v1 и мутации назначений).
  4. Бизнес-правила и маппинг. По формуле 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 — сами покрывающие назначения.
  5. Результат. Набор кодов прав, состав назначений и момент вычисления (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-permissions identity инвалидирует 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
ValidationError400Ресурс-поля без ResourceOrganizationIdСписок ошибокno retry
IDENTITY_USER_NOT_FOUND404Пользователь не существует / чужой компании«Пользователь не найден»no retry
FORBIDDEN403Чужой пользователь без identity.role-assignments.view«Недостаточно прав»no retry
UNAUTHORIZED401Нет токена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; dashboard identity-overview; runbook — не применяется.

Acceptance Criteria

  • Given пользователь с назначениями роли в scope Organization и Project, When GET /users/my/effective-permissions без ресурс-параметров, Then 200 с объединением кодов всех активных назначений; Assignments перечисляет источники; результат кладётся в кэш perm:{UserId}.
  • Given пользователь имеет роль в scope Project(P1) и роль в scope Organization, When запрос с ResourceProjectId = P2 (не входит в P1), Then в наборе — только права организационного назначения (покрывает любой проект), права Project(P1) не попадают.
  • Given актор с identity.role-assignments.view и целевой пользователь его компании, When GET /users/{id}/effective-permissions, Then 200 с правами сотрудника; факт доступа логируется (read-sensitive).
  • Given актор без identity.role-assignments.view, When запрос чужого userId, Then FORBIDDEN (403).
  • Given целевой пользователь другой компании, When запрос, Then IDENTITY_USER_NOT_FOUND (404) — кросс-tenant существование не раскрывается.
  • Given целевой пользователь Blocked/Archived, When запрос, Then 200 с PermissionCodes = [], Assignments = [] (не ошибка).
  • Given ResourceProjectId задан без ResourceOrganizationId, When запрос, Then ValidationError (400).
  • Given у пользователя есть назначение с ExpiresAt < now, When запрос, Then права этого назначения в набор не входят.
  • Given кэш недоступен, When self-запрос, Then результат вычисляется из PostgreSQL без кэша (200, деградация без ошибки).

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

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

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