We’ve all interacted with applications and products that have a role editor that’s spiritually similar to the one above. As an administrator of the app, you get to define the names of the roles, and the capabilities that they imply. This allows you to set up the roles to match the roles at your company and federate out permissions to match. Often these applications will come with a default set of roles and permissions that match a pre-canned workflow, but allow you to change the workflow to fit your organization.
JIRA, for example, comes with a built-in role called admin
, and lets you define roles for the various people who will be contributing to this particular productivity app.
The example above was inspired by a far simpler version of an issue tracking tool which only has issues and comments on those issues.
In today’s post we will work through how to model this application in Authzed’s schema language, how to integrate it with your application, and how to direct that integration’s reads and writes to both an instance of our open-source implementation SpiceDB or to our hosted service Authzed.
Let’s get started by modeling the schema in the Authzed Playground!
Modeling the Schema
Modeling the User
The first thing we usually do when modeling any application in Authzed is create an object definition for users themselves:
definition user {}
This allows us to refer to users as subjects in test relationships with the following syntax:
user:claudia
user:really-long-user-id-maybe-a-uuid
Modeling Capabilities
Before we can start assigning capabilities to roles, we must first enumerate them! In Authzed we think about capabilities in terms of permissions. As a reminder, permissions are the external API of your permissions system, and they are the integration point with your application at enforcement points. For example, before we allow someone to create an issue within a project we probably want to check that the user has permission.
In pseudocode:
if authzed.check(resource="project:oursoftware", permission="create_issue", subject="user:claudia"):
# let "claudia" create the issue on the "oursoftware" project
create_issue()
For our example, the schema would be the following:
definition user {}
definition project {
permission create_issue = todo
}
definition issue {
permission assign = todo
permission resolve = todo
permission create_comment = todo
}
definition comment {
permission delete = todo
}
For now I have used the placeholder todo
for the actual computation of those permissions.
You can see from our original example that these permissions closely follow the capabilities defined at the top of our control panel.
Modeling Roles
Since this is an article about user defined roles, we probably want to also model out the roles themselves.
In order to keep permissions flexible in Authzed, we separate out permissions from relations.
Relations define how objects can relate to other objects.
Permissions are how we interpret relations to make access control decisions.
Our role
object will relate to users that have the role: member
and to a project
for which the role is defined.
definition role {
relation project: project
relation member: user
}
That’s it!
Now that we have roles, we can start filling in the relations and definitions on our project
, issue
, and comment
objects.
Adding Relations to Bind Roles to Capabilities
As mentioned earlier, we separate out permissions and relations in Authzed in order to keep the permissions computation flexible. The first thing we will do is add one relation for each grantable capability, and set the allowable type to users who are the members of a role:
definition project {
relation issue_creator: role#member
relation issue_assigner: role#member
relation any_issue_resolver: role#member
relation assigned_issue_resolver: role#member
relation comment_creator: role#member
relation comment_deleter: role#member
permission create_issue = todo
}
This will allow us to grant a capability to a role with the following pseudocode:
# Grant the "issue_creator" capability to those who have been granted the "project_manager" role
authzed.write(resource="project:oursoftware", relation="issue_creator", subject="role:project_manager#member")
Now that we have the proper relations in place, we can compute all of the permissions we left blank earlier!
Computing Permissions from Relations
Permissions such as create_issue
which are both granted and evaluated at the organization level, are very easy to compute:
definition project {
relation issue_creator: role#member
relation issue_assigner: role#member
relation any_issue_resolver: role#member
relation assigned_issue_resolver: role#member
relation comment_creator: role#member
relation comment_deleter: role#member
permission create_issue = issue_creator
}
This permission, in short, says: "those who are members of any role which has an issue_creator
relationship on this project, shall have the create_issue
permission."
The rest of our permissions are evaluated on issue
and comment
, but the relationships are on the parent project object.
How can we calculate these permissions?
Authzed schema provides an arrow ->
operator which allows you to traverse through relationships matching a relation on an object, and then evaluate a relation on that object.
For example, project->issue_assigner
, says "follow any relationships with the project
relation, and compute the issue_assigner
relation on those objects.
From here it is easy to see how to compute indirect permissions.
We also need to add a relationship between the issue and project:
definition issue {
relation project: project
permission assign = project->issue_assigner
permission resolve = ??? + project->any_issue_resolver
permission create_comment = project->comment_creator
}
We have a problem though: one of the capabilities that we can grant is assigned_issue_resolver
.
We don’t have any way of knowing if a particular user is assigned to the issue.
We can fix this with another relation, and now compute the resolve
using that relation:
definition issue {
relation project: project
relation assigned: user
permission assign = project->issue_assigner
permission resolve = (project->assigned_issue_resolver & assigned) + project->any_issue_resolver
permission create_comment = project->comment_creator
}
We use the intersection operation (&
) to make sure that the user is both the assigned user, and is a member of a role which grants them the assigned_issue_resolver
capability.
The last permission to fill in is the delete
permission on comments.
Similar to how issue
objects are related to project
objects, comment
objects are related to issue
objects.
We can fill in the permission as follows:
definition comment {
relation issue: issue
permission delete = issue->???
}
Oh no!
We found another problem.
Until support for nested arrows is finished, the ->
operator only lets us traverse one level and recompute a relation, but the capability for comment_deleter
is nested two levels away on the project!
We can solve this problem and keep the schema normalized by creating a synthetic permission on the issue
which will do the next step of the resolution.
The complete example is as follows:
definition issue {
relation project: project
relation assigned: user
permission assign = project->issue_assigner
permission resolve = (project->assigned_issue_resolver & assigned) + project->any_issue_resolver
permission create_comment = project->comment_creator
// synthetic relation
permission project_comment_deleter = project->comment_deleter
}
definition comment {
relation issue: issue
permission delete = issue->project_comment_deleter
}
The final capability that we haven’t modeled is the role_manager
capability, which will be used to determine who is allowed to make changes to roles.
Adding Role Meta-Permissions
We need to add some permissions to the role itself in order to allow roles to be created, bound to capabilities, and bound to users. However, we don’t want the built-in roles to be modifiable. We can do this all in our schema as well!
First we will add a role_manager
capability to our project
object:
definition project {
relation issue_creator: role#member
relation issue_assigner: role#member
relation any_issue_resolver: role#member
relation assigned_issue_resolver: role#member
relation comment_creator: role#member
relation comment_deleter: role#member
relation role_manager: role#member
permission create_issue = issue_creator
permission create_role = role_manager
}
Then we will add a built_in_role
relation on our role
object, and use that to disable certain permissions (delete
, add_permissionon
, and remove_permission
) on the roles which would allow them to be modified or removed:
definition role {
relation project: project
relation member: user
relation built_in_role: project
permission delete = project->role_manager - built_in_role->role_manager
permission add_user = project->role_manager
permission add_permission = project->role_manager - built_in_role->role_manager
permission remove_permission = project->role_manager - built_in_role->role_manager
}
By adding a built_in_role relationship with the project and using the exclusion operator, when any of the role modifying permissions are evaluated, the result will be an empty set of users since built in roles will have relationships for both project->role_manager and built_in_role->role_manager.
Putting It All Together
The following is the complete schema, which we can now start adding relationships to:
Integrating With an Application
We need to add a few things to an application before we can fully utilize the schema that we’ve developed. We need to:
- Bootstrap the permissions when we create a new project
- Write a relationship from an issue to a project whenever we create a new issue
- Write a relationship from a comment to an issue whenever we make a comment
- Write or delete a relationship whenever a role is modified to change the permission
- Protect all of the above with the proper permissions checks.
The following pseudocode blocks give a rough idea of how to do this for some of the operations in our application:
def create_project(creator_id, project_id):
authzed.write([
# Create the roles in the project
(resource: "role:{project_id}-admin", relation: "project", subject: "project:{project_id}"),
(resource: "role:{project_id}-developer", relation: "project", subject: "project:{project_id}"),
(resource: "role:{project_id}-user," relation: "project", subject: "project:{project_id}"),
(resource: "role:{project_id}-admin", relation: "built-in-role", subject: "project:{project_id}"),
(resource: "role:{project_id}-developer", relation: "built-in-role", subject: "project:{project_id}"),
(resource: "role:{project_id}-user", relation: "built-in-role", subject: "project:{project_id}"),
# Grant the permissions for the built-in roles
(resource: "project:{project_id}", relation: "issue_creator", subject: "role:{project_id}-admin"),
(resource: "project:{project_id}", relation: "issue_assigner", subject: "role:{project_id}-admin"),
(resource: "project:{project_id}", relation: "any_issue_resolver", subject: "role:{project_id}-admin"),
(resource: "project:{project_id}", relation: "assigned_issue_resolver", subject: "role:{project_id}-admin"),
(resource: "project:{project_id}", relation: "comment_creator", subject: "role:{project_id}-admin"),
(resource: "project:{project_id}", relation: "comment_deleter", subject: "role:{project_id}-admin"),
(resource: "project:{project_id}", relation: "role_manager", subject: "role:{project_id}-admin"),
(resource: "project:{project_id}", relation: "issue_creator", subject: "role:{project_id}-developer"),
(resource: "project:{project_id}", relation: "assigned_issue_resolver", subject: "role:{project_id}-developer"),
(resource: "project:{project_id}", relation: "comment_creator", subject: "role:{project_id}-developer"),
(resource: "project:{project_id}", relation: "issue_creator", subject: "role:{project_id}-user"),
(resource: "project:{project_id}", relation: "comment_creator", subject: "role:{project_id}-user"),
# Bind the project creator as an admin
(resource: "role:{project_id}-admin", relation: "member", subject: "user:{creator_id}"),
])
def create_issue(creator_id, project_id):
if authzed.check(resource: "project:{project_id}", permission: "create_issue", subject: "user:{creator_id}"):
issue_id = create_issue_in_database()
# Bind the issue to the project in authzed
authzed.write(resource: "issue:{issue_id}", relation: "project", subject: "project:{project_id}")
def assign_issue(assigner_id, assigned_to_id, issue_id):
if authzed.check(resource: "issue:{issue_id}", permission: "assign", subject: "user:{assigner_id}"):
# Assign the issue to the assigned_to
authzed.write(resource: "issue:{issue_id}", relation: "assigned", subject: "user:{assigned_to_id}")
def create_role(creator_id, project_id, role_name):
if authzed.check(resource: "project:{project_id}", permission: "create_role", subject: "user:{creator_id}"):
# Bind the role to the project in authzed
authzed.write(resource: "role:{project_id}-{role_name}", relation: "project", subject: "project:{project_id}")
def add_capability_to_role(creator_id, project_id, role_name, capability_name):
if authzed.check(resource: "role:{project_id}-{role_name}", permission: "add_permission", subject: "user:{creator_id}"):
# Bind the role to the capability in authzed
authzed.write(resource: "project:{project_id}", relation: capability_name, subject: "role:{project_id}-{role_name}")
def add_member_to_role(creator_id, project_id, role_name, user_to_grant):
if authzed.check(resource: "role:{project_id}-{role_name}", permission: "add_user", subject: "user:{creator_id}"):
# Bind the role to the capability in authzed
authzed.write(resource: "role:{project_id}-{role_name}", relation: "member", subject: "user:{user_to_grant}")
And so on.
Wrapping Up
I hope this has been a useful introduction to the theory and practice behind adding user defined roles to an application using Authzed. This powerful paradigm is often used to great effect in business-to-business and productivity apps, but you can even find similar permissions panels in consumer and social products. If you have any questions, get stuck adding user-defined roles to your own application, or just want to say hi, come join us on Discord.
Additional Reading
If you’re interested in learning more about Authorization and Google Zanzibar, we recommend reading the following posts: