Resource-Specific Roles

Almost every application starts with roles at the organization level. But it's a very coarse model since it only allows you to grant access to all resources in an organization.

A natural way to provide more fine-grained access is to extend the concept of roles to other resources in the application.

We call this pattern resource-specific roles since we're granting users access to a specific resource by granting them a role directly on the resource

When you start thinking in terms of resource-specific roles, it might become clear that organization-level roles are a specific case of resource-specific roles where the resource is the organization!

Implement the logic

Let's say our starting point is the organization roles example and we want to add granularity at the repository level.

Whereas previously we defined permissions like repository.read on the organization, we now want the granularity to express which repository the user can read.


actor User { }
resource Organization {
roles = ["admin", "member"];
permissions = [
"read", "add_member", "repository.create",
"repository.read", "repository.delete"
];
# role hierarchy:
# admins inherit all member permissions
"member" if "admin";
# org-level permissions
"read" if "member";
"add_member" if "admin";
# permission to create a repository
# in the organization
"repository.create" if "admin";
# permissions on child resources
"repository.read" if "member";
"repository.delete" if "admin";
}

To do that, turn Repository into its own resource and express the repository.read permission as a read permission on the repository. Next, inherit the "member" and "admin" roles from the organization.


actor User { }
resource Organization {
roles = ["admin", "member"];
permissions = [
"read", "add_member", "repository.create",
];
# role hierarchy:
# admins inherit all member permissions
"member" if "admin";
# org-level permissions
"read" if "member";
"add_member" if "admin";
# permission to create a repository
# in the organization
"repository.create" if "admin";
}
resource Repository {
permissions = ["read", "delete"];
roles = ["member", "admin"];
relations = {
organization: Organization,
};
# inherit all roles from the organization
role if role on "organization";
# admins inherit all member permissions
"member" if "admin";
"read" if "member";
"delete" if "admin";
}

Let's say our starting point is the organization roles example and we want to add granularity at the repository level.

Whereas previously we defined permissions like repository.read on the organization, we now want the granularity to express which repository the user can read.

To do that, turn Repository into its own resource and express the repository.read permission as a read permission on the repository. Next, inherit the "member" and "admin" roles from the organization.


actor User { }
resource Organization {
roles = ["admin", "member"];
permissions = [
"read", "add_member", "repository.create",
"repository.read", "repository.delete"
];
# role hierarchy:
# admins inherit all member permissions
"member" if "admin";
# org-level permissions
"read" if "member";
"add_member" if "admin";
# permission to create a repository
# in the organization
"repository.create" if "admin";
# permissions on child resources
"repository.read" if "member";
"repository.delete" if "admin";
}

Test it works

The new rules are equivalent to what we had before. One difference is that in order for Oso Cloud to know whether a user can access a repository it'll now need to know what organization the repository belongs to.

However, the benefit of adding this additional resource and relation data is now we can grant specific users granular access to specific repositories without needing to make them an admin of the entire organization.

For example, we can make Alice an admin of the "deleteme" repository so that she can delete it.


test "org members inherit permissions on repositories belonging to the org" {
setup {
has_role(User{"alice"}, "member", Organization{"acme"});
has_relation(Repository{"anvil"}, "organization", Organization{"acme"});
has_relation(Repository{"bar"}, "organization", Organization{"foo"});
}
assert allow(User{"alice"}, "read", Organization{"acme"});
assert allow(User{"alice"}, "read", Repository{"anvil"});
assert_not allow(User{"alice"}, "delete", Repository{"anvil"});
assert_not allow(User{"alice"}, "read", Repository{"bar"});
}
test "repository admins can delete repositories, regardless of their organization role" {
setup {
has_role(User{"alice"}, "member", Organization{"acme"});
has_relation(Repository{"anvil"}, "organization", Organization{"acme"});
has_relation(Repository{"deleteme"}, "organization", Organization{"acme"});
has_role(User{"alice"}, "admin", Repository{"deleteme"});
}
assert_not allow(User{"alice"}, "delete", Repository{"anvil"});
assert allow(User{"alice"}, "delete", Repository{"deleteme"});
}

If you want additional support modeling your authorization policies, try the Oso Modeler (opens in a new tab), a free UI-based tool designed to help you align your application's needs to common authorization patterns.