Перейти к основному содержимому
Версия: 1.12

Разработка ABAC политик авторизации

Авторизация запросов в API выполняется при обработке запроса на Spirit IAP (Oathkeeper), в который встроен авторизатор Open Policy Agent (OPA).

Язык для разработки политик авторизации — Rego. В статье рассмотрены примеры правил для MtRBAC-модели Spirit IAM.

Важно

Политики хранятся децентрализованно, подключаются к конкретному инстансу защищаемого бэкенда.

Политики авторизации следует писать только для API конкретного защищаемого бэкенда.

Совет

Актуальная версия Spirit IAP предоставляет работу с Rego v0 и Rego v1. Чтобы включить Rego v1, в конфигурационном файле для opa-builtin присвойте параметру rego_v1_enabled значение true.

Пример конфига opa-builtin
opa-builtin.yaml
rego_v1_enabled: true
query: data.http.authz
paths_to_policies:
- /etc/opa-config/policies
- /etc/opa-config/data
integrated_systems_fetcher:
enabled: false
Пример фрагмента конфига IAP с указанием конфига opa-builtin
oathkeeper.yaml
opa_builtin:
config: /etc/opa-builtin/opa-builtin.yaml
Внимание

При загрузке политики, пути до которых передаются через параметр конфига paths_to_policies, должны относиться к одной версии Rego.

При разработке политики можно использовать следующие данные:

  • Контекст запроса (URI, метод, тело запроса, query-параметры, заголовки запроса).
  • Контекст пользователя (данные аутентификации).
  • (Опционально) Данные из внешних источников, которые могут представлять собой:
    • статические данные, подключаемые вместе с набором политик.

Контекст запроса и контекст пользователя

Spirit IAP выполняет аутентификацию пользователя, после чего вызывает встроенный движок OPA, передавая ему:

  • контекст запроса — URI, метод, query-параметры, заголовки запроса, тело запроса;
  • контекст пользователя — данные аутентификации.

В момент работы политики этот контекст доступен в объекте input, примеры которых приведены ниже:

  • input.extra.identity — атрибуты пользователя;
  • input.extra.identity.kind — тип учетной записи инициатора запроса (User или ServiceAccount);
  • input.extra.permissions — полномочия пользователя в Spirit IAM, при этом:
    • input.extra.permissions.global_roles[_] — описывает полномочия пользователя, определенные глобальными ролями;
    • input.extra.permissions.projects[_] — описывает полномочия пользователя, определенные проектными ролями, при этом каждый объект включает:
      • атрибут input.extra.permissions.projects[_].key = ключ тенанта;
      • массив объектов input.extra.permissions.projects[_].roles[_], включающий проектные роли и их полномочия в этом тенанте.
  • input.upstream_request, описывающий входящий запрос в составе атрибутов:
    • path — URI запроса;
    • method — метод запроса;
    • body — тело запроса (если есть);
    • query — query-параметры запроса (если есть).
Примечание

Состав включенных в OPA input данных зависит от способа аутентификации и от логики конкретного аутентификатора Spirit IAP.

Совет

При написании политик авторизации рекомендуется опираться на полномочия, зарегистрированные конкретно для вашего сервиса.

  • Не завязываться на полномочия других сервисов — их могут изменить, исключить из роли или включить в другую роль.
  • Не завязываться на название роли — одно и то же полномочие может участвовать в разных ролях, роль могут переименовать.

Примеры OPA Input

Для запросов от имени пользователей и сервис-аккаунтов Spirit

Примечание

Структура данных синхронизирована для разных типов учетных записей (пользователи / СА) и способов аутентификации (сессия / токен / делегированный токен), и позволяет сократить количество политик.

В общем случае если в процессе авторизации вам не нужно проверять тип учетной записи и способ аутентификации, то для обработки всех пользовательских запросов достаточно одной политики.

Отличия в структуре обусловлены спецификой разных типов учетных записей и способов аутентификации.

