Политики эндпоинтов — Versola Docs
/docs
До 1.00.7.5versola.ioGitHub

Политики эндпоинтов

Правила на CEL, которые edge вычисляет для каждого эндпоинта (allow, inject, userinfo, step-up, max age): доступные переменные, примеры и код ответа для каждой ошибки.

У каждого эндпоинта ресурса может быть политика, которую edge вычисляет при каждом запросе: булево правило allow, правила inject, меняющие пересылаемый запрос, и требования step-up. Правила пишутся на CEL. На странице перечислены поля, данные, доступные правилу, порядок вычисления и поведение при сбоях. Сначала нужно зарегистрировать ресурс (Как зарегистрировать ресурс).

Поля политики

ПолеТипЗначение
allowCEL, booleanДолжно вернуть true, чтобы вызов пошел дальше. Пусто или отсутствует: проверки нет
injectсписок правилКаждое правило: target (header, query или body), name, expression (CEL). Результат заменяет значение клиента с этим именем
fetchUserInfobooleanЗагрузить claims пользователя из /userinfo в auth до вычисления правил. Это один лишний вызов на запрос
stepUpConditionCEL, booleanЕсли true, в токене должен быть acr из stepUpAcr
stepUpAcrстрокаЗначения acr через пробел. acr токена должен совпасть с одним из них
maxAgeцелое, секундыauth_time токена должен быть не старше этого значения. Проверяется отдельно, с условием или без

Разрешения в политику не входят. Эндпоинт должен быть разрешен еще и через разрешение роли или клиента, и эта проверка идет первой (Разрешение).

Что может читать правило

Любое выражение (allow, inject, stepUpCondition) видит одни и те же три переменные:

ПеременнаяТипСодержимое
tokenmapClaims проверенного access token. Целые числа — CEL int, остальные — double. Есть всегда: sub, client_id, aud (список), tenant_id, roles (список), iat, exp, jti. Есть, если их выдал auth: acr, auth_time, sid, cnf
usermapClaims из /userinfo. Пусто, пока fetchUserInfo выключен
request.path.paramsmap строкЗначения, связанные сегментами {name} пути эндпоинта
request.querymap строкПервое значение каждого query-параметра
request.queryAllmap списков строкВсе значения каждого query-параметра
request.headersmap строкЗаголовки запроса, первое значение
request.headersAllmap списков строкЗаголовки запроса, все значения
request.bodymapРазобранное JSON-тело. Есть только если Content-Type равен application/json, а тело не пустое

Язык — стандартный CEL со стандартными макросами: сравнения, логические операторы, in, арифметика, ?:, has(x.y), all, exists, exists_one, map, filter, строковые и списочные функции. Выражения компилируются один раз и кэшируются. Консоль проверяет синтаксис и тип результата при сохранении, поэтому синтаксическая ошибка или небулев allow не дадут сохранить ресурс.

Правило, которое читает то, чего нет в запросе, не вызывает ошибку CEL: это отсутствующие данные. Отсутствующие данные — это request.body.total в запросе без JSON-тела или user.plan у пользователя без такого claim. Необязательные значения защищайте через has():

!has(request.query.draft) || "manager" in token.roles

Отсутствующие данные в allow дают 403. Везде, где значение необязательное, используйте has().

Порядок вычисления

