How-To Guides
Build Role-Based Access Control (RBAC)

Build Role-Based Access Control (RBAC)

Role-based access control (RBAC) is so ubiquitous that Oso provides syntax for modeling RBAC. This syntax makes it easy to create a role-based authorization policy with roles and permissions -- for example, declaring that the "reader" role on a repository allows a user to "read" the details about that repository. In this guide, we'll walk through building a simple RBAC policy for GitCloud, a social coding platform with a microservices architecture.

Declare application types as actors and resources

Oso makes authorization decisions by determining if an actor can perform an action on a resource:

  • Actor: who is performing the action? User:Ariana
  • Action: what are they trying to do? "read"
  • Resource: what are they doing it to? Repo:Acme

Oso makes these decisions using a combination of authorization rules (a policy) and authorization data (called facts). For now, we'll focus on writing a policy.

The first step of building an RBAC policy is telling Oso which application types are actors and which are resources. In GitCloud, the most important resource type that requires access control is Repo , representing a code repository. Declare this as a resource in your policy file:

resource Repo {}

GitCloud also has a User type, which is an actor. Declare that, too:

actor User {}

This piece of syntax is called a resource block, and it performs two functions: it identifies the type as an actor or a resource, and it provides a centralized place to declare roles and permissions for that particular type.

Declare roles and permissions

In GitCloud, users can perform actions such as reading basic information about a repository or scheduling actions like deploys via the GitCloud Actions microservice. Users can also be assigned roles, such as the "maintainer" or "reader" role for a particular Repo.

Inside the curly braces of your resource block, declare the roles and permissions for that resource:

resource Repo {
  roles = ["maintainer", "reader"];
  permissions = [
    "read",
    "schedule_action",
  ];
}

Even though reading a repository and scheduling a GitCloud Action are handled by different microservices, you'll declare both of these permissions in one central policy.

Grant permissions to roles

Next, you'll write shorthand rules that grant permissions to roles. For example, if you grant the "schedule_action" permission to the "maintainer" role in the Repo resource block, then a user who's been assigned the "maintainer" role for a particular repository can schedule GitCloud Actions for that repository. Here's the Repo resource block with a few shorthand rules added:

resource Repo {
  roles = ["maintainer", "reader"];
  permissions = [
    "read",
    "schedule_action",
  ];
 
  # An actor has the "schedule_action" permission if they have the "maintainer" role.
  "schedule_action" if "maintainer";
   # An actor has the "read" permission if they have the "maintainer" role.
  "read" if "maintainer";
  # An actor has the "read" permission if they have the "reader" role.
  "read" if "reader";
}

Shorthand rules expand to regular Polar rules when a policy is loaded. The "read" if "maintainer" shorthand rule above expands to:

has_permission(actor: Actor, "read", repo: Repo) if
  has_role(actor, "maintainer", repo);

Instances of the User type will match the Actor specializer because of the actor User {} resource block declaration.

Grant roles to other roles

All of the shorthand rules you've written so far have been in the <permission> if <role> form, but you can also write <role1> if <role2> rules. This type of rule is great for situations where you want to express that <role2> should be granted every permission you've granted to <role1>.

In the previous snippet, the permissions granted to the "maintainer" role are a superset of those granted to the "reader" role. If you replace the existing "read" if "maintainer" rule with "reader" if "maintainer", the "maintainer" role still grants the "read" permission:

resource Repo {
  roles = ["maintainer", "reader"];
  permissions = [
    "read",
    "schedule_action",
  ];
 
  # An actor has the "schedule_action" permission if they have the "maintainer" role.
  "schedule_action" if "maintainer";
  # An actor has the "read" permission if they have the "reader" role.
  "read" if "reader";
 
  # An actor has the "reader" role if they have the "maintainer" role.
  "reader" if "maintainer";
}

In addition, any permissions you grant the "reader" role in the future will automatically propagate to the "maintainer" role.

Assign roles in the application

You've written a policy describing the actors, resources, roles, and permissions in GitCloud. Your policy is ready to go! At this point you'll want to upload it to Oso-- read the Oso quickstart for more details.

Your policy describes the logical relationship between roles and permissions for users and repositories in general, but you still need to tell Oso data about specific users and their roles on specific repositories. These pieces of concrete authorization data are called facts.

To tell Oso the fact that User:Ariana has the role "reader" on Repo:Acme, use the tell command in the Oso SDK:

CLINodePythonGoRuby
oso-cloud tell has_role User:Ariana reader Repo:Acme

Typically, you'll want to tell Oso facts programmatically. For instance, when a user creates a new repository in GitCloud, you might want to assign them the "maintainer" role on that repository. To accomplish that, you'd add the tell SDK call to the service that handles repository creation.

Check permissions

With your policy and the relevant role assignment facts in place, you can start checking permissions. Use the authorize command in the Oso SDK:

CLINodePythonGoRuby
oso-cloud authorize User:Ariana read Repo:Acme

authorize returns a boolean authorization decision. In this case, your policy specifies that any User with the "reader" role on a Repo can read it, and you assigned User:Ariana the "reader" role on Repo:Acme, so this authorize command returns true. Ariana is authorized to read Acme.

Note that any service in your application can check permissions without having to know anything about roles. All the logic is neatly encapsulated in your policy.

Baby Got RBAC

Your complete policy looks like this:

actor User {}
 
resource Repo {
  roles = ["maintainer", "reader"];
  permissions = [
    "read",
    "schedule_action",
  ];
 
  # An actor has the "schedule_action" permission if they have the "maintainer" role.
  "schedule_action" if "maintainer";
  # An actor has the "read" permission if they have the "reader" role.
  "read" if "reader";
 
  # An actor has the "reader" role if they have the "maintainer" role.
  "reader" if "maintainer";
}
 

Happy authorizing!

If you'd like to play around with a more fully-featured version of this policy and application, check out the GitCloud repository on GitHub.

Oso does much more than RBAC-- it's powerful and flexible enough to handle ReBAC, ABAC, and beyond. To see how to model complex authorization requirements with Oso, take a look at our guide to authorization modeling.