Пример OPA Input для запроса с сессионными куками пользователя
{
"subject": "83b8fd55-004c-4d88-841b-27dc2767714b",
"subject_name": "ad.admin",
"extra":
{
"active": true,
"authenticated_at": "2026-02-20T09:33:26.463856Z",
"authentication_methods":
[
{
"aal": "aal1",
"completed_at": "2026-02-20T09:35:45.663922479Z",
"method": "oidc",
"provider": "oidc-mock"
}
],
"authenticator_assurance_level": "aal1",
"devices":
[
{
"id": "0a988120-c5d6-4c55-aeaa-5d024a1e98cc",
"ip_address": "",
"location": "",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
}
],
"expires_at": "2026-03-22T09:33:26.463856Z",
"id": "7c6cee48-3eef-4768-8a5a-857b95cd2091",
"identity":
{
"created_at": "2026-02-20T07:46:17.445602Z",
"id": "83b8fd55-004c-4d88-841b-27dc2767714b",
"kind": "User",
"organization_id": null,
"schema_id": "default",
"schema_url": "http://iam.example.com/api/v1/schemas/…",
"state": "active",
"state_changed_at": "2026-02-20T07:46:17.441312Z",
"traits":
{
"email": "admin@example.com",
"family_name": "Adminov",
"given_name": "Admin",
"name": "Admin Adminov",
"preferred_username": "ad.admin",
"restricted": false
},
"updated_at": "2026-02-20T09:32:18.020843Z",
"verifiable_addresses":
[
{
"created_at": "2026-02-20T07:46:17.450471Z",
"id": "f49df716-a3fd-4985-875c-f56895673ee6",
"status": "pending",
"updated_at": "2026-02-20T07:46:17.450471Z",
"value": "admin@example.com",
"verified": false,
"via": "email"
}
]
},
"issued_at": "2026-02-20T09:33:26.463856Z",
"permissions":
{
"global_roles":
[],
"groups":
[],
"projects":
[
{
"key": "test",
"roles":
[
{
"permissions":
[
"iam.project_lead"
],
"role": "Project lead"
}
]
}
]
},
"sid": "7c6cee48-3eef-4768-8a5a-857b95cd2091",
"spirit_iam":
{
"permissions":
{
"global_permissions":
[],
"tenant_permissions":
{
"test":
[
"iam.project_lead"
]
}
}
}
},
"header": null,
"match_context":
{
"regexp_capture_groups":
[],
"url":
{
"Scheme": "http",
"Opaque": "",
"User": null,
"Host": "tenant-manager-oathkeeper",
"Path": "/api/v2/roles",
"Fragment": "",
"RawQuery": "",
"RawPath": "",
"RawFragment": "",
"ForceQuery": false,
"OmitHost": false
}
},
"upstream_request":
{
"detected_content_type": "",
"host": "tenant-manager-oathkeeper",
"is_body_truncated": false,
"method": "GET",
"path": "/api/v2/roles",
"query":
{}
}
}
С токеном пользователя (Bearer)
{
"subject": "83b8fd55-004c-4d88-841b-27dc2767714b",
"subject_name": "ad.admin",
"extra":
{
"client_id": "spirit-cli",
"identity":
{
"id": "83b8fd55-004c-4d88-841b-27dc2767714b",
"kind": "User",
"traits":
{
"email": "admin@example.com",
"family_name": "Adminov",
"given_name": "Admin",
"name": "Admin Adminov",
"preferred_username": "ad.admin",
"restricted": false
}
},
"permissions":
{
"global_roles":
[],
"groups":
[],
"projects":
[
{
"key": "test",
"roles":
[
{
"permissions":
[
"iam.project_lead"
],
"role": "Project lead"
}
]
}
]
},
"scope": "permissions openid offline",
"spirit_iam":
{
"permissions":
{
"global_permissions":
[],
"tenant_permissions":
{
"test":
[
"iam.project_lead"
]
}
}
},
"sub": "83b8fd55-004c-4d88-841b-27dc2767714b"
},
"header": null,
"match_context":
{
"regexp_capture_groups":
[],
"url":
{
"Scheme": "http",
"Opaque": "",
"User": null,
"Host": "tenant-manager-oathkeeper",
"Path": "/api/v2/tenants/test",
"Fragment": "",
"RawQuery": "",
"RawPath": "",
"RawFragment": "",
"ForceQuery": false,
"OmitHost": false
}
},
"upstream_request":
{
"detected_content_type": "",
"host": "tenant-manager-oathkeeper",
"is_body_truncated": false,
"method": "GET",
"path": "/api/v2/tenants/test",
"query":
{}
}
}
С токеном сервис-аккаунта Spirit (Bearer)
{
"subject": "9b060735-baf9-4398-9149-a42012def7c7",
"subject_name": "sa-test-subject",
"extra":
{
"client_id": "b4eb36fc-9010-47c4-bd32-982634edee17",
"identity":
{
"id": "9b060735-baf9-4398-9149-a42012def7c7",
"kind": "ServiceAccount",
"traits":
{
"email": null,
"family_name": null,
"given_name": null,
"name": "sa-test-subject",
"preferred_username": "sa-test-subject",
"tenant": "test"
}
},
"permissions":
{
"global_roles":
[],
"groups":
[],
"projects":
[
{
"key": "test",
"roles":
[
{
"permissions":
[
"iam.project_lead"
],
"role": "Project lead"
},
{
"permissions":
[
"gitlab.developer"
],
"role": "GitLab developer"
}
]
}
]
},
"scope": "permissions openid offline",
"spirit_iam":
{
"permissions":
{
"global_permissions":
[],
"tenant_permissions":
{
"test":
[
"iam.project_lead",
"gitlab.developer"
]
}
}
},
"sub": "9b060735-baf9-4398-9149-a42012def7c7"
},
"header": null,
"match_context":
{
"regexp_capture_groups":
[],
"url":
{
"Scheme": "http",
"Opaque": "",
"User": null,
"Host": "tenant-manager-oathkeeper",
"Path": "/api/v2/tenants/test",
"Fragment": "",
"RawQuery": "",
"RawPath": "",
"RawFragment": "",
"ForceQuery": false,
"OmitHost": false
}
},
"upstream_request":
{
"detected_content_type": "",
"host": "tenant-manager-oathkeeper",
"is_body_truncated": false,
"method": "GET",
"path": "/api/v2/tenants/test",
"query":
{}
}
}
С делегированным токеном от имени пользователя Spirit
{
"subject": "83b8fd55-004c-4d88-841b-27dc2767714b",
"subject_name": "ad.admin",
"extra":
{
"act":
{
"sub": "3f0dbc19-2f70-40cf-897f-bb4ff6a7dbfb"
},
"aud":
[
"test-system",
"http://iam.example.com/api/v1/exchange/token"
],
"exp": 1771620081,
"iat": 1771584081,
"identity":
{
"id": "83b8fd55-004c-4d88-841b-27dc2767714b",
"kind": "User",
"traits":
{
"email": "admin@example.com",
"family_name": "Adminov",
"given_name": "Admin",
"name": "Admin Adminov",
"preferred_username": "ad.admin",
"restricted": false
}
},
"iss": "http://iam.example.com/api/v1",
"jti": "e0f66824-5e11-416f-af81-dbfaf69ac04b",
"permissions":
{
"global_roles":
[],
"groups":
[],
"projects":
[
{
"key": "test",
"roles":
[
{
"permissions":
[
"iam.project_lead"
],
"role": "Project lead"
},
{
"permissions":
[
"gitlab.developer"
],
"role": "GitLab developer"
}
]
}
]
},
"spirit_iam":
{
"permissions":
{
"global_permissions":
[],
"tenant_permissions":
{
"test":
[
"iam.project_lead",
"gitlab.developer"
]
}
}
},
"sub": "83b8fd55-004c-4d88-841b-27dc2767714b"
},
"header": null,
"match_context":
{
"regexp_capture_groups":
[],
"url":
{
"Scheme": "http",
"Opaque": "",
"User": null,
"Host": "tenant-manager-oathkeeper",
"Path": "/api/v2/tenants/test",
"Fragment": "",
"RawQuery": "",
"RawPath": "",
"RawFragment": "",
"ForceQuery": false,
"OmitHost": false
}
},
"upstream_request":
{
"detected_content_type": "",
"host": "tenant-manager-oathkeeper",
"is_body_truncated": false,
"method": "GET",
"path": "/api/v2/tenants/test",
"query":
{}
}
}
С делегированным токеном от имени сервис-аккаунта Spirit
{
"subject": "9b060735-baf9-4398-9149-a42012def7c7",
"subject_name": "sa-test-subject",
"extra":
{
"act":
{
"sub": "3f0dbc19-2f70-40cf-897f-bb4ff6a7dbfb"
},
"aud":
[
"test-system",
"http://iam.example.com/api/v1/exchange/token"
],
"exp": 1771610051,
"iat": 1771574051,
"identity":
{
"id": "9b060735-baf9-4398-9149-a42012def7c7",
"kind": "ServiceAccount",
"traits":
{
"email": null,
"family_name": null,
"given_name": null,
"name": "sa-test-subject",
"preferred_username": "sa-test-subject",
"tenant": "test"
}
},
"iss": "http://iam.example.com/api/v1",
"jti": "328620cc-3b7f-4075-aa3a-81f5c1fb6650",
"permissions":
{
"global_roles":
[],
"groups":
[],
"projects":
[
{
"key": "test",
"roles":
[
{
"permissions":
[
"iam.project_lead"
],
"role": "Project lead"
}
]
}
]
},
"spirit_iam":
{
"permissions":
{
"global_permissions":
[],
"tenant_permissions":
{
"test":
[
"iam.project_lead"
]
}
}
},
"sub": "9b060735-baf9-4398-9149-a42012def7c7"
},
"header": null,
"match_context":
{
"regexp_capture_groups":
[],
"url":
{
"Scheme": "http",
"Opaque": "",
"User": null,
"Host": "tenant-manager-oathkeeper",
"Path": "/api/v2/tenants/test",
"Fragment": "",
"RawQuery": "",
"RawPath": "",
"RawFragment": "",
"ForceQuery": false,
"OmitHost": false
}
},
"upstream_request":
{
"body":
{
"attributes":
{
"description": "test tenant description"
},
"kind": "Tenant"
},
"detected_content_type": "application/json",
"host": "tenant-manager-oathkeeper",
"is_body_truncated": false,
"method": "PATCH",
"path": "/api/v2/tenants/test",
"query":
{}
}
}