flowchart TD
    A["Request to /resources/{resourceId}/{path}"] --> B{"Token found?\nBearer, DPoP or EDGE_SESSION"}
    B -- no --> R401A["401"]
    B -- yes --> C{"Signature valid\nand not expired?"}
    C -- "expired, cookie" --> C2["Refresh with stored refresh token\nrotate cookie"]
    C -- "expired, header" --> R401B["401"]
    C -- "invalid" --> R401C["401"]
    C2 -- "refresh fails" --> R401D["401 + Location /login/{presetId}\ncookie cleared"]
    C2 --> D
    C -- yes --> D{"Revoked?\nin-memory list"}
    D -- yes --> R401E["401"]
    D -- no --> E{"Token binding OK?\nDPoP proof, cnf rules"}
    E -- no --> R401F["401 + WWW-Authenticate"]
    E -- yes --> F{"Resource and endpoint\nregistered?"}
    F -- no --> R404["404"]
    F -- yes --> G{"Role or client permission\ngrants this endpoint?"}
    G -- no --> R403A["403"]
    G -- yes --> H{"Audience covers\nthe resource?"}
    H -- no --> R403B["403"]
    H -- yes --> I["Fetch /userinfo\nonly if fetchUserInfo"]
    I --> J{"allow rule true?"}
    J -- "false or data missing" --> R403C["403"]
    J -- "rule broken" --> R500A["500"]
    J -- yes --> K{"Step-up needed?\nstepUpCondition, stepUpAcr, maxAge"}
    K -- yes --> R401G["401 + WWW-Authenticate\ninsufficient_user_authentication"]
    K -- "condition broken" --> R500B["500"]
    K -- no --> L["Evaluate inject rules\nheader, query, body"]
    L -- "data missing" --> R403D["403"]
    L -- "rule broken" --> R500C["500"]
    L --> M["Forward to the resource\nBasic credential or caller's Bearer token"]
  1. Токен. Найден, подпись верна, срок не истек. Просроченный токен из cookie обновляется, просроченный токен из заголовка — нет.
  2. Отзыв. Поиск в памяти по ID токена, семейству refresh-токенов, сессии и субъекту.
  3. Привязка. Proof DPoP и правила cnf (Токены с привязкой к отправителю).
  4. Поиск. Сначала ресурс, затем эндпоинт по методу и пути. Литеральный сегмент выигрывает у сегмента {param}, поэтому /users/me приоритетнее /users/{userId}.
  5. Разрешение. Эндпоинт разрешен какой-либо ролью вызывающего или, для сервисного токена (sub равен client_id), разрешениями клиента.
  6. Audience. В aud есть resource://{resourceId} (или resource://edge) для внутреннего ресурса либо абсолютный URI ресурса для публичного.
  7. Userinfo, только если включен fetchUserInfo. Для токена, привязанного к ключу или к сертификату вызывающего, этому вызову нужны собственный сертификат edge, блок native и mutual-tls.edge-trusted-certificates в auth (Mutual-TLS listener).
  8. allow.
  9. Step-up. Намеренно после allow: вызывающий без права на эндпоинт получает обычный 403, а не предложение повторно войти, которое закончилось бы тем же отказом.
  10. Inject, затем пересылка.

Правила работают с данными одного запроса. Между запросами ничего не сохраняется, а allow и stepUpCondition не видят результат друг друга.

Примеры

Allow по роли или claim

GET /invoices с необязательным фильтром draft. Черновики видят менеджеры, остальные с этим разрешением видят только закрытые счета.

ПолеЗначение
Метод, путьGET /invoices
allow!has(request.query.draft) || "manager" in token.roles

Claim, который есть только в userinfo, требует включить fetchUserInfo:

ПолеЗначение
Метод, путьGET /reports
Fetch userinfoвключено
allowhas(user.department) && user.department == "finance"

Подстановка идентичности пользователя

Upstream читает идентичность из заголовков и не разбирает токен.

TargetNameExpression
headerX-User-Idtoken.sub
headerX-Tenant-Idtoken.tenant_id

То, что клиент прислал в X-User-Id, сначала удаляется, поэтому заголовок подделать нельзя. Так же работают правило query (имя tenant, выражение token.tenant_id) и правило body (имя userId, выражение token.sub).

Владение через параметр пути

Пользователь читает только свои заказы.

ПолеЗначение
Метод, путьGET /users/{userId}/orders
allowrequest.path.params.userId == token.sub

Step-up для чувствительного эндпоинта

Переводы выше лимита требуют более сильной аутентификации, а для любого перевода вход должен быть не старше десяти минут.

ПолеЗначение
Метод, путьPOST /payments/transfer
allowhas(request.body.amount) && request.body.amount > 0
stepUpConditionrequest.body.amount > 100000
stepUpAcrotp
maxAge600

