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
| Поле | Значение |
|---|---|
| ID | UC-IDN-008 |
| Контекст | identity (см. docs/06-services/identity/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | System (m2m: gateway / бэкенд-сервисы PIN; НЕ конечный пользователь) |
| Permissions / scope | m2m 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 |
Основной Поток
- Пользовательский запрос приходит в сервис-потребитель (пример: агент открывает шахматку проекта X —
chessboardдолжен проверитьchessboard.units.view). - Сервис-потребитель проверяет локальный кэш прав (
key = userId + resource); cache-miss → шаг 3. - Сервис вызывает
POST /internal/v1/authorize/check-permission(check-permission.md) с телом{ checks: [{ checkId (≤64), userId, permissionCode, resourceOrganizationId?, resourceProjectGroupId?, resourceProjectId? }] }— от 1 до 50 проверок за вызов (batch встроен в контракт); ресурс-поля опциональны (без координат = платформенный ресурс). - Проверить вход и доступ: system-actor из m2m claims (
client_id→CallerService, логируется); размер батча1..50и форма каждого item (patternpermissionCode, согласованность ресурс-полей) — иначеValidationError(400 на весь батч). Существование пользователей/permissions НЕ валидируется как ошибка: несуществующий пользователь или код даютIsAllowed = falseсReason(deny by default). - Для набора проверок взять эффективные права каждого пользователя: из кэша прав, при промахе — вычислить
по активным назначениям (
Status = Activeи (ExpiresAt == nullилиExpiresAt > now)) и ролям этих назначений; коды permission — из каталога SEED (139 позиций). Назначения каждого пользователя грузятся один раз (DistinctпоuserId). - Вычислить решение по каждому
check: пользователь не найден → denyUserNotFound;Status ∉ {Active, Invited}→ denyUserInactive; код не в SEED-каталоге → denyPermissionUnknown; покрытие scopeCovers(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. - Вернуть
{ results: [{ checkId, isAllowed, reason (Allowed / UserNotFound / UserInactive / PermissionUnknown / NoCoveringAssignment), matchedAssignmentId? }] }(200 — и allow, и deny; 4xx/5xx — только ошибки самого вызова); вычисленные наборы прав промахов кэшируются (TTL 5 мин). Состояние identity не меняется (read-only). - Сервис-потребитель кэширует ответ (TTL ≤ 5 мин) и применяет решение:
isAllowed = true→ выполняет операцию (для*-own/*-team— дополнительно свой record-level фильтр по владельцу записи);isAllowed = false→ 403 пользователю. - При изменении прав (UC-IDN-005 grant/revoke, UC-IDN-007 архив) identity публикует
UserAccessChangedKafkaEventвpin.identity.access.v1(key =UserId). - Consumers (gateway, crm, chessboard, catalog) по событию удаляют кэш-записи пользователя; identity сбрасывает и свой кэш прав. Следующий запрос пользователя вычисляет права заново (eventual-окно ≤ TTL кэша).
Альтернативные Потоки
A1. Пользователь заблокирован/архивирован
- Шаг 6:
Status ∈ {Blocked, Archived}→ denyReason = UserInactive(200 сisAllowed = false; deny by default, не ошибка — блокировка действует мгновенно даже при живом access-JWT). Потребитель отвечает пользователю 403. Отдельно access-JWT перестаёт обновляться (сессии отозваны INV-S7).
A2. Гость (неаутентифицированный запрос)
- Identity не вызывается: потребитель применяет статический набор виртуальной роли
guest(SEED role.md;catalog.listings.view-public,news.posts.view,favorites.items.manage-self,selections.collections.view-shared) из собственной конфигурации/кэша каталога.
A3. Batch-проверка (авторизация списков)
- Отдельного batch-endpoint-а нет: batch встроен в основной контракт — один вызов
POST /internal/v1/authorize/check-permissionпринимаетchecks: []от 1 до 50 элементов (для разных ресурсов и/или пользователей). Назначения каждого пользователя грузятся один раз (шаг 5,DistinctпоuserId), покрытие считается по каждому check-у. - Полный набор прав пользователя (для UI «что мне доступно» / админ-экранов компании) — public-метод
GET /api/v1/users/{userId}/effective-permissions(get-user-effective-permissions.md), не internal-канал.
A4. Identity недоступен
- Потребитель с валидным кэшем работает по кэшу до TTL; без кэша — fail-closed (403/503 пользователю). Fail-open запрещён (mission-critical безопасность).
A5. Неизвестный код permission у потребителя
- Потребитель встречает код, которого нет в его enforcement-карте → трактует как «право отсутствует» (deny by default, permission.md «Совместимость»).
Постусловие
- Потребитель принял авторизационное решение; кэш прав пользователя актуален (≤ TTL / до события).
- Состояние identity не изменено (read-only).
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента (сервиса-потребителя) |
|---|---|---|---|
UNAUTHORIZED | 401 | Невалидный m2m-токен / чужой audience | Алерт конфигурации; fail-closed |
ValidationError | 400 | Батч вне 1..50 / форма item (pattern кода, ресурс-поля) | Исправить вызов |
INTERNAL | 500 | Инфраструктурный сбой (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.v1 | pin.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 |
| AC2 | Platform-назначение (pin-manager) покрывает ресурсы любых компаний | тест кросс-tenant |
| AC3 | Blocked/Archived → deny UserInactive (200 с isAllowed = false), не ошибка | тест A1 |
| AC4 | Expired-назначение (ExpiresAt <= now) не участвует в объединении | тест срока действия |
| AC5 | После UserAccessChangedKafkaEvent следующий запрос отражает новые права (окно ≤ TTL) | сквозной тест с UC-IDN-005 |
| AC6 | Internal-area недоступна с пользовательским JWT | негативный тест 401 |
| AC7 | Батч 1..50 проверок за одну загрузку назначений на пользователя; > 50 → 400 | тест A3 + счётчик запросов к хранилищу |
| AC8 | Identity недоступен и валидного кэша нет → fail-closed (deny), fail-open запрещён (A4) | тест недоступности |
| AC9 | Гость: identity не вызывается, применяется статический набор роли guest (A2) | тест анонимного доступа |
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): Identity Internal API — реестр методов и общие правила области, POST /internal/v1/authorize/check-permission — Проверка доступа (internal)
- index.md (1): Identity Service (Pin.Identity)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы: ../api/internal/check-permission.md
(internal-area,
POST /internal/v1/authorize/check-permission) и ../api/company-user/get-user-effective-permissions.md (company-user,GET /api/v1/users/{userId}/effective-permissions). - Domain entity: ../domain/role.md (правило
effectivePermissions, иерархия scope), ../domain/permission.md (каталог, deny by default), ../domain/user.md (статусы). - Событие инвалидации:
UserAccessChangedKafkaEvent— ../domain/role.md «События». - Record-level enforcement:
docs/06-services/crm/(владелец/группа на записях). - Решения: ADR-0052 (tenant vs scope), ADR-0053 (админ-панель платформы — отдельная аутентификация m2m).