Для запросов от имени интегрированных сервисов

Пример для вызова с JWT-токеном сервиса, зарегистрированного в Spirit.

С JWT токеном межсервисной аутентификации Spirit
{
"subject": "some-service",
"extra":
{
"system": "some-service",
"tenant": "some-tenant"
},
"header": null,
"upstream_request":
{
"body": null,
"host": "app.example.com",
"is_body_truncated": false,
"method": "GET",
"path": "/api/v2/tenants/test-tenant/users-tree",
"query":
{
"limit":
[
"10"
],
"offset":
[
"0"
],
"search":
[
""
]
}
}
}
Анализ тела запроса в политиках

Тело запроса в OPA Input доступно в input.upstream_request.body. Обязательно передавайте заголовок Content-Type в запросах, чтобы оно попало в OPA Input.

Дополнительные данные для анализа в политике

В дополнение к контексту, поставляемому Spirit IAP, вы можете использовать дополнительные данные в своих политиках авторизации.

Рассмотрим только два способа:

  • подключение бандла со статическими данными;
  • получение данных путем внешнего http-вызова в момент исполнения политики.

Больше сведений о способах поставки внешних данных см. во внешней документации.

Статические данные

Статические данные можно поставлять в виде файла в формате json, подключаемого совместно с политиками авторизации в момент старта Spirit IAP.

