Permissions management is an interesting topic. Modern applications are often
complex beasts. It doesn’t take much time to hit the point where certain
functionality must be allowed only to the selected users or access to a resource
should be granted only under certain conditions.
Building a flexible permissions management system is not easy. They tend to be
tightly coupled with the business logic and executed every time a decision
whether to grant or deny access is required. There are as many requirements as
software systems out there. A permissions management systems must also perform.
If decisions are to be made often, the latency to make a decision must be
minimal.
Ory Keto is one of the Ory platform components and a few days ago it has seen a
major upgrade. Versions 0.6.0-alpha.1 is a complete reimplementation of Keto and
is marketed as the first open source implementation of
Zanzibar: Google’s Consistent, Global Authorization System.
From the Zanzibar abstract:
Determining whether online users are authorized to access digital objects is
central to preserving privacy. This paper presents the design, implementation,
and deployment of Zanzibar, a global system for storing and evaluating access
control lists. Zanzibar provides a uniform data model and configuration
language for expressing a wide range of access control policies from hundreds
of client services at Google, including Calendar, Cloud, Drive, Maps, Photos,
and YouTube. Its authorization decisions respect causal ordering of user
actions and thus provide external consistency amid changes to access control
lists and object contents. Zanzibar scales to trillions of access control
lists and millions of authorization requests per second to support services
used by billions of people. It has maintained 95th-percentile latency of less
than 10 milliseconds and availability of greater than 99.999% over 3 years of
production use.
I have been using previous Keto versions for some R&D work. The new version has
sparked my interest because the firebuild system I am working on is going to
be in need of a permissions system at some point in time.
To better understand the new Keto, I wanted a simple, easy to follow scenario so
a judgement can be formed quickly.
In one short sentence: it was very easy and it’s impressive!!
The scenario
The scenario I was evaluating:
- a company employs a director and IT staff
- the director contracts a consultant
- the IT staff subscribes to external services
Find out what the company pays for directly and indirectly.
Here’s the rough diagram:

