Rego 102: Combinar consultas con AND/OR y mensajes personalizados
9 de noviembre de 2023
0 minutos de lecturaEsta serie de publicaciones ofrece una introducción sencilla a Rego, el lenguaje de políticas de los creadores del motor Open Policy Agent (OPA). Si eres principiante y quieres empezar a escribir políticas como código con Rego, estás en el lugar indicado.
En esta serie de tres partes, veremos lo siguiente:
Parte 2 (¡esta parte!): Sintaxis intermedia de Rego
Parte 3: Tipos de valores y reglas
Como recordatorio, Rego es un lenguaje declarativo de consultas creado por los autores del framework Open Policy Agent (OPA). La Cloud Native Computing Foundation (CNCF) aceptó OPA como proyecto alojado en etapa de incubación en abril de 2019, y OPA se graduó de esa etapa en 2021.
Rego se usa para escribir políticas como código, lo que aplica prácticas de programación como el control de versiones y el diseño modular para evaluar recursos de la nube e infraestructura como código (IaC). OPA es el motor que evalúa las políticas como código escritas en Rego. Snyk usa el lenguaje Rego para crear reglas personalizadas.
Repaso de la parte 1
En la parte 1 de esta serie, explicamos que una regla de Rego es una asignación condicional. Una regla consulta la entrada para encontrar una coincidencia con una condición y, si la encuentra, asigna un valor a una variable.
Puedes leer una regla de esta manera:
Este es el ejemplo que usamos, que representa una política corporativa según la cual solo Alice, una administradora de red, debe tener permiso para crear y eliminar redes virtuales en la cuenta de producción:
OPA evalúa un documento de entrada en formato JSON o YAML según una regla para emitir una decisión de política. El documento de entrada a continuación representa al usuario que inició sesión:
Si usas OPA para evaluar la entrada según esta regla, encontrará una coincidencia con la consulta input.user == "alice". Por lo tanto, a la variable de la cabecera de la regla, allow, se le asigna el valor de la cabecera, true. Este es el resultado que lo demuestra:
OPA determinó que Alice, la usuaria que inició sesión, tiene permiso para crear y eliminar redes virtuales en la cuenta de producción. La entrada cumple con la regla.
AND y OR
Hasta ahora, solo mostramos reglas con una consulta. Una regla también puede contener varias consultas. En ese caso, las consultas representan varias condiciones que todas deben cumplirse para asignar un valor a una variable. Hay un AND implícito: «Esta condición debe cumplirse AND esta condición debe cumplirse».
Por ejemplo, en la regla siguiente, tanto input.user == "alice" AND input.environment == "prod" deben ser verdaderos para asignar el valor true a la variable allow:
En algunos casos, puede ser más apropiado usar OR. Puedes representar OR usando la misma cabecera en varias reglas:
Este conjunto de reglas se puede leer así:
allow es true si user es "alice" OR si user es "bob".
Técnicamente, el conjunto de reglas forma una sola regla porque ambas tienen la misma cabecera. Como defines esta regla en varios pasos, se llama regla incremental.
Si quieres, puedes quitar la segunda cabecera y unir los cuerpos. Una forma más concisa de escribir lo anterior es:
Quizás te preguntes por qué usamos el operador de unificación = en lugar del operador de asignación :=. Esto se debe a que las variables son inmutables en Rego. Aunque las reglas con la misma cabecera se tratan como una sola regla incremental, si intentas usar el operador de asignación, en la práctica estás «asignando» la misma variable varias veces, y Rego no lo permite. En su lugar, usamos el operador de unificación en la cabecera de la regla porque unifica varias reglas con el mismo nombre.
Si te cuesta recordar cuándo usar cada operador en la cabecera de la regla, hay una forma más sencilla gracias a los valores predeterminados y a un poco de azúcar sintáctico.
Valores predeterminados en las cabeceras de las reglas
El valor predeterminado que se asigna a una variable en la cabecera de una regla es true. Por eso, Rego ofrece un poco de azúcar sintáctico: cuando una regla asigna el valor true a la variable, puedes omitir := true de la cabecera de la regla.
Eso significa que esta regla AND…
…es igual a esta regla AND:
Y, del mismo modo, esta regla OR…
...es igual a esta regla OR:
¡Genial, de verdad!
Palabra clave default
Como explicamos en la parte 1, si ninguna entrada coincide con la consulta de una regla, la variable de la cabecera no recibe el valor indicado en la cabecera.
Para demostrarlo, volvamos a la regla de ejemplo, que dice que solo Alice, una administradora de red, debe tener permiso para crear y eliminar redes virtuales en la cuenta de producción:
Y supongamos que tenemos un documento de entrada en el que Bob es el usuario que inició sesión:
Como input.user no es "alice", al evaluar la regla según la entrada, OPA no encuentra ninguna coincidencia. Por lo tanto, a allow no se le asigna el valor true, y el resultado de la evaluación es un conjunto vacío:
En este caso, decimos que el valor de allow está indefinido. Cada vez que OPA consulta la entrada para evaluar una regla, solo devuelve los valores que coinciden. Si no hay ningún valor coincidente, no hay nada que devolver; por eso, el conjunto está vacío.
¿Y si queremos que OPA devuelva false cuando allow no sea explícitamente true? Podemos usar la palabra clave default para establecer un valor predeterminado. Esto significa que, si la evaluación de una regla no da explícitamente true, devuelve un valor específico (en este caso, false) en lugar de un conjunto vacío de resultados. Para hacerlo, escribimos una regla adicional que también usa allow:
Ahora, si OPA determina que input.user no es "alice", allow no se evalúa como un conjunto vacío. En su lugar, toma el valor predeterminado que declaramos: false:
Ten en cuenta que, cuando especificas la palabra clave default, usas el operador de unificación = en lugar del operador de asignación := tanto en la regla donde defines el valor predeterminado como en la regla donde defines la asignación condicional. De nuevo, esto se debe a que las variables son inmutables. Si intentas usar el operador de asignación, estarías «asignando» la misma variable varias veces, algo que Rego no permite. En su lugar, usamos el operador de unificación:
Por supuesto, puedes aprovechar el azúcar sintáctico que describimos antes y omitir = true en la regla con la asignación condicional. Esto es totalmente válido y quizás más fácil de usar, porque no necesitas recordar qué operador usar en la asignación condicional:
Mensajes personalizados
A veces, quizás quieras devolver una serie de mensajes en lugar de un simple resultado de aprobado o reprobado, true o false/indefinido. Puedes hacerlo usando la cabecera de regla deny[msg] y asignando el mensaje deseado a la variable msg. La regla siguiente comprueba si el usuario no es Alice y, en ese caso, asigna la cadena "User is denied access" a msg, que luego se agrega al conjunto deny (hablaremos más sobre los conjuntos en la parte 3):
Para probarlo, supongamos que nuestro documento de entrada contiene el nombre del usuario que inició sesión:
Cuando evaluamos la regla según la entrada anterior con Rego Playground de OPA o el comando opa eval -i input.json -d check_user.rego "data.rules.check_user" --format pretty (consulta las instrucciones en nuestra publicación de la parte 1), el resultado es el siguiente:
¿Qué está pasando aquí? En realidad, estamos creando una regla de conjunto, un concepto al que volveremos en la próxima publicación de esta serie. Por ahora, basta con entender que, en lugar de un único resultado true o false/indefinido, devolvemos un conjunto de mensajes asignados a la variable deny. En Rego, un conjunto es una lista desordenada de elementos únicos, como números enteros { 1, 2, 3 }, cadenas { "alice", "bob", "carlotta" } o incluso otros conjuntos { { 1, 2}, {3, 4} }. Puedes crear un conjunto con cualquier tipo compatible con Rego o incluso combinar distintos tipos en un mismo conjunto.
En el ejemplo de nuestra regla, cada elemento del conjunto deny es una cadena que contiene un mensaje. Este documento de entrada en particular solo tiene un elemento en el conjunto, pero puede haber más; te mostraremos un ejemplo más adelante en esta publicación.
¿Y si queremos incluir información adicional en el mensaje? Podemos usar la función integrada sprintf para mostrar el valor del campo input.user que provocó un resultado deny:
La función sprintf acepta dos argumentos: una cadena y un arreglo de valores. En este caso, el único elemento del arreglo es una cadena representada por input.user. Usamos %v como marcador de posición en el primer argumento; al evaluar la regla, se reemplaza con el valor del arreglo.
Ahora, si evaluamos la regla con la siguiente entrada…
…vemos este resultado:
La palabra clave not
Puedes negar una expresión anteponiéndole la palabra clave not para que signifique lo contrario. La mayoría de las veces, querrás usarla en una consulta para indicar que una propiedad está ausente de la entrada. Por ejemplo, esta consulta:
…significa «El documento de entrada tiene una propiedad tags.environment y su valor no es false», mientras que esta consulta:
…significa «El documento de entrada no tiene una propiedad tags.environment o tags.environment tiene el valor false». No hay superposición ni punto medio: una expresión y su inversa son mutuamente excluyentes.
Aquí tienes una regla de ejemplo que asigna true a deny si a la entrada le falta una etiqueta department :
Usemos esta entrada:
Si evaluamos esta entrada según la regla anterior, veremos que deny devuelve true porque falta la propiedad obligatoria department:
Evaluar una regla de ejemplo con OPA
Probemos los conceptos que vimos en esta publicación evaluando una regla de ejemplo. Al igual que en la parte 1, nos centraremos en dos maneras de interactuar con OPA:
Usar Rego Playground
Usar la herramienta de línea de comandos de OPA
Para ver las instrucciones sobre cómo usar estas interfaces, consulta la parte 1.
Esta vez, usaremos un ejemplo más cercano al mundo real que incluye un pod de Kubernetes. Este es el manifiesto JSON que usaremos como entrada:
Y esta es la regla que evaluaremos, escrita para aplicar la política de la empresa «Los pods de Kubernetes deben tener etiquetas de versión y entorno»:
Esta regla ilustra algunos de los conceptos que vimos en esta publicación:
deny[msg]para devolver un conjunto de mensajes personalizados en lugar detrueofalse/undefinedEstructura de reglas AND y OR:
Denegar si el objeto de Kubernetes es un pod AND le falta la etiqueta de versión, OR:
Denegar si el objeto de Kubernetes es un pod AND le falta la etiqueta de entorno
La palabra clave
notpara comprobar si falta una propiedadLa función sprintf para devolver un mensaje que indique el nombre del pod que no cumple con los requisitos
Para que te resulte más fácil, creamos un playground que ya contiene este contenido: https://play.openpolicyagent.org/p/KNVK9kEvIT
Si evalúas la regla seleccionando el botón Evaluate en el playground o ejecutando un comando como opa eval -i input.json -d check_pod.rego "data.rules.check_pod" --format pretty si ejecutas OPA localmente, verás este resultado:
Como podemos ver, el pod de Kubernetes que estamos revisando no cumple con nuestra regla porque la entrada no contiene una propiedad labels.environment .
Ahora, quitemos la propiedad labels.release. La sección labels de la entrada debería verse así:
Si evalúas la regla ahora, verás que el conjunto deny contiene dos mensajes:
Por último, agreguemos las propiedades labels.release y labels.environment a la entrada, para que se vea así:
¿Qué pasa si volvemos a evaluar la regla? Vemos que el conjunto deny está vacío:
Esto significa que nuestro pod cumple con los requisitos porque OPA no agregó ningún mensaje al conjunto deny. ¡Genial!
¿Qué sigue?
No olvides volver a nuestro blog para leer Rego para principiantes, parte 3, donde exploraremos reglas de conjunto, reglas de objeto, funciones e iteración.
Mientras tanto, aquí tienes algunos recursos útiles:
Si te interesa usar Rego para escribir reglas personalizadas para Snyk IaC, consulta nuestra documentación aquí. Además de los conjuntos de reglas integrados de Snyk, alineados con los requisitos de seguridad y cumplimiento, las reglas personalizadas de IaC+ te permiten definir controles de seguridad personalizados en todo tu SDLC.
IaC+ te ofrece una vista única y controles para tus problemas de configuración, desde el código hasta la nube, con una interfaz de usuario para problemas, un conjunto de reglas y un motor de políticas que abarcan IDE, SCM, CLI, CI/CD, Terraform Cloud y entornos de nube implementados, como AWS, Azure y Google Cloud.
