Role-Based Access Control for Application Authorization¶
When managing access to resources within 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).
While there is no one way to implement RBAC, there are two types of relationships that must be considered in a roles system:
1. User-role relationships define what roles a user has. The relationship could be a direct assignment from user-to-role, but could also be an indirect relationship that depends on additional information.
2. Role-permission relationships define the access permissions that a role grants to a user. In oso, permissions generally consist of an action and a resource.
oso represents these relationships as rules in a policy file, written in our declarative logic programming language called Polar. In general, a policy specifies user-role relationships with
user_in_role rules and role-permission relationships with
role_allow rules. The general form of an allow rule that calls these rules looks like this:
allow(actor, action, resource) if user_in_role(actor, resource, role) and role_allow(role, action, resource);
The rest of this document explains how to implement these rules for different RBAC use cases.
“Global” roles refers to a single set of roles that applies to the entire application. Roles are not scoped to a particular domain, such as a tenant or a resource. Global roles are often useful in single-tenant applications that require a small set of roles
Static mappings between users and roles can be specified in Polar. This avoids implementing user-role mappings in your application code, but does mean that role assignments must be hardcoded for all users.
This example assumes that users are stored as a
User model, but any object can be used to represent a user (including a simple string).
To avoid hardcoding role assignments, which may be useful if you expect to assign new users to roles dynamically, you can store user-role assignments in your application, and look up the assignment in the policy:
Role permissions are defined in Polar with
role_allow rules. These are very similar to
allow rules, but instead of taking an actor as the first argument, they take a role.
In order to use roles in your application, define an
allow rule that uses the role logic you’ve defined, and query it using the
is_allowed() method in the oso library.
allow role defined, you can query it using the oso library:
Roles in a multi-tenant application¶
In multi-tenant applications, roles are usually scoped to only apply to users and resources within a particular tenant.
One-to-many tenant-user and tenant-resource relationships¶
A straight-forward multi-tenant RBAC system has the following characteristics:
Users and resources can only belong to a single tenant
The same set of roles exists for all tenants
Roles have the same permissions for all tenants (e.g.
adminin tenant_1 provides the same access control rights as it does in tenant_2, but users in tenant_1 cannot access resources in tenant_2).
A role model that meets the above characteristics is very similar to the model for Global Roles.
User-role mappings and role-permission mappings can be done the same way as Global Roles, with
All that is required to scope roles to single tenants is to check tenancy in the
allow rule that implements the role check.
The above check will ensure that the user’s role will only apply to resources within the same tenant as the user. This model requires that the tenant is accessible on both user and resource objects.
Many-to-many tenant-user relationships¶
In some applications, users can belong to multiple tenants, and may have different roles in each tenant. An example of this is GitHub, where users can belong to multiple organizations, and may have a different role in each organization.
In this case, mapping users to roles actually becomes mapping users to roles and tenants. This can be done entirely in the policy with
user_in_role_for_tenant rules. This approach avoids needing to store any role data in the application, but does mean that role assignments are hardcoded for all users.
To avoid hardcoding role assignments for users, the user-role-tenant assignments can be stored as application data. One implementation of this would be to store the roles on the user. Since users can have different roles depending on the tenant, roles should be stored by tenant.
As long as roles have the same permissions across all tenants,
role_allow rules can be used to specify role-permission mappings, as with single-tenant roles.
If the roles have different permissions depending on the tenant, the
role_allow rule can be modified to take the tenant as an argument:
To enable the above rules, write an allow rule that calls
user_in_role_for_tenant to get the relevant role, and call
role_allow. The tenant ID of the resource is used to look up the role, to make sure that the role is associated with the same tenant as the resource the actor is trying to access.
Role hierarchies represent a model where certain roles are senior to others. More senior roles inherit permissions from less senior roles. For example, an organization may have a “manager” role and a “programmer” role. The “manager” role is more senior than the “programmer”, and therefore it inherits the permissions of the “programmer” role, in addition to its own permissions.
With roles represented as strings in oso policies, role inheritance can be represented with the following structure:
By adding the above
role_allow, any role hierarchies declared with
inherits_role rules will be enforced. Permissions should be assigned to roles directly using
With these roles in place, users with the “manager” role will be able to take any action on both programming resources and manager resources.
Adding a new role to the hierarchy is very simple with this structure. For example, adding an “admin” role that inherits permissions from the “manager” role would require adding one rule:
This role hierarchy structure supports multiple inheritance, meaning that a single role can inherit from multiple junior roles (by adding more
inherits_role rules). For example, there may be a “test_engineer” role that the “manager” also inherits permissions from. Simply adding another
inherits_role for “manager” will implement this model.
When controlling access to more than one type of resource, it is often useful to use roles that specifically apply to one resource or another. For example, in a project management app there might be
Project resources, which have the following roles: “member”, “developer”, and “manager”. These roles assign permissions specifically to the
If these roles are pre-defined, they generally will confer the same permissions across all
Project resources, but the users assigned to the role will differ from project-to-project. In other words, the role-permission mappings are specific to the resource type, while the user-role mappings are specific to the resource instance.
This model can be implemented in Polar by implementing
role_allow rules, which are enabled with the following top-level
Users are generally assigned a resource-specific role on a per-resource basis. Meaning, a user could have the “member” role for Project 1 and the “admin” role for Project 2, and the user’s access would be different for each resource. Users can be mapped to roles on a per-resource basis in Polar, by hardcoding the user-role-resource assignments:
To avoid hardcoding the user-role-resource assignments, the assignments can be stored as application data and accessed from the policy.
There are a variety of ways to store these mappings in the application. The following rules show how the mapping might be accessed in different ways, depending on the mapping implementation.
Scoping the permissions of a role to a single resource type is straight-forward in Polar, using rule specializers.
Resource Hierarchies/ Nested Resources¶
It is common for resources to be nested inside of other resources. To propagate access control through a resource hierarchy, it can be useful to use a role to grant access to the top-level resource, and infer permissions for nested resources based on that role. For example, there may be
Document resources nested within the
Project resource, and the
Project “member” role should also grant certain kinds of access to documents within the project.
Using roles with user groups¶
Assigning roles to User groups¶
Sometimes it is helpful to assign a role to a group of users, rather than an individual user. A good example of this is GitHub. In GitHub, users within an Organization can be added to Teams. Roles can be assigned to teams, rather than users, and the access granted by a team-level role applies to all the team members. For this example, let’s say that team-level roles are scoped to resources.
Roles within a hierarchy of groups¶
Applications often represent organization hierarchies by creating hierarchical user groups. For example, GitHub supports nested Teams. Recursive
group_in_role rules can be used to propagate roles through a group hierarchy.
Sometimes it is convenient for user-role relationships to be implied, rather than direct. For example, in GitHub’s permissions system, the user who owns an organization or repository is assigned the “admin” role for that resource by default.
Implied role assignments eliminate the need to keep direct user-role mappings up to date in the event that the data they depend on changes. E.g., if the ownership of a repository is switched, the “admin” role should automatically be reassigned to the new owner.
This can be implemented in Polar by adding conditions to the body of