*entication: Who is this person?
*orization: What can this person do?
There are a plethora of hosted authentication services out there (Auth0, AWS Cognito, FusionAuth, to name a few). These services are very capable of answering question 1. Depending on which one you use and how you use it, it’s likely that it can also achieve a great deal of other things.
Not only do these “identity providers” (IdPs) deal with identity - they can also deal with how these identities fit into your organisation. This is often achieved, in the simplest scenarios, by adding identities to groups (e.g. Auth0, AWS Cognito). In more complex scenarios, it might be achieved via integrations with LDAP, offering a hierarchy of identities (think a tree structure with parents inheriting the permissions of its children nodes).
Cerbos doesn’t know what design decisions users may have made when building their organisational structures. Perhaps each defined role in the IdP is independent? Perhaps they intersect (logical or
)? Perhaps they union (logical and
)? Perhaps there are implied hierarchies? Perhaps there are explicit ones?
In the spirit of simplicity and clean programmatic boundaries, Cerbos made the decision to offload this responsibility to the IdPs entirely, and maintain a completely flat and flexible role structure.
To the end user, the cost of this decision could be the need to replicate policy rules between roles which may have a predefined relationship within the IdP. However, the benefit is that they have no knowledge requirement of any of those relationships that are defined within the IdP. When developing an application, the IdP and all of its complexity can remain entirely obscured, meaning a much simpler development experience.
That said, perhaps you have no predefined relationships in your IdP and you would like to implement role relationships into your application. Let’s take a look at a few potential approaches:
This is the simplest implementation. Each role type has all of its access rules attached directly to it. The cost is duplicating rule logic between roles (more dev time). The benefit is that you only ever need to pass the specific roles when executing a query against Cerbos.
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: default
resource: batmobile
rules:
- actions: ["drive", "wash"] # we duplicate `wash` here
effect: EFFECT_ALLOW
roles:
- batman
- actions: ["wash"]
effect: EFFECT_ALLOW
roles:
- batcave_assistant
from cerbos.sdk.model import Principal, Resource
from cerbos.sdk.client import CerbosClient
p = Principal(
user_id,
roles=["batman"],
)
r = Resource(
resource_id,
kind="batmobile",
)
with CerbosClient("https://localhost:3592") as c:
is_allowed = c.is_allowed("wash", p, r)
It’s worth acknowledging that the policy rules from each role the principal is a member of are union
'd when resolving against a resource (e.g. all rules from role A and
all from role B). If you look at the example above, the batman
role wouldn’t strictly need to have the wash
action if everybody who had the batman
role also, by default, had the batcave_assistant
role. However, by keeping the repetition and applying all access rules to all roles, we’re ensuring a clean boundary between the application and the IdP.
Cerbos provides the concept of derived roles. These enable you to build much more complex relationships via a combination of Cerbos’ powerful attribute mechanism and the IdP provided identities. Theoretically speaking; given that you can group the IdP roles, you can effectively model a single level of inheritance whereby the derived role becomes the “parent” of all listed IdP roles.
Food for thought: Cerbos’ attribute system is completely freeform, and very powerful - any arbitrary attribute can be used. Dependent on your needs, perhaps your problem can be solved without any complex application-defined relationships and just through the use of derived roles.
The following example shows how you might model a multi-tenant system where principals can belong to multiple "teams", with different roles for each:
The batcave has multiple batmobiles on rotation, with a team responsible for each. To ensure a high quality of maintenance, Batman insists that each team can only be responsible for carrying out work on their own batmobile, and that it is up to other teams to inspect and sign off on them (thus preventing any shoddy work).
To model this, we maintain admin
and user
as regular IdP roles, and then derive the following additional roles from the user
(admin
maintains it's super-privileges):
apiVersion: api.cerbos.dev/v1
derivedRoles:
name: team_roles
definitions:
- name: mechanic
parentRoles:
- user
condition:
match:
expr: R.attr.teamId in P.attr.teams.filter(x, P.attr.teams[x].role == "mechanic")
- name: inspector
parentRoles:
- user
condition:
match:
expr: R.attr.teamId in P.attr.teams.filter(x, P.attr.teams[x].role == "inspector")
We then import these, and use them in specific resource policies, like so:
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: default
resource: batmobile
importDerivedRoles:
- team_roles
rules:
- actions:
- "*"
effect: EFFECT_ALLOW
roles:
- admin
- actions:
- "drive:slowly"
- "oil_change"
effect: EFFECT_ALLOW
derivedRoles:
- mechanic
- actions: ["inspect"]
effect: EFFECT_ALLOW
derivedRoles:
- inspector
Now we can determine access based on what role the principal has in the given team. In this case, we're seeing what actions Alfred can carry out on batmobiles belonging to team1
and team2
:
roles = {"user"} # retrieved from the IdP
p = Principal(
"albert",
roles=roles,
attr={
"teams": {
"team1": {
"role": "mechanic",
},
"team2": {
"role": "inspector",
},
},
},
)
actions = ["drive:*", "inspect", "drive:slowly", "oil_change"]
resource_list = ResourceList(
resources=[
ResourceAction(
Resource(
"bat1",
kind="batmobile",
attr={
"teamId": "team1",
},
),
actions=actions,
),
ResourceAction(
Resource(
"bat2",
kind="batmobile",
attr={
"teamId": "team2",
},
),
actions=actions,
),
]
)
with CerbosClient(host="http://localhost:3592") as c:
resp = c.check_resources(principal=p, resources=resource_list)
resp
is as follows:
{
"results": [
{
"resource": {
"id": "bat1",
"kind": "batmobile",
"attr": {},
"policy_version": "default",
"scope": ""
},
"actions": {
"drive:*": "EFFECT_DENY",
"drive:slowly": "EFFECT_ALLOW",
"inspect": "EFFECT_DENY",
"oil_change": "EFFECT_ALLOW"
},
"validation_errors": null
},
{
"resource": {
"id": "bat2",
"kind": "batmobile",
"attr": {},
"policy_version": "default",
"scope": ""
},
"actions": {
"drive:*": "EFFECT_DENY",
"drive:slowly": "EFFECT_DENY",
"inspect": "EFFECT_ALLOW",
"oil_change": "EFFECT_DENY"
},
"validation_errors": null
}
],
"status_code": 200,
"status_msg": null
}
You can see how, given his different roles in each team, Alfred's permissions also change.
Perhaps you don’t want principals to carry multiple roles. Perhaps you want them to be associated with a single role, e.g. their job title, or their position within an organisation.
In this approach, we model an organisation as a tree structure. The root of the tree represents full organisation ownership. Each child node represents a department, sub-department, team, etc; in descending order. By default, each node will automatically receive all of the permissions of its children. When you want to obtain rules for a given role, you simply carry out a sub-tree traversal, taking a union of all of the policies of each of the nodes’ children.
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: default
resource: batmobile
rules:
- actions: ["drive:*"]
effect: EFFECT_ALLOW
roles:
- batman
- actions: ["inspect"]
effect: EFFECT_ALLOW
roles:
- butler
- actions: ["drive:slowly"]
effect: EFFECT_ALLOW
roles:
- batcave_mechanic
- actions: ["oil_change"]
effect: EFFECT_ALLOW
roles:
- batcave_jr_mechanic
- actions: ["wash"]
effect: EFFECT_ALLOW
roles:
- batcave_valet
A tree structure can be modelled in many ways. For simplicity, in the below example, I’m modelling it as a map based structure, with string
keys, and set[string]
values. The key represents a node, and its corresponding set represents its children.
# batman (root)
# └── butler
# ├── batcave_valet
# └── batcave_mechanic
# └── batcave_jr_mechanic
tree: dict[str, set] = {
"batman": {"butler"},
"butler": {"batcave_mechanic", "batcave_valet"},
"batcave_mechanic": {"batcave_jr_mechanic"},
"batcave_jr_mechanic": {},
"batcave_valet": {},
}
def get_all_children_roles(role: str) -> set:
roles = set()
rem = [role]
while len(rem):
cur, *rem = rem
roles.add(cur)
if (children := tree.get(cur)) is not None:
rem.extend(children)
return roles
role = "butler" # retrieved from the IdP
p = Principal(
"alfred",
roles=get_all_children_roles(role),
)
resource_list = ResourceList(
resources=[
ResourceAction(
Resource(
"bat1",
kind="batmobile",
),
actions=["drive:*", "inspect", "drive:slowly", "oil_change", "wash"],
),
]
)
with CerbosClient(host="http://localhost:3592") as c:
resp = c.check_resources(principal=p, resources=resource_list)
The butler
role can do everything, except for driving like a lunatic - the above returns the following (truncated for clarity):
{
"results": [
{
"actions": {
"drive:*": "EFFECT_DENY",
"drive:slowly": "EFFECT_ALLOW",
"inspect": "EFFECT_ALLOW",
"oil_change": "EFFECT_ALLOW",
"wash": "EFFECT_ALLOW"
},
}
],
}
A batcave_mechanic
can only drive:slowly
or carry out an oil_change
:
{
"results": [
{
"actions": {
"drive:*": "EFFECT_DENY",
"drive:slowly": "EFFECT_ALLOW",
"inspect": "EFFECT_DENY",
"oil_change": "EFFECT_ALLOW",
"wash": "EFFECT_DENY"
},
}
],
}
You can see how a call to the get_all_children_roles
method with the single role returns a set of all children roles, and therefore all of the corresponding access rules.
Needless to say, batman
can do anything.
Cerbos has been built to be clean and unopinionated, while still offering the capability to model any system you can throw at it. If you're just playing around and don't know where to start, then check out the examples on the playground to get a taste of what it can do.
As always, if you have any questions or feedback on this topic or anything else Cerbos, join our Slack community and we'll be keen to chat! Thanks for reading!
Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team