Способ подходит для реализации исключений в политиках. Например, вам нужно разрешить конкретному сервис-аккаунту доступ к административному API вашего сервиса.

Для этого кейса в репозитории с политиками создайте файл следующего содержания:

{
"admin_service_accounts":
[
"c0d216e7-f7dd-46b1-a990-d6b64f516cc8"
]
}

Далее в момент исполнения политик эти данные можно извлечь следующим способом: data.admin_service_accounts, пример использования в политике:

input.subject == data.admin_service_accounts[_]

Написание политик авторизации

Нет единственного «правильного» способа написать политику. Здесь мы приведем подходы, которые, с нашей точки зрения, являются удачными.

Совет

Политика по умолчанию = действие запрещено (default allow = false). Политика по умолчанию срабатывает, если ни одна детальная политика не вернула true.

Совет

Политики можно разделять на несколько .rego файлов для удобства поддержания кодовой базы. Например, отдельно выделять политики глобальных ролей или для разных эндпоинтов.

При этом все политики должны входить в один пакет — по умолчанию = package http.authz.

Объявление алиасов для доступа к данным контекста авторизации

import input.extra.permissions as permissions

Объявление функций для стандартных политик

Пример функции для стандартной проверки, есть ли у субъекта полномочия, чтобы выполнить операцию над ресурсом:

