RBAC with Ory Keto

Building bare bones RBAC with Keto 0.6
thumbnail

Role-base Access Control is an access control method whereby the entity roles define the level of access. Usually when talking about RBAC, the entity is a person and the object is a resource or a task (function) granted to a person. The usual example goes like this:

In an organization, the job functions define the roles of employees. Only employees in specific roles are allowed to execute certain tasks. The employees are given permissions to execute these tasks. Sometimes the employees may gain additional permissions to execute more tasks, sometimes certain permissions are taken away and the employees cannot execute selected tasks anymore.

Update, 17th of May 2021: When publishing this article yesterday, I have incorrectly assumed that usersets aren’t implemented by Keto. The usersets are implemented, as explained by Patrik from the Ory team here[1]. What is not yet implemented is the rewrites functionality. I have updated the article to reflect this new fact.

§theory RBAC

The English Wikipedia has a very good, comprehensive entry on RBAC[2]. We can find a number of relevant RBAC related definitions, mainly:

  1. Role assignment: A subject can exercise a permission only if the subject has selected or been assigned a role.
  2. Role authorization: A subject’s active role must be authorized for the subject. With rule 1 above, this rule ensures that users can take on only roles for which they are authorized.
  3. Permission authorization: A subject can exercise a permission only if the permission is authorized for the subject’s active role. With rules 1 and 2, this rule ensures that users can exercise only permissions for which they are authorized.

These can be a subject to additional organizational hierarchy where higher-level roles incorporate the ones of the subordinates. With this in mind, we find the conventions useful:

  • S = Subject = A person or automated agent
  • R = Role = Job function or title which defines an authority level
  • P = Permissions = An approval of a mode of access to a resource
  • SE = Session = A mapping involving S, R and/or P
  • SA = Subject Assignment
  • PA = Permission Assignment
  • RH = Partially ordered Role Hierarchy. RH can also be written: ≥ (The notation: x ≥ y means that x inherits the permissions of y.)
    • A subject can have multiple roles.
    • A role can have multiple subjects.
    • A role can have many permissions.
    • A permission can be assigned to many roles.
    • An operation can be assigned to many permissions.
    • A permission can be assigned to many operations.

Further, Wikipedia suggests the following set theory notation:

  • PA ⊆ P × R
  • SA ⊆ S × R
  • RH ⊆ R × R

Grokking these is straightforward, in order:

  • permission assignment is a subset of permissions multiplied by roles
  • subject assignment is a subset of subjects multiplied by roles
  • role hierarchy is a subset of roles multiplied by other roles

In other words:

the subject (a user) is allowed (has the permission) to execute certain action (on an object) when they have certain roles; the roles can inherit permissions of other roles

The second part is important because it suggests that the permissions of certain roles in the hierarchy can change.

What’s crucial, we can parse the first statement in reverse:

an action can be performed on an object by a subject holding specific roles

Let’s hold on to that thought and proceed.

§the…ory keto

Keto is an implementation of the Zanzibar whitepaper[3] from Google. Zanzibar, thus Keto, is used to implement Access Control Lists (ACL). RBAC differs from ACL in that RBAC assigns permissions to operations instead of objects. Keto (Zanzibar) does not have any knowledge of the system it controls the access on behalf of. There are four major terms in the Zanzibar whitepaper:

  • a relation tuple
  • an object
  • a relation
  • a subject

where the relation tuple is a result of an object, permission and a subject. Straight from the whitepaper:

〈tuple〉::=〈object〉‘#’〈relation〉‘@’〈user〉
〈object〉::=〈namespace〉‘:’〈objectid〉
〈user〉::=〈userid〉|〈userset〉
〈userset〉::=〈object〉‘#’〈relation〉

This is the notation you can see in the Keto examples on the internet. For now, I will ignore the namespace part and use a single namespace for the example further down.

In the relation tuple, the user and relation are pretty obvious. The object part leaves room for improvisation. A little bit higher up, we have discussed that RBAC assigns permissions to operations instead of objects. Before moving on, we need to establish a couple of facts for the purpose of this article:

  • the Zanzibar object implies an RBAC operation,
  • the Zanzibar subject is the RBAC object,
  • the relation defines the permission.

§so, RBAC with ACL?

I hear you say. But why not. Zanzibar is the outcome of Google’s work on the global access control system powering things like Calendar, Cloud, Drive and so on.

Their Cloud products alone contain dozens of sub-products, each defining own roles. Each of those roles carries object permissions (general service actions or individual object actions) the user can be granted.

§how to proceed

From the previous theory, two statements are the strongest signals of how to move forward:

  1. RBAC differs from ACL in that RBAC assigns permissions to operations instead of objects.
  2. An action can be performed on an object by a subject holding specific roles.

