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

UC-IDN-008. Проверка доступа сервисом (internal introspection эффективных прав)

Назначение

Internal-сценарий авторизации: gateway или бэкенд-контекст (crm, chessboard, catalog, news…) запрашивает у identity решение allow/deny по permission пользователя на ресурсе, кэширует ответ и инвалидирует кэш по pin.identity.access.v1. Это единственная каноническая реализация формулы effectivePermissions(user, resource) (role.md) и enforcement-механизм модели Scoped-RBAC. Человекочитаемый срез прав для UI отдаёт public-метод get-user-effective-permissions.md. Связан с контекстом identity.

Metadata

ПолеЗначение
IDUC-IDN-008
Контекстidentity (см. docs/06-services/identity/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольSystem (m2m: gateway / бэкенд-сервисы PIN; НЕ конечный пользователь)
Permissions / scopem2m client-credentials (service-token); internal-area недоступна с пользовательскими JWT; сам вызов permissions не требует — он и ЕСТЬ источник permissions
Tenantвызов кросс-tenant: identity вычисляет права относительно ресурса любого tenant (ADR-0052: кросс-tenant видимость определяется платформенными назначениями)
Record-levelНЕ вычисляется identity: record-level (*.view-own/view-team) enforce-ит владеющий контекст по полям владельца записи; identity сообщает только наличие права

Предусловие

  • Вызывающий сервис имеет валидный m2m-токен и сетевой доступ к internal-area Pin.Identity.Api.
  • Пользователь существует (UserId из subject проверяемого запроса).
  • SEED permissions и системные роли загружены.

Триггер

ПолеЗначение
ИсточникAPI call (internal, m2m)
Триггерящий элементAuthorization middleware gateway / проверка прав сервиса-потребителя при cache-miss

Основной Поток

  1. Пользовательский запрос приходит в сервис-потребитель (пример: агент открывает шахматку проекта X — chessboard должен проверить chessboard.units.view).
  2. Сервис-потребитель проверяет локальный кэш прав (key = userId + resource); cache-miss → шаг 3.
  3. Сервис вызывает POST /internal/v1/authorize/check-permission (check-permission.md) с телом { checks: [{ checkId (≤64), userId, permissionCode, resourceOrganizationId?, resourceProjectGroupId?, resourceProjectId? }] } — от 1 до 50 проверок за вызов (batch встроен в контракт); ресурс-поля опциональны (без координат = платформенный ресурс).
  4. Проверить вход и доступ: system-actor из m2m claims (client_idCallerService, логируется); размер батча 1..50 и форма каждого item (pattern permissionCode, согласованность ресурс-полей) — иначе ValidationError (400 на весь батч). Существование пользователей/permissions НЕ валидируется как ошибка: несуществующий пользователь или код дают IsAllowed = false с Reason (deny by default).
  5. Для набора проверок взять эффективные права каждого пользователя: из кэша прав, при промахе — вычислить по активным назначениям (Status = Active и (ExpiresAt == null или ExpiresAt > now)) и ролям этих назначений; коды permission — из каталога SEED (139 позиций). Назначения каждого пользователя грузятся один раз (Distinct по userId).
  6. Вычислить решение по каждому check: пользователь не найден → deny UserNotFound; Status ∉ {Active, Invited} → deny UserInactive; код не в SEED-каталоге → deny PermissionUnknown; покрытие scope Covers(assignment.Scope, resource) (role.md): Platform — всегда; Organization(o)resource.OrganizationId == o; ProjectGroup(g)resource.ProjectGroupId == g; Project(p)resource.ProjectId == p. Иерархия Platform ⊃ Organization ⊃ ProjectGroup ⊃ Project действует вниз, не вверх (назначение Project(X) НЕ покрывает ресурс организации); ресурс без координат покрывается только Platform. Allow ⇔ существует покрывающее назначение, чья роль содержит permissionCode (union-семантика, deny-правил нет — модель аддитивная); первый матч → MatchedAssignmentId.
  7. Вернуть { results: [{ checkId, isAllowed, reason (Allowed / UserNotFound / UserInactive / PermissionUnknown / NoCoveringAssignment), matchedAssignmentId? }] } (200 — и allow, и deny; 4xx/5xx — только ошибки самого вызова); вычисленные наборы прав промахов кэшируются (TTL 5 мин). Состояние identity не меняется (read-only).
  8. Сервис-потребитель кэширует ответ (TTL ≤ 5 мин) и применяет решение: isAllowed = true → выполняет операцию (для *-own/*-team — дополнительно свой record-level фильтр по владельцу записи); isAllowed = false → 403 пользователю.
  9. При изменении прав (UC-IDN-005 grant/revoke, UC-IDN-007 архив) identity публикует UserAccessChangedKafkaEvent в pin.identity.access.v1 (key = UserId).
  10. Consumers (gateway, crm, chessboard, catalog) по событию удаляют кэш-записи пользователя; identity сбрасывает и свой кэш прав. Следующий запрос пользователя вычисляет права заново (eventual-окно ≤ TTL кэша).

Альтернативные Потоки

A1. Пользователь заблокирован/архивирован

  1. Шаг 6: Status ∈ {Blocked, Archived} → deny Reason = UserInactive (200 с isAllowed = false; deny by default, не ошибка — блокировка действует мгновенно даже при живом access-JWT). Потребитель отвечает пользователю 403. Отдельно access-JWT перестаёт обновляться (сессии отозваны INV-S7).

A2. Гость (неаутентифицированный запрос)

  1. Identity не вызывается: потребитель применяет статический набор виртуальной роли guest (SEED role.md; catalog.listings.view-public, news.posts.view, favorites.items.manage-self, selections.collections.view-shared) из собственной конфигурации/кэша каталога.

A3. Batch-проверка (авторизация списков)

  1. Отдельного batch-endpoint-а нет: batch встроен в основной контракт — один вызов POST /internal/v1/authorize/check-permission принимает checks: [] от 1 до 50 элементов (для разных ресурсов и/или пользователей). Назначения каждого пользователя грузятся один раз (шаг 5, Distinct по userId), покрытие считается по каждому check-у.
  2. Полный набор прав пользователя (для UI «что мне доступно» / админ-экранов компании) — public-метод GET /api/v1/users/{userId}/effective-permissions (get-user-effective-permissions.md), не internal-канал.

A4. Identity недоступен

Fail-open запрещён
  1. Потребитель с валидным кэшем работает по кэшу до TTL; без кэша — fail-closed (403/503 пользователю). Fail-open запрещён (mission-critical безопасность).

A5. Неизвестный код permission у потребителя

  1. Потребитель встречает код, которого нет в его enforcement-карте → трактует как «право отсутствует» (deny by default, permission.md «Совместимость»).

Постусловие

  • Потребитель принял авторизационное решение; кэш прав пользователя актуален (≤ TTL / до события).
  • Состояние identity не изменено (read-only).

Возможные Ошибки

КодHTTPУсловиеПоведение клиента (сервиса-потребителя)
UNAUTHORIZED401Невалидный m2m-токен / чужой audienceАлерт конфигурации; fail-closed
ValidationError400Батч вне 1..50 / форма item (pattern кода, ресурс-поля)Исправить вызов
INTERNAL500Инфраструктурный сбой (PostgreSQL)Retry с backoff; fail-closed (deny)

Несуществующий userId или неизвестный permissionCode — НЕ ошибка вызова: 200 с isAllowed = false и Reason = UserNotFound / PermissionUnknown (deny by default, check-permission.md).

Доменные События

СобытиеКогда поднимаетсяSubscribers
Read-only проверка событий не поднимает

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
UserAccessChangedKafkaEvent.v1pin.identity.access.v1Не этим UC (публикуют UC-IDN-005/006/007); этот UC — потребитель инвалидацииgateway, crm, catalog, chessboard — evict кэша (шаг 10)
Согласованность решения авторизации

Ключ партиционирования инвалидации — UserId; потребитель удаляет из кэша только записи затронутого пользователя. Худшее окно рассинхронизации = TTL кэша (≤ 5 мин) плюс лаг доставки события: до его закрытия отозванное право может краткое время оставаться разрешённым, а выданное — ещё не действовать. Блокировка/ архивация пользователя распространяется двумя путями сразу — отзыв сессий (access-JWT перестаёт обновляться после истечения короткого TTL, UC-IDN-007 A5) и инвалидация кэша прав, — поэтому фактический доступ прекращается в пределах меньшего из этих окон; fail-open запрещён (A4), при недоступности identity без валидного кэша решение — deny.

Acceptance Criteria

#КритерийПроверка
AC1Назначение Organization покрывает любой проект компании; Project(X) — только X и не покрывает ресурс организацииматричный тест покрытия scope
AC2Platform-назначение (pin-manager) покрывает ресурсы любых компанийтест кросс-tenant
AC3Blocked/Archived → deny UserInactive (200 с isAllowed = false), не ошибкатест A1
AC4Expired-назначение (ExpiresAt <= now) не участвует в объединениитест срока действия
AC5После UserAccessChangedKafkaEvent следующий запрос отражает новые права (окно ≤ TTL)сквозной тест с UC-IDN-005
AC6Internal-area недоступна с пользовательским JWTнегативный тест 401
AC7Батч 1..50 проверок за одну загрузку назначений на пользователя; > 50 → 400тест A3 + счётчик запросов к хранилищу
AC8Identity недоступен и валидного кэша нет → fail-closed (deny), fail-open запрещён (A4)тест недоступности
AC9Гость: identity не вызывается, применяется статический набор роли guest (A2)тест анонимного доступа

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

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

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