Ory Keto in Docker Compose
I have previously written
about my reference Ory Docker Compose
and that is what I’m using further. To start the local installation, assuming
the containers are already built, as explained in the
repository readme:
git clone https://github.com/radekg/ory-reference-compose.git
cd ory-reference-compose/compose
docker-compose -f compose.yml up
The new Keto version exposes two API servers:
- the write API: default port is
4467
- the read API: default port is
4466
Before we can start querying Keto for a decision, we have to create a few
relation tuples.
Keto does not try understanding our data, it infers the decision by inspecting
and traversing tuples it knows.
Relation tuples can be created using cURL via the write API. The Compose setup
publishes both Keto APIs so we can start like this:
curl -XPUT --data '{
"namespace": "default-namespace",
"object": "company-a",
"relation": "employs",
"subject": "director"
}' http://localhost:4467/relation-tuples
curl -XPUT --data '{
"namespace": "default-namespace",
"object": "company-a",
"relation": "employs",
"subject": "it-staff"
}' http://localhost:4467/relation-tuples
The associations above imply that the company pays for the director and the IT
staff. We could model it like this:
curl -XPUT --data '{
"namespace": "default-namespace",
"object": "company-a",
"relation": "pays",
"subject": "default-namespace:company-a#employs"
}' http://localhost:4467/relation-tuples
That subject means: the company pays for anybody it employs. Let’s see this
in action by executing this request against the read API:
curl --silent 'http://localhost:4466/expand?namespace=default-namespace&object=company-a&relation=pays&max-depth=10' | jq '.'
The output is:
{
"type": "union",
"subject": "default-namespace:company-a#pays",
"children": [
{
"type": "union",
"subject": "default-namespace:company-a#employs",
"children": [
{
"type": "leaf",
"subject": "director"
},
{
"type": "leaf",
"subject": "it-staff"
}
]
}
]
}
Now, the IT staff subscribes to AWS, Dropbox and GCP. These relations could be
modeled like this:
curl -XPUT --data '{
"namespace": "default-namespace",
"object": "it-staff",
"relation": "subscribes",
"subject": "aws"
}' http://localhost:4467/relation-tuples
curl -XPUT --data '{
"namespace": "default-namespace",
"object": "it-staff",
"relation": "subscribes",
"subject": "dropbox"
}' http://localhost:4467/relation-tuples
curl -XPUT --data '{
"namespace": "default-namespace",
"object": "it-staff",
"relation": "subscribes",
"subject": "gcp"
}' http://localhost:4467/relation-tuples
As these will bear the cost to the company, the JSON report should list them and
can be done with this new relation tuple:
curl -XPUT --data '{
"namespace": "default-namespace",
"object": "company-a",
"relation": "pays",
"subject": "default-namespace:it-staff#subscribes"
}' http://localhost:4467/relation-tuples
That means: the company pays for everything the IT staff subscribes to. If we
execute the /expand call again, the output will be:
{
"type": "union",
"subject": "default-namespace:company-a#pays",
"children": [
{
"type": "union",
"subject": "default-namespace:company-a#employs",
"children": [
{
"type": "leaf",
"subject": "director"
},
{
"type": "leaf",
"subject": "it-staff"
}
]
},
{
"type": "union",
"subject": "default-namespace:it-staff#subscribes",
"children": [
{
"type": "leaf",
"subject": "aws"
},
{
"type": "leaf",
"subject": "dropbox"
},
{
"type": "leaf",
"subject": "gcp"
}
]
}
]
}
Pretty cool, regardless of how many services the IT staff would subscribe to in
the future, they will get listed in the output!
Knowing about the cost of a consultant contracted by the director could be
modeled in the following way:
curl -XPUT --data '{
"namespace": "default-namespace",
"object": "director",
"relation": "contracts",
"subject": "consultant"
}' http://localhost:4467/relation-tuples
curl -XPUT --data '{
"namespace": "default-namespace",
"object": "company-a",
"relation": "pays",
"subject": "default-namespace:director#contracts"
}' http://localhost:4467/relation-tuples
{
"type": "union",
"subject": "default-namespace:company-a#pays",
"children": [
{
"type": "union",
"subject": "default-namespace:company-a#employs",
"children": [
{
"type": "leaf",
"subject": "director"
},
{
"type": "leaf",
"subject": "it-staff"
}
]
},
{
"type": "union",
"subject": "default-namespace:director#contracts",
"children": [
{
"type": "leaf",
"subject": "consultant"
}
]
},
{
"type": "union",
"subject": "default-namespace:it-staff#subscribes",
"children": [
{
"type": "leaf",
"subject": "aws"
},
{
"type": "leaf",
"subject": "dropbox"
},
{
"type": "leaf",
"subject": "gcp"
}
]
}
]
}
And if the director decides to contract a solicitor?
curl -XPUT --data '{
"namespace": "default-namespace",
"object": "director",
"relation": "contracts",
"subject": "solicitor"
}' http://localhost:4467/relation-tuples
the existing relation already covers that:
curl --silent 'http://localhost:4466/expand?namespace=default-namespace&object=company-a&relation=pays&max-depth=10' | jq '.'
...
{
"type": "union",
"subject": "default-namespace:director#contracts",
"children": [
{
"type": "leaf",
"subject": "consultant"
},
{
"type": "leaf",
"subject": "solicitor"
}
]
},
...
Very neat. At some point, we might want to have an answer to the following
direct question:
does the company pay for the consultant?
curl -XPOST --data '{
"namespace": "default-namespace",
"object": "company-a",
"relation": "pays",
"subject": "consultant"
}' http://localhost:4466/check
To which the answer is:
{
"allowed": true
}
If we ever had to understand what relation causes the company to pay for the
consultant, we would use the expand call with sufficient depth.
Notes on the examples
The examples use literal object identifiers for readability purposes. Keto
documentation suggests using UUIDv4 or SHA-1 hashes instead. The concept is
better explained on this page.
Conclusion
This very simple example shows in just few steps how awesome the new version of
Keto is.
The API is very simple to use and understand. The flexibility and applicability
is endless.
This is something what will greatly benefit firebuild and I am sure we will
see Keto being used all over the place.
The Ory team deserves huge applause for making this technology so easily
approachable.
Footnotes
-
Zanzibar: Google’s Consistent, Global Authorization System
-
Ory reference Docker Compose and thoughts on the platform
-
Reference Ory Docker Compose setup readme
-
Ory Keto concepts: relation tuples
-
Ory Keto concepts: objects