Oso is a batteries-included library for adding authorization to your application. It lets you write policies that define who can do what in your app.
When managing access to resources in an application, it can be useful to group permissions into roles, and assign these roles to users. This is known as Role-Based Access Control (RBAC). This lets you express your authorization logic as two pieces:
- The policy, which grants permissions to particular roles. This is where you express rules like "members can create repositories" or "owners can update role assignments".
- Role assignments, which are determined by your application data. Often this is expressed as a roles table in your database, but you can also assign roles using relationships that already exist in your data.
Oso provides a configuration-based approach to adding role-based access control to your application. An Oso policy lets you configure roles for your models using the Polar language, a language specifically designed for building authorization:
resource Organization {
# Define ALL actions that can be taken on organizations
permissions = ["read", "create_repo", "manage_role_assignments"];
# Define organization roles that users can have
roles = ["member", "owner"];
# Member permissions
"read" if "member";
"create_repo" if "member";
# Owners can manage role assignments
"manage_role_assignments" if "owner";
# The owner role implies the member role
"member" if "owner";
}
Read on to learn about how to add Oso to your Ruby app and connect it with your application data. If you'd like to skip straight to an example of Oso used in an app, check out our Rails example app, a Github clone written with authorization in mind.
Setting up the Oso instance
First, we'll cover some of the basics of integrating Oso into an application.
The Oso
class is the entrypoint to using Oso in our application. We usually will have a global instance that is created during application initialization and shared across requests.
Loading our policy
Oso uses the Polar language to define authorization policies. An authorization policy specifies which actions are allowed and which data a user can access. The policy is defined in a Polar file, which lives alongside our code.
We'll start with an empty file called app/policy/authorization.polar
. Load the policy at application start using the OSO.load_files
function:
# During application start
require 'oso'
OSO = Oso.new
OSO.register_class(User)
OSO.register_class(Organization)
# ... register other classes used by the policy
OSO.load_files(['app/policy/authorization.polar'])
Controlling access with roles
Now, let's add role-based access control to our app. To set up roles in Oso, we must:
- Add role and resource configurations to our policy.
- Assign roles to users by defining a rule in our policy called
has_role
.
Configuring our first resource
Roles in Oso are scoped to resources. A role is a grouping of permissions: the actions that may be performed on that resource. Roles are assigned to actors (e.g., users) to grant them all the permissions the role has.
We define resources in Polar using resource blocks. The Organization
resource represents an Organization in the our example application. Let's walk through the resource definition for Organization
. Insert the following code into app/policy/authorization.polar
:
actor User {}
resource Organization {
# TODO: add permissions, roles, and permission assignments
}
NOTE: in order to define a resource block for the Organization
type, we must already have registered an Organization
class using register_class
, as we did above.
In our resource block, we first define the list of available actions for this resource (also called "permissions"). Our example is based loosely on a Github clone, so for now the available actions on organizations are read
, create_repo
, and manage_role_assignments
:
actor User {}
resource Organization {
permissions = ["read", "create_repo", "manage_role_assignments"];
# TODO: add roles and permission assignments
}
Now, we define our roles and the permissions granted to each role:
actor User {}
resource Organization {
permission = ["read", "create_repo", "manage_role_assignments"];
roles = ["member", "owner"];
# Member permissions
"read" if "member";
"create_repo" if "member";
# Owners can manage role assignments
"manage_role_assignments" if "owner";
# The owner role implies the member role
"member" if "owner";
}
This resource definition defines two roles:
- member: Has the
read
andcreate_repo
permission. - owner: Has the
manage_role_assignments
permission, and also inherits themember
role which grantsread
andcreate_repo
permissions.
Adding has_permission
to our policy
To enable resource block syntax, we add an allow rule referencing the has_permission
rule:
allow(actor, action, resource) if
has_permission(actor, action, resource);
Oso will now allow use our resource's permissions to decide which actions are allowed in the app.
Assigning roles to users
Now we've configured Org roles and set up our policy. For users to have access, we must assign them roles.
We use our own data models to build roles with Oso. We just need to tell Oso what roles a user has for a particular resource through the has_role
rule. As an example, we might add a method onto the user that returns a list of roles for that user:
class User
ORGS = [Organization.create, Organization.create, Organization.create]
ROLES = {
"alice": [
{"name": "member", "resource": ORGS[0]},
{"name": "owner", "resource": ORGS[1]}
],
"bob": [
{"name": "owner", "resource": ORGS[2]}
]
}
attr_reader :name
def initialize(name)
@name = name
end
# This could also return records from a database!
def roles
ROLES[name]
end
end
And we add the has_role
rule to our policy using the following code:
has_role(actor: User, role_name: String, resource: Organization) if
role in actor.roles and
role_name = role.name and
resource = role.resource;
Oso evaluates the has_role
rule with actor
bound to the same actor that we call the allow
rule with, typically an instance of some User
model. It should return a true result when the user does indeed have the role_name
role for the given resource
.
With the has_role
rule implemented in your policy, we now have role-based access control enabled! When our app calls OSO.authorize(actor, action, resource)
, the has_permission
rule will check whether the user has a role that grants them permission to perform action
on the given resource
.
An example of calling OSO.authorize
in a controller:
class OrganizationsController
def show
organization = Organization.find(params[:id])
# Ensure current_user has the "read" permission, raise if not
OSO.authorize(current_user, "read", organization)
organization.to_json
end
end
Our full policy in authorization.polar
is below:
allow(actor, action, resource) if
has_permission(actor, action, resource);
actor User {}
resource Organization {
permission = ["read", "create_repo", "manage_role_assignments"];
roles = ["member", "owner"];
# Member permissions
"read" if "member";
"create_repo" if "member";
# Owners can manage role assignments
"manage_role_assignments" if "owner";
# The owner role implies the member role
"member" if "owner";
}
has_role(actor: User, role_name: String, resource: Organization) if
role in actor.roles and
role_name = role.name and
resource = role.resource;
Check out a complete example
The example we defined here is quite basic: we only defined three permission on organizations, and we hard-coded the roles for each user into our User
class. In a real app, we have a number of different resources whose permissions might depend on each other, and we have data that we need to load from a database.
For a more comprehensive example of building RBAC in Ruby using Oso, check out Gitclub, a clone of Github's authorization model. It contains permissions on organizations, repositories, and issues. It also has two types of roles: organization roles and repository roles, which can be assigned separately.
Where to go next?
If you want to learn more about integrating Oso into your Ruby app, here are some resources to get started:
- Quickstart in Ruby: run through an example of adding Oso authorization rules to a simple app.
- Add Oso to an existing app: some more details about using Oso to add role based access control (RBAC) to your app.
- Enforce Oso policies in ruby: how to use the
Oso#authorize
method and surface authorization errors to users.
If at any point you get stuck, drop into our Slack and we'll unblock you.
We're also happy to schedule a 1x1 with an Oso engineer to help you get started.