policy_tenant_permission(policy_name, glob_path, http_methods, required_permission) = r {
r := [name |
name := policy_name

request_method in http_methods
glob.match(glob_path, ["/"], request_path)
tenant_key := split(request_path, "/")[4]
permissions.projects[i].key == tenant_key
permissions.projects[i].roles[_].permissions[_] == required_permission
]
}

Каждый вызов этой функции представляет реализацию политики для конкретного кейса с передачей параметров:

  • маска URI запрашиваемого ресурса;
  • метод(ы) http-запроса;
  • полномочие, которое необходимо иметь субъекту для выполнения операции над ресурсом.

Пример вызова функции с передачей параметров политики:

policy_tenant_permission(
"policy_tenant_s3_buckets_user_create",
"/api/v2/tenants/*/s3-buckets{,/}",
["POST"],
"s3aas.s3_buckets.create"
),

Авторизация для взаимодействий service-2-service

spirit_iam — рекомендуемый к использованию аутентификатор, предоставляет возможность аутентифицировать все типы субъектов (пользователи, сервис-аккаунты, JWT-токены сервисов).

Для ограничения доступа сервисов к вашему API по whitelist используйте списки доступа, поставляемые в составе политик как статичные данные.

Пример whitelist:

{
"allowed_systems": [
{
"system": "catalog-service",
"tenant": "example-tenant"
}
]
}

Пример использования в политике:

# Интегрированные системы с JWT — доступ к API каталога
[name |
name := "allow_systems_with_jwt_read_catalog_api"

input.upstream_request.method == "GET"
input.subject == data.allowed_systems[i].system
input.extra.tenant == data.allowed_systems[i].tenant
startswith(request_path, "/api/v2/catalog-units")
],

Определение типа субъекта (инициатора запроса)

Если при принятии авторизационного решения вам требуется проанализировать тип субъекта (пользователь / сервис-аккаунт / система, аутентифицированная через JWT-токен), вы можете использовать следующую функцию:

identity_kind(request) = "System" {
not request.extra.identity
} else = request.extra.identity.kind

Вызывайте ее в политиках авторизации:

# Разрешаем обращение только для систем, аутентифицированных по токену межсервисной авторизации (s2s JWT)
identity_kind(input) == "System"

Полный пример файла с политиками

Полный пример файла с политиками
policies.rego
package http.authz
import future.keywords.in

import input.extra.permissions as permissions
import input.upstream_request.method as request_method
import input.upstream_request.path as request_path

default allow = false

deny = reason {
not allow
reason := "No allow policies applied"
}

# функция универсальной политики с проверкой полномочий пользователя в тенанте
policy_tenant_permission(policy_name, glob_path, http_methods, required_permission) = r {
r := [name |
name := policy_name

request_method in http_methods
glob.match(glob_path, ["/"], request_path)
tenant_key := split(request_path, "/")[4]
permissions.projects[i].key == tenant_key
permissions.projects[i].roles[_].permissions[_] == required_permission
]
}


