Политики эндпоинтов — Versola Docs
VersolaVersola/docs
До 1.00.7.1versola.kzGitHub

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

Правила на 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.
  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 вашего тенанта (Challenges & Security в консоли). Запрос, не прошедший хотя бы одну проверку, получает:

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).

Чтобы требовать 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 и нажмите + Create Resource, либо откройте существующий ресурс для редактирования.
  2. Нажмите Add endpoint. Введите метод и относительный путь, например /users/{userId}/orders.
  3. Включите Fetch userinfo, если какое-то правило читает user.
  4. Введите Allow (CEL). Пустое поле означает отсутствие проверки.
  5. В разделе Inject нажмите Add rule и задайте Target, Name и Expression (CEL).
  6. Заполните Step-up Condition (CEL), Step-up ACR и Max Auth Age (seconds).
  7. Сохраните ресурс. edge подхватит изменение не позднее чем через configuration-cache-refresh-interval.
Карточка эндпоинта: Allow (CEL), Step-up Condition (CEL), Step-up ACR, Max Auth Age и правило Inject
Endpoint ресурса с Allow, step-up и правилом Inject.
Сохранённая политика, только для чтения: строка Required Permission показывает, какие разрешения покрывают эндпоинт
Endpoint ресурса раскрыт: поля политики на CEL.

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

Что дальше