У каждого эндпоинта ресурса может быть политика, которую edge вычисляет при каждом запросе: булево правило allow, правила inject, меняющие пересылаемый запрос, и требования step-up. Правила пишутся на CEL. На странице перечислены поля, данные, доступные правилу, порядок вычисления и поведение при сбоях. Сначала нужно зарегистрировать ресурс (Как зарегистрировать ресурс).
Поля политики
| Поле | Тип | Значение |
|---|---|---|
allow | CEL, boolean | Должно вернуть true, чтобы вызов пошел дальше. Пусто или отсутствует: проверки нет |
inject | список правил | Каждое правило: target (header, query или body), name, expression (CEL). Результат заменяет значение клиента с этим именем |
fetchUserInfo | boolean | Загрузить claims пользователя из /userinfo в auth до вычисления правил. Это один лишний вызов на запрос |
stepUpCondition | CEL, boolean | Если true, в токене должен быть acr из stepUpAcr |
stepUpAcr | строка | Значения acr через пробел. acr токена должен совпасть с одним из них |
maxAge | целое, секунды | auth_time токена должен быть не старше этого значения. Проверяется отдельно, с условием или без |
Разрешения в политику не входят. Эндпоинт должен быть разрешен еще и через разрешение роли или клиента, и эта проверка идет первой (Разрешение).
Что может читать правило
Любое выражение (allow, inject, stepUpCondition) видит одни и те же три переменные:
| Переменная | Тип | Содержимое |
|---|---|---|
token | map | Claims проверенного access token. Целые числа — CEL int, остальные — double. Есть всегда: sub, client_id, aud (список), tenant_id, roles (список), iat, exp, jti. Есть, если их выдал auth: acr, auth_time, sid, cnf |
user | map | Claims из /userinfo. Пусто, пока fetchUserInfo выключен |
request.path.params | map строк | Значения, связанные сегментами {name} пути эндпоинта |
request.query | map строк | Первое значение каждого query-параметра |
request.queryAll | map списков строк | Все значения каждого query-параметра |
request.headers | map строк | Заголовки запроса, первое значение |
request.headersAll | map списков строк | Заголовки запроса, все значения |
request.body | map | Разобранное 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"]
- Токен. Найден, подпись верна, срок не истек. Просроченный токен из cookie обновляется, просроченный токен из заголовка — нет.
- Отзыв. Поиск в памяти по ID токена, семейству refresh-токенов, сессии и субъекту.
- Привязка. Proof DPoP и правила
cnf(Токены с привязкой к отправителю). - Поиск. Сначала ресурс, затем эндпоинт по методу и пути. Литеральный сегмент выигрывает у сегмента
{param}, поэтому/users/meприоритетнее/users/{userId}. - Разрешение. Эндпоинт разрешен какой-либо ролью вызывающего или, для сервисного токена (
subравенclient_id), разрешениями клиента. - Audience. В
audестьresource://{resourceId}(илиresource://edge) для внутреннего ресурса либо абсолютный URI ресурса для публичного. - Userinfo, только если включен
fetchUserInfo. Для токена, привязанного к ключу или к сертификату вызывающего, этому вызову нужны собственный сертификатedge, блокnativeиmutual-tls.edge-trusted-certificatesвauth(Mutual-TLS listener). allow.- Step-up. Намеренно после
allow: вызывающий без права на эндпоинт получает обычный 403, а не предложение повторно войти, которое закончилось бы тем же отказом. - 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 | включено |
allow | has(user.department) && user.department == "finance" |
Подстановка идентичности пользователя
Upstream читает идентичность из заголовков и не разбирает токен.
| Target | Name | Expression |
|---|---|---|
header | X-User-Id | token.sub |
header | X-Tenant-Id | token.tenant_id |
То, что клиент прислал в X-User-Id, сначала удаляется, поэтому заголовок подделать нельзя. Так же работают правило query (имя tenant, выражение token.tenant_id) и правило body (имя userId, выражение token.sub).
Владение через параметр пути
Пользователь читает только свои заказы.
| Поле | Значение |
|---|---|
| Метод, путь | GET /users/{userId}/orders |
allow | request.path.params.userId == token.sub |
Step-up для чувствительного эндпоинта
Переводы выше лимита требуют более сильной аутентификации, а для любого перевода вход должен быть не старше десяти минут.
| Поле | Значение |
|---|---|
| Метод, путь | POST /payments/transfer |
allow | has(request.body.amount) && request.body.amount > 0 |
stepUpCondition | request.body.amount > 100000 |
stepUpAcr | otp |
maxAge | 600 |
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 в логе |
|---|---|---|
| Нет учетных данных | 401 | token_missing |
| Неверная подпись, просроченный токен из заголовка | 401 | token_invalid, token_expired |
| Просроченный токен из cookie, который нельзя обновить | 401 с Location: /login/{presetId} и сброшенной cookie | нет |
| Отозванный токен | 401 | нет |
| Привязка не прошла | 401, WWW-Authenticate: DPoP error="invalid_dpop_proof" либо use_dpop_nonce с DPoP-Nonce, либо голый 401 | dpop_*, certificate_bound_token_outside_session |
| Неизвестный ресурс или нет эндпоинта для метода и пути | 404 | нет |
| Нет разрешения на эндпоинт | 403 | permission_denied |
| Audience не покрывает ресурс | 403 | audience_denied |
auth отклоняет токен на /userinfo | 401 | user_info_unauthorized |
Иной сбой /userinfo | 500 | нет |
allow вернул false | 403 | access_rule_denied |
allow читает отсутствующие данные | 403 | access_rule_denied, с описанием |
allow сломан: не компилируется, ошибка выполнения, не boolean | 500 | access_rule_failed |
| Step-up не выполнен | 401, WWW-Authenticate: Bearer error="insufficient_user_authentication" | step_up_required |
stepUpCondition читает отсутствующие данные | считается true | нет |
stepUpCondition сломан | 500 | step_up_condition_failed |
inject читает отсутствующие данные | 403 | inject_rule_denied |
inject сломан | 500 | inject_rule_failed |
Отказы возвращают только статус без пояснений. Код попадает в ключ error строки лога запроса и в метрики. Правило inject, вычисленное в null, ничего не добавляет, а значение клиента с этим именем все равно удаляется.
Два ограничения, которые стоит учесть. Значения inject — строки: число или boolean преобразуется через toString. Для подстановки в тело нужно JSON-тело в виде объекта, а любое другое тело пересылается без изменений и без подстановки.
Задать политику в консоли
- Откройте Resources, откройте ресурс и нажмите Edit endpoints (либо сначала создайте ресурс и добавьте эндпоинты на его странице).
- Нажмите Add. Введите метод и относительный путь, например
/users/{userId}/orders. - Включите Fetch userinfo, если какое-то правило читает
user. - В разделе Who may call введите Allow (CEL). Пустое поле означает отсутствие проверки.
- В разделе Step-up заполните Condition (CEL), отметьте Required ACR и задайте Max auth age (seconds).
- В разделе Inject нажмите Add rule и задайте Target, Name и Expression (CEL).
- Нажмите Save endpoints.
edgeподхватит изменение не позднее чем черезconfiguration-cache-refresh-interval.


Сохранение ресурса никому не дает доступ. Привяжите эндпоинт к разрешению, а разрешение к роли, как в Как зарегистрировать разрешение.
Что дальше
- Как защитить API с помощью edge: полный пример с curl.
- Обзор edge: жизненный цикл запроса вокруг конвейера.
- RFC 9470: вызов step-up.