# массив политик
policies[p] { p := [

# политика для создания бакета S3, реализованная как вызов функции policy_tenant_permission()
# с передачей параметров политики
policy_tenant_permission(
"policy_tenant_s3_buckets_user_create",
"/api/v2/tenants/*/s3-buckets{,/}",
["POST"],
"s3aas.s3_buckets.create"
),

# аналогичная политика для удаления бакета S3
policy_tenant_permission(
"policy_tenant_s3_buckets_user_delete",
"/api/v2/tenants/*/s3-buckets/*",
["DELETE"],
"s3aas.s3_buckets.delete"
),


# кастомная политика, с проверкой внешних данных из подключаемого файла
# разрешает чтение любых ресурсов в API сервиса для прописанных в конфиге сервис-аккаунтов
[name |
name := "policy_admin_sa_read_all"

request_method in ["GET"]
glob.match("**", ["/"], request_path)
input.subject == data.admin_service_accounts[_]
],

# остальные политики ...

][_][_]
}

# вызов политик (здесь ничего менять не нужно)
allow {
policies[_]
}

Тестирование политик

OPA имеет встроенные механизмы тестирования политик. Подробнее см. во внешней документации.

Все тесты — синтетические (на моках), не требуют развернутого окружения.

Запуск тестов рекомендуется оформлять в CI, для этого используется бинарник OPA.

Совет

Рекомендуем разрабатывать тесты для покрытия всех кейсов:

  • успешной авторизации;
  • неуспешной авторизации.

Тесты оформляются на языке rego. Можно оформлять наборы тестовых данных как в составе файла с тестами, так и внешними файлами в формате JSON.

Примеры тестовых данных

Мок полномочий пользователей

users.json — данные о полномочиях пользователя. Имитация блока данных input.extra.permissions в OPA input:

{
"users": {
"newcomer": {
"global_roles": [],
"projects": []
},
"admin": {
"global_roles": [
{
"permissions": [
"s3aas.s3aas_admin"
]
}
]
},
"s3_manager": {
"projects": [
{
"key": "tenant-1",
"roles": [
{
"permissions": [
"s3aas.s3_buckets.create"
]
}
]
}
]
}
}
}
Мок URI запрашиваемых ресурсов

paths.json — используется для имитации URI запрашиваемого ресурса. Имитация input.upstream_request.path в контексте запроса:

{
"path": {
"resource_list": "/api/v2/tenants/tenant-1/s3-buckets",
"resource": "/api/v2/tenants/tenant-1/s3-buckets/resource-1"
}
}

Примеры тестов

Важно

Имя каждого теста должно начинаться с префикса test_.

tests.rego
package http.authz # должно соответствовать пакету политик, рекомендуем не менять

# пример проверки на успешную авторизацию
test_admin_read_buckets {
allow
with input.upstream_request.method as "GET"
with input.extra.permissions as data.users.admin # используется мок users.json
with input.upstream_request.path as data.path.resource_list # используется мок paths.json
}

# пример проверки на неуспешную авторизацию (когда запрос должен быть отказан, то есть не найдено разрешающих политик)
test_create_bucket_denied {
not allow
with input.upstream_request.method as "POST"
with input.extra.permissions as data.users.newcomer
with input.upstream_request.path as data.path.resource_list
}

Запуск тестов

../opa test policies.rego test.rego users.json paths.json
data.http.authz.test_admin_read_buckets: FAIL (565.13µs)
--------------------------------------------------------------------------------
PASS: 7/8
FAIL: 1/8

Видно, что 1 тест упал. Смотрим в тест, смотрим в политики, видим, что забыли разрешить админу чтение списка бакетов.

Совет

Можно запускать команду opa test с флагом --explain, принимающим значения:

  • notes — данные о длительности каждого теста;
  • full — полные пошаговые сведения о применении политик по каждому тестовому запросу;
  • fails — пошаговые сведения о применении политик по упавшим тестам.

Чтобы проверить покрытие политик тестами, запустите команду opa test с флагом --coverage.