Code Post: Building Relationship-Based Access Control in Ruby on Rails

Y'all have seen me do personal posts and cinema posts. Now it's time for the code posts. If you are here for me ranting about cinema or whining about my life and don't want to read about me nerding out to code I promise not to take it personal. I'm also assuming a target audience of this blog of someone who has familiarity with database architecture. I'll still be back next week and I promise I'll do code posts at most once a month.

So! Relationship-Based Access Control! Also known as "Fine Grain Access Control" it stands in contrast to "Role-Based Access Control" which, if you have familiarity with authorization systems, you probably have heard of, or at least know about. It's management of access to areas and data based off of the role a user has: like an admin could go anywhere but someone with a "Financial Manager" role wouldn't be able to edit permissions of another user. If you're familiar with Role-Based Access Control (RBAC), you may also be familiar of how quickly it gets away from you. Before you know it there are 20 different roles for very specific things and a user has to have at least five roles to do anything.

Enter Relationship-Based Access Control (ReBAC). It was actually designed by Google with Project Zanzibar. That has influenced contemporary Google authorization. You are probably familiar with it if you've interacted with Google Docs. You create a document. You can then give permissions to others with a specific scope of "they are an owner of this doc," "they can edit this doc," "they can only view this doc."

With ReBAC, the intent is to take the relationship a user has with an object and add a scope to it. ReBAC also takes it a step further and allows users and objects to inherit relationships with "User Sets" and "Object Domains." A User in a User Set that can oversee the Object Domain of Blogs as "owners" immediately has a relationship with every new blog post created.

One of the important elements of scopes is that they inherit all the permissions of scopes lower on their hierarchy. In a traditional system there are three scopes: viewers, editors, and owners.
  • Viewers have read access to an object
  • Editors have edit access to an object and all lower scope permissions
  • Owners have delete access to an object and all lower scope permissions
There's nothing sacred to these scopes though. If you need a specific permission that has different powers - say an author scope that a User has with a Blog Post - there's nothing stopping you from making that.

Now let's take a look at actual implementation. For this I built a polymorphic many-to-many join table that also stores the scope. It looks kinda like this
It is a rectangle with rounded edges that reads "RebacPermissions" representing the title of the table. Underneath it are the fields: id, permission_owner_id, permission_owner_type, permission_object_id, permission_object_type, permission_scope
With it, we track the relationship a polymorphic Owner has with a polymorphic Object, and the weight of that relationship is tracked with the scope. In this situation an Owner could be a User or a User Set; an Object could be a Blog Post or the Blog Object Domain; and the scope could be viewer, editor, owner, or even author.

I then created a method within both the User and User Set models called has_relationship_with? that takes an Object as a variable. It would either return the string of the scope or nil, intending no relationship exists.

In the case of the user, the method might look like this

  def has_relationship_with?(object)
    current_permission = nil 

    relationship = RebacPermission.find_by(permission_owner: self, permission_object: object)

    current_permission = relationship&.permission_scope

    user_sets.each do |set|
      relationship = set.has_relationship_with? object

      current_permission = RebacPermission.compare_scopes(relationship, current_permission)
    end 

    unless object.is_a? DashObject
      relationship = RebacPermission.find_by(permission_owner: self, permission_object: object.dash_object)

      current_permission = RebacPermission.compare_scopes(relationship&.permission_scope, current_permission)
    end 

    current_permission
  end

Likewise, in the User Set it might look like this

  def has_relationship_with?(object)

    relationship = RebacPermission.find_by(permission_owner: self, permission_object: object)


    current_permission = relationship&.permission_scope


    unless object.is_a? DashObject

      relationship = RebacPermission.find_by(permission_owner: self, permission_object: object.dash_object)


      current_permission = RebacPermission.compare_scopes(current_permission, relationship&.permission_scope)

    end 


    current_permission

  end 

 

You can see the User object references this User Set and both reference a class method in RebacPermission of compare_scopes which simply returns which scope has priority in the hierarchy.

With this system, it's still on you to build systems around these permission checks, but limiting all checks around one simple call of has_relationship_with? has been pretty darn useful.

Thanks for reading! If there's interest in more, I can show how I specifically set up the relationships in the Rails models. 

Comments

Popular posts from this blog

Disney's Cars and Whether What You Do Defines Who You Are

Well... is it CANON???

Back From the Rot