The first one is a constraint. Actions, say granular read or write, should not be assigned to individual objects.

This makes sense because if a certain action should no longer be allowed for a certain role, there should be no need to iterate over every object to remove a permission. Instead, a permission should be revoked from a role and the users holding these roles should no longer be allowed to perform the revoked action.

The second statement is more of a clue. At the time of the decision making, two criteria are known:

  • the object on which the action is to be performed
  • the user (subject) for which the decision has to be reached

It is safe to assume that knowing the user implies knowing the roles the user belongs to. The second statement stems from the first one and shows that asking is the user allowed to execute this action is the wrong thing to do.

The correct question to ask is: can an action be performed on this object by someone holding these roles.

§the scenario

To visualize a PoC RBAC with Keto, let’s consider the following imaginary scenario.

  1. There is a SaaS business offering compute services.
  2. Compute resources can be created, viewed and deleted.
  3. The Company is the client of SaaS and has a compute environment called production.
  4. The IT director, Hermes, can create, view and delete compute resources.
  5. The Company employs two development directors: Fry and Bender. Both of them can only view the resources in production.
  6. As the time passes, the Company asks Fry to speed up the deployment to production and gives him the permissions to create and delete resources in production.

The SaaS and the Company will not be included in the implementation, they’re irrelevant to this example.

§implementation

Let’s start by launching the Ory stack locally:

1
2
3
git clone https://github.com/radekg/ory-reference-compose.git
cd ory-reference-compose/compose
docker-compose -f compose.yml up psql keto-migrate keto

Keto read API runs on localhost:4466 and the write API is reachable via localhost:4467. The compose stack creates a single namespace called default-namespace.

Let’s start with defining the production platform by creating the following relation tuples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "production",
    "relation": "create",
    "subject": "default-namespace:production-creator#member"
}' http://localhost:4467/relation-tuples
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "production",
    "relation": "view",
    "subject": "default-namespace:production-viewer#member"
}' http://localhost:4467/relation-tuples
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "production",
    "relation": "delete",
    "subject": "default-namespace:production-deleter#member"
}' http://localhost:4467/relation-tuples

In the Zanzibar notation, these are:

  • production#create@(default-namespace:production-creator#member): roughly translates to: production allows create by anybody bound as a member of production-creator
  • production#view@(default-namespace:production-viewer#member): production allows view by anybody bound as a member of production-viewer
  • production#delete@(default-namespace:production-deleter#member): production allows delete by anybody bound as a member of production-deleter

These relations can be read as properties (or capabilities) of the production platform. The production-[creator|viewer|deleter] is a handle for a referencing object, these referencing objects, called production-[creator|viewer|deleter] respectively, can be seen as the bindings.

Let’s bind the IT director first. The following relation tuples together define a role called it-director.

production-creator#member@default-namespace:it-director#member
production-viewer#member@default-namespace:it-director#member
production-deleter#member@default-namespace:it-director#member

What we are saying here is that any person who is a member of the it-director role will be treated as a member of the respective production-X role, which we have already bound to respective create, view and delete.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "production-creator",
    "relation": "member",
    "subject": "default-namespace:it-director#member"
}' http://localhost:4467/relation-tuples
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "production-viewer",
    "relation": "member",
    "subject": "default-namespace:it-director#member"
}' http://localhost:4467/relation-tuples
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "production-deleter",
    "relation": "member",
    "subject": "default-namespace:it-director#member"
}' http://localhost:4467/relation-tuples

If there was a higher level system responsible for actual role management, the tool could present a role object called IT Director with these granular tuples hidden from view. The role management operator would be working with that abstraction instead of these granular items.

Next, we can bind the development director. Originally, this role should only be allowed the view of the production resources:

production-viewer#member@default-namespace:dev-director#member
1
2
3
4
5
6
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "production-viewer",
    "relation": "member",
    "subject": "default-namespace:dev-director#member"
}' http://localhost:4467/relation-tuples

Let’s put the people in their actual positions:

it-director#member@Hermes
dev-director#member@Fry
dev-director#member@Bender
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "it-director",
    "relation": "member",
    "subject": "Hermes"
}' http://localhost:4467/relation-tuples
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "dev-director",
    "relation": "member",
    "subject": "Fry"
}' http://localhost:4467/relation-tuples
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "dev-director",
    "relation": "member",
    "subject": "Bender"
}' http://localhost:4467/relation-tuples

We can now ask Keto if the respective people can perform their tasks, for example, can Hermes create resources in production?

1
2
3
4
5
6
curl -XPOST --data '{
    "namespace": "default-namespace",
    "object": "production",
    "relation": "create",
    "subject": "Hermes"
}' http://localhost:4466/check
1
{"allowed":true}

What about Fry?

