Role-Based Access Control Using a Graph Database
We recently launched a new internal application to enable help center agents to view and edit help center content across all of our communication channels. It’s a knowledge base application, and what makes it unique from a development perspective is that it leverages the Azure Cosmos DB implementation of the Gremlin Graph API. One of the business requirements that we have is to limit access to resources in the graph based on a user’s role. We found very few articles on how to implement such a feature using a graph database and thought it helpful to share our approach.
First let’s take a look at the application!
The application is designed specifically for use by the help center teams which are comprised of a combination of content editors and help center agents who have access to the content. There are three main navigation points in the application: Workspaces, Channels, and the Article Hierarchy. A Workspace defines the space for a team or group. A Channel represents one of our communication channels for that space, and the Articles are listed as part of a general hierarchy for the Workspace. The Article hierarchy is static within the Workspace and the Channel selects what content to load for the article.
In the application, each user is granted view or edit rights to specific Workspaces and Channels based on their role. Before discussing the access approach, it is worthwhile to understand how the application is modeled.
The Gremlin Graph API is a property graph that defines objects as vertexes and associations as edges. Every vertex and edge may contain any number of properties which are key-value pairs.
For the knowledge base application, each Workspace is defined by a single Workspace vertex. There is a single vertex for each individual channel in the workspace, and every channel has an Article vertex that maps back to an article in the hierarchy. The Hierarchy vertex contains a property that stores the hierarchy structure for the articles in the workspace.
In typical role-based access models there is a separation between Groups and Roles, with Roles defining access and permissions, and Groups associating users with Roles. This application limits users to single Group memberships, and those groups effectively define the role. This simplification reflects how this system is used, and it also reduces some complexity in the queries.
For the knowledge base application, a Group defines which workspaces a user can access, and whether the user can view or edit articles in a channel.
In the example above, there are two groups that can access the channels in a workspace. However, only the “editor” group can edit the articles and update the hierarchy.
Since Gremlin is a property graph, we added a “view” and “edit” property on the Group_Workspace and Group_Channel edges. The wsKey and chKey properties are the special key identifiers for each workspace and channel. And to be clear, a group can have access to multiple workspaces and channels, which is why the keys are required on the edge for proper query traversals.
view : bool
edit : bool
wsKey : string
view : bool
edit : bool
wsKey : string
chKey : string
Let’s look at some of the Gremlin queries that we use to validate if a user has access to a resource, starting with workspaces.
g.V() step is the standard Gremlin syntax for starting the query from the global list of Vertexes. From there, we look at all of the user vertexes to find the user with the userName that we are validating. Azure Cosmos DB supports scaling through partitions, and the partition key is added as a required property of the vertex. Including the partition key in the query limits the lookup for the user vertex to the USER partition, instead of incurring a cross-partition query, if it were not included.
.coalesce() step returns the result of the first traversal if a result exists, otherwise it returns the result of the next traversal after the comma. For the knowledge base, if the user has the
isAdmin property as
true, then they can view and edit all Workspaces, Channels, and Articles, so the coalesce step is used here as a way to short-circuit the validation check for admins.
For non-admins, the traversal continues onto the Group vertex via the User_Group edge using the
.out('User_Group') step. Then, all outgoing edges with the Workspace_Permission label are retrieved
.outE('Workspace_Permission')and filtered to only include the edges that have view set to true and the workspace key that the user is interested in accessing.
The last part of this is query is actually quite subtle. The
.count() step does what you expect, it counts the number of edges or vertexes returned from the previous step. The
.is(gte(1)) does not return a Boolean though. It returns the results of the previous step that matches the evaluation. So if a user is an admin, and the count returns a 1, then the full result of this traversal will be 1. If it returns an empty result, then the User_Group traversal continues and returns a 1 or an empty result.
Below is the query for editing articles in a Channel. In the knowledge base application, if a user has edit rights in a channel, they can edit any article in that channel. It’s very similar to the query above, but it adds the channel key that maps to the channel.
Authorizing users in ASP.NET Web API
There is a well-established way of handling authorization in ASP.NET using the Authorize Attribute. However, we wanted something that required a little less ceremony, flowed well with the F# async workflows that we’re using, and can be easily maintained.
Let’s look at an endpoint that is used to view an article. The endpoint requires the workspace, channel, and article id in the route which is passed on to the function that retrieves the article content. The function to read the article is part of an async computation that is passed as a parameter to the hasPermission function through partial application. The function is then queued onto the Task Threadpool.
scope settings allows us to define the permissions that we want to set on each resource, either at a course level, or fine-grained level.
module Role =
type WorkspaceKey = string
type ChannelKey = string
type Permission = View | Edit | Admin
type Scope =
| User of string
| Workspace of WorkspaceKey
| Channel of WorkspaceKey * ChannelKey
| AnyChannel of ChannelKey
Based on the role and scope, the hasPermission function validates the user’s access to a requested resource and executes the endpoint’s computation, or raises a 403 Forbidden response.
Peeking into the isAuthorized request, shows us how the Gremlin query is executed. For endpoints that require admin permissions, there is an admin query that checks if the user has the isAdmin property set. For all other permissions, the isAuthorizedScope will handle the validation.
The isAuthorizedScope executes the authorization query for the user who is trying to access the resource described in the scope with the given permission.
We’re using a helper function, getFirstAsync<int>, to get the first result from the query because Gremlin responses return a list of GraphSON items. The validateIsAuthorized function then checks whether the response was empty or if the user is granted access.
The knowledge base application is still fairly new and we are continuously adding new features to it. One of the next enhancements to the access model will enable content editors to mark an article as private and set which groups can access the article. It will build on the patterns described in this article, which we expect to evolve over time.
Here are a few of the resources that helped us along the way with our approach, and we hope that this article adds some further inspiration for role-based access control in a graph.
If you like the challenges of building complex & reliable systems and are interested in solving complex problems, check out our job openings.
The content and information in this blog post is the property of Jet, and cannot be copied without Jet’s express written consent. This content and information is provided for informational purposes on an “as is” basis at your sole risk. Jet makes no guarantee as to the accurateness, completeness or quality of the information, or its suitability to your specific purpose. Jet shall not be liable or responsible for any errors, omissions or inaccuracies in the information or your reliance on the information. You are solely responsible for verifying the information as being appropriate for your personal use.