
Пользовательская функция Rego на примере
7 апреля 2022 г.Open Policy Agent и собственный язык Rego богаты возможностями и в то же время просты в освоении. Однако иногда реалии начинают требовать все больше и больше по мере того, как проект растет, и разработчикам нужно разрабатывать решения. К счастью, Rego позволяет нам создавать [пользовательские встроенные функции] (https://www.openpolicyagent.org/docs/latest/extensions/#custom-built-in-functions-in-go) и сейчас я расскажу как это сделать на живом примере.
Я реализовал OPA для клиентов авторизации в API. Требовалось, исходя из ассоциации пользователя с группами (командами, юнитами и т.д.), разрешать или не разрешать какие-то действия (в реальности в политику было вовлечено гораздо больше факторов, но это не имеет отношения к текущей теме).
Допустим, у нас есть какая-то групповая структура и пользователи, которые могут одновременно состоять более чем в одной группе. Да и сами группы тоже имеют отношения «многие ко многим».
Как видите, один из разработчиков является участником сразу двух команд. Сначала он работал в команде биллинга, но обладал навыками devops и хотел развиваться как devops-инженер. Также есть отряд спецназа, который призван тушить пожары и помогать другим отрядам в нужное время вывозить требуемые объемы, поэтому он входит в состав двух других отрядов.
А действий в системе два:
- кордон - кордонный узел
- deploy — развернуть приложение в продакшене
Действия привязаны к группе и также применяются ко всем дочерним группам.
Задача — написать правило для OPA.
Решение сначала требует, чтобы организационная структура в формате json использовалась в правилах rego. Об этом:
```javascript
"пользователи": [
"id": "оливер",
"группы": ["выставление счетов"]
"id": "лиам",
"группы": ["сват"]
"группы": [
"id": "инфра",
"родитель": []
"id": "devops",
"родитель": ["инфра"]
"id": "админ",
"родитель": ["инфра"]
"идентификатор": "разработчик",
"родитель": []
"id": "поиск",
"родитель": ["разработчик"]
"id": "биллинг",
"родитель": ["разработчик"]
"id": "сват",
"родитель": ["информация", "выставление счетов"]
Поскольку правила распределяются по группам, достаточно проверить, состоит ли пользователь в нужной группе или нет. И является ли группа пользователей подгруппой другой группы (на любом уровне вложенности). Вот тут и возникла проблема. Если рекурсию делать на Rego (не уверен, что это возможно), то читабельность будет ужасной. Готовых функций тоже нет. Но, к счастью, есть возможность расширить Rego своими функциями.
Вот реализация самой пользовательской функции:
```javascript
// поиск всех родителей группы
func RegoGroupParentsFunction() func(*rego.Rego) {
вернуть rego.Function2(®o.Function{
Имя: "группа_родители",
Декл: типы.НоваяФункция(типы.Аргументы(типы.S, типы.А), типы.А),
func(bctx rego.BuiltinContext, groupID, groupsData ast.Term) (ast.Term, ошибка) {
группы := []Группа{}
если ошибка: = json.Unmarshal([]byte(groupsData.Value.String()), &groups); ошибка != ноль {
вернуть nil, fmt.Errorf("неупорядочить данные групп rego: %v", ошибка)
mappedGroups := карта[строка]*Группа{}
для k группа := диапазон групп {
mappedGroups[group.ID] = &groups[k]
gID := trimDoubleQuotes(groupID.Value.String())
родительские группы := []*Группа{}
SearchParentsRecursive(gID, mappedGroups, &parentGroups)
значения := []*ast.Term{}
для _, v := диапазон parentGroups {
val, ошибка := ast.InterfaceToValue(v)
если ошибка != ноль {
return nil, fmt.Errorf("преобразовать группу в значение rego: %v", ошибка)
значения = добавить (значения, ast.NewTerm (val))
вернуть ast.ArrayTerm(значения...), ноль
func SearchParentsRecursive (строка groupID, карта групп [строка] * Группа, результат * [] * Группа) {
группа, хорошо := группы[идентификатор группы]
если! хорошо {
вернуть
если len(group.Parents) == 0 {
вернуть
для _, parentID := range group.Parents {
parentGroup, хорошо := groups[parentID]
если! хорошо {
Продолжать
SearchParentsRecursive(parentID, группы, результат)
- результат = добавить (* результат, родительская группа)
Я могу описать, что здесь происходит:
```javascript
вернуть rego.Function2(®o.Function{
Имя: "группа_родителей",
Декл: типы.НоваяФункция(типы.Аргументы(типы.S, типы.А), типы.А),
func(bctx rego.BuiltinContext, groupID, groupsData ast.Term) (ast.Term, ошибка) {
Определена новая функция group_parents
, которая имеет два аргумента: идентификатор группы, которую вы ищете, и структуру группы, описанную выше. Соответственно для рего-функций с тремя аргументами есть функция Функция3
, с четырьмя Функция4
и т.д.
Далее происходит парсинг json со структурой и рекурсивный поиск всех родителей. Сложности возникают при возврате результата, т.к. интерфейс opa-sdk не очень очевиден.
```javascript
значения := []*ast.Term{}
для _, v := диапазон parentGroups {
val, ошибка := ast.InterfaceToValue(v)
если ошибка != ноль {
return nil, fmt.Errorf("преобразовать группу в значение rego: %v", ошибка)
значения = добавить (значения, ast.NewTerm (val))
вернуть ast.ArrayTerm(значения...), ноль
Функция должна возвращать массив со всеми родительскими группами, чтобы пройти сами правила. Поэтому мы возвращаем ast.ArrayTerm. Это массив значений rego (Term
), который должен быть предварительно создан с помощью ast.NewTerm
. Перед этим вам также нужно будет преобразовать строковые значения из идентификатора ast.InterfaceToValue.
Теперь group_parents доступна в политиках rego. Я покажу вам, как это работает:
```javascript
пакет group_search
импортировать future.keywords.in
по умолчанию parent_groups_is_ok = ложь
Проверяем, что группа SWAT состоит из всех стоящих групп биллинга и devops
parent_groups_is_ok {
группы: = group_parents («сват», data.groups)
группы[0].name == "выставление счетов"
группы[1].name == "devops"
Убеждаемся, что в большом количестве групп swat нет
parent_groups_not_exists {
группы: = group_parents («сват», data.groups)
группы[_].имя != "поиск"
Теперь я могу проверить:
```javascript
func TestGroupParentsOk(t *testing.T) {
запрос, ошибка := rego.New(
rego.Query("data.group_search.parent_groups_is_ok"),
RegoGroupParentsFunction()
rego.LoadBundle ("тестовые данные"),
).PrepareForEval(context.Background())
если ошибка != ноль {
t.Fatalf("подготовить рего-запрос: %v", ошибка)
набор результатов, ошибка: = запрос.Eval(context.Background())
если ошибка != ноль {
t.Fatalf("оценка повторного запроса: %v", ошибка)
если len(resultSet) == 0 {
t.Error("неопределенный результат")
assert.True(t, resultSet.Allowed())
функция TestGroupParentsNotExist(t *testing.T) {
запрос, ошибка := rego.New(
rego.Query("data.group_search.runtime_parent_groups_not_exists"),
RegoGroupParentsFunction(),
rego.LoadBundl ("тестовые данные"),
).PrepareForEval(context.Background())
если ошибка != ноль {
t.Fatalf("подготовить рего-запрос: %v", ошибка)
набор результатов, ошибка: = запрос.Eval(context.Background())
если ошибка != ноль {
t.Fatalf("оценка повторного запроса: %v", ошибка)
если len(resultSet) == 0 {
t.Error("неопределенный результат")
assert.True(t, resultSet.Allowed())
Таким образом можно продлить рего столько, сколько вам нужно. Например, добавить работу с какими-то внешними источниками: базами данных, заголовками и т.д. Или как в моем случае реализовать сложную логику.
Оригинал