1
2
3
4
5
6
curl -XPOST --data '{
    "namespace": "default-namespace",
    "object": "production",
    "relation": "delete",
    "subject": "Fry"
}' http://localhost:4466/check
1
{"allowed":false}

But, both him and Bender, should be able to view:

1
2
3
4
5
6
curl -XPOST --data '{
    "namespace": "default-namespace",
    "object": "production",
    "relation": "view",
    "subject": "Bender"
}' http://localhost:4466/check
1
{"allowed":true}

So far, so good.

§Fry’s opportunity

The Company has finally decided to give Fry the opportunity and improve the production delivery speed.

A new role has been carved out just for Fry:

1
2
3
4
5
6
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "fast-dev-director",
    "relation": "member",
    "subject": "Fry"
}' http://localhost:4467/relation-tuples

He can’t yet create or delete:

1
2
3
4
5
6
curl -XPOST --data '{
    "namespace": "default-namespace",
    "object": "production",
    "relation": "create",
    "subject": "Fry"
}' http://localhost:4466/check
1
{"allowed":false}

For that, the fast-dev-director#member must be explicitly allowed by the respective production-X role.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "production-creator",
    "relation": "member",
    "subject": "default-namespace:fast-dev-director#member"
}' http://localhost:4467/relation-tuples
curl -XPUT --data '{
    "namespace": "default-namespace",
    "object": "production-deleter",
    "relation": "member",
    "subject": "default-namespace:fast-dev-director#member"
}' http://localhost:4467/relation-tuples

Can he now create and delete?

1
2
3
4
5
6
curl -XPOST --data '{
    "namespace": "default-namespace",
    "object": "production",
    "relation": "create",
    "subject": "Fry"
}' http://localhost:4466/check
1
{"allowed":true}
1
2
3
4
5
6
curl -XPOST --data '{
    "namespace": "default-namespace",
    "object": "production",
    "relation": "delete",
    "subject": "Fry"
}' http://localhost:4466/check
1
{"allowed":true}

He should still be able to view through the dev-director role:

1
2
3
4
5
6
curl -XPOST --data '{
    "namespace": "default-namespace",
    "object": "production",
    "relation": "view",
    "subject": "Fry"
}' http://localhost:4466/check
1
{"allowed":true}

He can, indeed. What about Bender?

1
2
3
4
5
6
curl -XPOST --data '{
    "namespace": "default-namespace",
    "object": "production",
    "relation": "delete",
    "subject": "Bender"
}' http://localhost:4466/check
1
{"allowed":false}

This works as expected.

§Fry has fried the production

As Fry isn’t very smart, he did fry the production system and the Company was forced to remove his access. It was done like this:

1
curl --silent -X DELETE 'http://localhost:4467/relation-tuples?namespace=default-namespace&object=fast-dev-director&relation=member&subject=Fry'

And Fry could no longer create and delete in production.

Alternatively, the Company should have been able to remove the role binding, like this:

1
2
curl --silent -X DELETE 'http://localhost:4467/relation-tuples?namespace=default-namespace&object=production-creator&relation=member&subject=default-namespace:fast-dev-director#member'
curl --silent -X DELETE 'http://localhost:4467/relation-tuples?namespace=default-namespace&object=production-deleter&relation=member&subject=default-namespace:fast-dev-director#member'

Which would leave Fry the fast-dev-director role assignment but the missing binding would no longer allow him to create and delete resources. However, these two delete statements do not seem to be removing the actual binding.

§summary

How does this example compare to the RBAC theory from the beginning of the article?

  1. Role assignment: A subject can exercise a permission only if the subject has selected or been assigned a role: check, Fry had to be explicitly assigned using the member relation to the fast-dev-director role.
  2. Role authorization: A subject’s active role must be authorized for the subject. With rule 1 above, this rule ensures that users can take on only roles for which they are authorized: check, respective production-X explicitly allows fast-dev-director#member
  3. Permission authorization: A subject can exercise a permission only if the permission is authorized for the subject’s active role. With rules 1 and 2, this rule ensures that users can exercise only permissions for which they are authorized: check, we have verified that even though Fry was given the fast-dev-director role, he could not create nor delete before the relevant production-X binding was established; the non-functional curl -XDELETE throws a spanner in the works but it’s unrelated to the fact that Keto does provide this capability and the core issue can be fixed

§wrapping up

This simple example shows that a simple RBAC is doable with Ory Keto. Without the rewrites, the configuration can be quite verbose but the example shows that different relation tuples can be chained together to form a more complex decision tree.

The example with deletes not removing the expected tuples indicate that Keto might still not be totally bulletproof but things can only get better.

Working directly with tuples can probably lead to a big headache. An efficient way of working would definitely require a tool managing and verifying the relation tuples based on some higher level role and membership abstraction.