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


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