Querying Redis
Enterprise OPA provides the redis.query
built-in function for querying Redis at the time of a policy decision.
Overview
In the following example, we will create a simple policy which validates toppings for orders at a restaurant. We will use Redis to store the types of toppings allowed for different types of food orders. This will allow us to quickly change the allowed toppings without deploying a new bundle by adjusting the values stored in Redis.
The input document we will expect:
- A
type
, one ofpizza
,sandwich
, orsalad
- An array of
toppings
The policy will output an object with a Boolean allowed
key, and if the request was denied, it will also include a reason
field explaining why.
Project Setup
You'll need to have Docker already installed, set up, and working to follow along.
-
Launch a Redis server
docker run --publish 6379:6379 redis:latest
-
Open a new terminal and note down the container ID. You can find it by using the command
docker ps
. You may wish to save it in an environment variable for later use:# example, your container ID will be different
export REDIS_CONTAINER_ID=6228413ddb17 -
Populate the Redis database with sample data:
docker exec $REDIS_CONTAINER_ID redis-cli SADD salad_toppings tomato onion carrot radish "bell pepper" olive feta gorgonzola bacon
docker exec $REDIS_CONTAINER_ID redis-cli SADD pizza_toppings onion "bell pepper" olive feta bacon
docker exec $REDIS_CONTAINER_ID redis-cli SADD sandwich_toppings lettuce tomato bacon onion swiss bacon -
Create a Rego policy using the
redis.query()
builtin, save it to./policy.rego
package main
import rego.v1
addr := "localhost:6379"
toppings_key := sprintf("%s_toppings", [input.type])
invalid_toppings[t] if {
some t in input.toppings
redis.query({"addr": addr, "command": "SISMEMBER", "args": [toppings_key, t]}).results != true
}
main := decision if {
input.type == ["sandwich", "pizza", "salad"][_]
count(invalid_toppings) > 0
decision := {
"allow": false,
"reason": sprintf("order of type '%s' is not allowed to contain toppings: %+v", [input.type, invalid_toppings]),
}
} else := decision if {
input.type == ["sandwich", "pizza", "salad"][_]
count(invalid_toppings) == 0
decision := {"allow": true}
} else := decision if {
decision := {
"allow": false,
"reason": sprintf("order type '%s' is not allowed", [input.type]),
}
} -
Launch an Enterprise OPA server:
eopa run -s ./policy.rego
-
Now, in a new terminal, we can use
curl
to see how this policy will react to different inputs. For example:curl -LSs -X POST --data '{"input": {"type": "pizza", "toppings": ["onion", "bacon"]}}' localhost:8181/v1/data/main/main
{"result":{"allow":true}}
curl -LSs -X POST --data '{"input": {"type": "pizza", "toppings": ["onion", "bacon", "gorgonzola"]}}' localhost:8181/v1/data/main/main
{"result":{"allow":false,"reason":"order of type 'pizza' is not allowed to contain toppings: {\"gorgonzola\"}"}} -
To demonstrate how we can change the behavior of the policy without loading a new bundle, we'll add
gorgonzola
to the allowed pizza toppings Redis database. Remember to re-export yourREDIS_CONTAINER_ID
if you have changed terminals.docker exec $REDIS_CONTAINER_ID redis-cli SADD pizza_toppings gorgonzola
1
curl -LSs -X POST --data '{"input": {"type": "pizza", "toppings": ["onion", "bacon", "gorgonzola"]}}' localhost:8181/v1/data/main/main
{"result":{"allow":true}}
Using Redis Authentication
Previously, we used an unauthenticated Redis server, but in many production use cases, you will need to use authentication to connect to Redis. Picking up where we left off in the previous section...
We can add a user with a password to our Redis database using the commands below.
docker exec $REDIS_CONTAINER_ID redis-cli config set requirepass supersecret123
docker exec $REDIS_CONTAINER_ID redis-cli acl setuser tutorial allcommands allkeys on '>letmein!'
We can now access Redis using either the password on the default user:
eopa eval -f pretty 'redis.query({"addr": "localhost:6379", "auth": {"password": "supersecret123"}, "command": "SMEMBERS", "args": ["pizza_toppings"]})'
{
"results": [
"onion",
"bell pepper",
"olive",
"feta",
"bacon",
"gorgonzola"
]
}
... or using our tutorial
user:
eopa eval -f pretty 'redis.query({"addr": "localhost:6379", "auth": {"username": "tutorial", "password": "letmein!"}, "command": "SMEMBERS", "args": ["pizza_toppings"]})'
{
"results": [
"onion",
"bell pepper",
"olive",
"feta",
"bacon",
"gorgonzola"
]
}
We can modify the policy we wrote in the previous section to use this new username and password by changing the line:
redis.query({"addr": addr, "command": "SISMEMBER", "args": [toppings_key, t]}).results != true
to:
redis.query({"addr": addr, "auth": {"username": "tutorial", "password": "letmein!"}, "command": "SISMEMBER", "args": [toppings_key, t]}).results != true
To avoid hard-coding database credentials in your policy, look into the vault helpers discussed in the built-in documentation.