stepUpAcr берет значения из словаря ACR вашего тенанта (Step-up (ACR vocabulary) на странице Security в консоли) или встроенное sensitive, которое требует самый сильный фактор из имеющихся у пользователя (см. RFC 9470); консоль предлагает sensitive, только если оно уже задано через API. Запрос, не прошедший хотя бы одну проверку, получает:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="insufficient_user_authentication", acr_values="otp", max_age="600"

В ответе перечислены все невыполненные требования, поэтому одного повторного входа хватает на оба. Приложение отправляет браузер на /login/{presetId}?acr_values=otp&max_age=600 и повторяет запрос после входа (RFC 9470). Если запрос пришел с cookie сессии, в 401 есть еще заголовок Location с этим URL, собранным из пресета самой сессии, так что страница, отданная через edge, может перейти по нему, не зная пресета.

Чтобы требовать acr при каждом вызове, задайте stepUpCondition равным true. Если задан stepUpAcr, а условия нет, ничего не требуется. Если условие истинно, а stepUpAcr не задан, действует только maxAge.

Поведение при сбоях

СитуацияОтветКод error в логе
Нет учетных данных401token_missing
Неверная подпись, просроченный токен из заголовка401token_invalid, token_expired
Просроченный токен из cookie, который нельзя обновить401 с Location: /login/{presetId} и сброшенной cookieнет
Отозванный токен401нет
Привязка не прошла401, WWW-Authenticate: DPoP error="invalid_dpop_proof" либо use_dpop_nonce с DPoP-Nonce, либо голый 401dpop_*, certificate_bound_token_outside_session
Неизвестный ресурс или нет эндпоинта для метода и пути404нет
Нет разрешения на эндпоинт403permission_denied
Audience не покрывает ресурс403audience_denied
auth отклоняет токен на /userinfo401user_info_unauthorized
Иной сбой /userinfo500нет
allow вернул false403access_rule_denied
allow читает отсутствующие данные403access_rule_denied, с описанием
allow сломан: не компилируется, ошибка выполнения, не boolean500access_rule_failed
Step-up не выполнен401, WWW-Authenticate: Bearer error="insufficient_user_authentication"step_up_required
stepUpCondition читает отсутствующие данныесчитается trueнет
stepUpCondition сломан500step_up_condition_failed
inject читает отсутствующие данные403inject_rule_denied
inject сломан500inject_rule_failed

Отказы возвращают только статус без пояснений. Код попадает в ключ error строки лога запроса и в метрики. Правило inject, вычисленное в null, ничего не добавляет, а значение клиента с этим именем все равно удаляется.

Два ограничения, которые стоит учесть. Значения inject — строки: число или boolean преобразуется через toString. Для подстановки в тело нужно JSON-тело в виде объекта, а любое другое тело пересылается без изменений и без подстановки.

Задать политику в консоли

  1. Откройте Resources, откройте ресурс и нажмите Edit endpoints (либо сначала создайте ресурс и добавьте эндпоинты на его странице).
  2. Нажмите Add. Введите метод и относительный путь, например /users/{userId}/orders.
  3. Включите Fetch userinfo, если какое-то правило читает user.
  4. В разделе Who may call введите Allow (CEL). Пустое поле означает отсутствие проверки.
  5. В разделе Step-up заполните Condition (CEL), отметьте Required ACR и задайте Max auth age (seconds).
  6. В разделе Inject нажмите Add rule и задайте Target, Name и Expression (CEL).
  7. Нажмите Save endpoints. edge подхватит изменение не позднее чем через configuration-cache-refresh-interval.
Панели одного эндпоинта: Allow (CEL), Condition (CEL), Required ACR, Max auth age и правило Inject
Endpoint ресурса с Allow, step-up и правилом Inject.
Сохраненная политика, только для чтения: Who may call показывает разрешения, покрывающие эндпоинт, и правило Allow, а Step-up и Added to the request — остальное
Карточка Endpoints ресурса с правилами каждого эндпоинта.

Сохранение ресурса никому не дает доступ. Привяжите эндпоинт к разрешению, а разрешение к роли, как в Как зарегистрировать разрешение.

Что дальше