Custom Roles

Some applications reach the point where there is such diversity in how different organizations want their users to engage with features that it becomes impossible to anticipate all their needs and implement predefined roles that capture every use case.

In that case, rather than statically defining roles, the application can create custom roles on the fly.

Implement the logic

First, we need to create a new Role type to represent our custom roles.

When a user creates a new custom role, we will store metadata about the role in our application database and track it with a unique ID. But you grant permissions to the custom role by writing facts to Oso Cloud.


actor User { }
actor Role { }
resource Organization {
roles = ["admin", "member"];
permissions = [
"read", "add_member", "repository.create",
"repository.read", "repository.delete"
];
# role hierarchy:
# admins inherit all permissions that members have
"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";
}
# A custom role is defined by the permissions it grants
has_permission(actor: Actor, action: String, org: Organization) if
role matches Role and
has_role(actor, role, org) and
grants_permission(role, action);
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";
"read" if "repository.read" on "organization";
"delete" if "repository.delete" on "organization";
}

We need to declare the permissions that we want to be configurable at the organization level and then inherit those permissions on repositories.

Custom roles exposes permissions to users in a new way. You'll want to be careful about exposing too much flexibility and too many permissions.

Instead divide permissions into two groups:

  • Direct actions on the resource that your app checks in oso.authorize calls.
  • User-facing permissions that can be assigned to roles and manipulated by your application users.

For example, there might be three different internal checks for updating a repository's metadata (description, etc.), pushing code, and creating PRs. But you might initially expose these as a single "write" permission to users.


actor User { }
actor Role { }
resource Organization {
roles = ["admin", "member"];
permissions = [
"read", "add_member", "repository.create",
"repository.read", "repository.delete"
];
# role hierarchy:
# admins inherit all permissions that members have
"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";
}
# A custom role is defined by the permissions it grants
has_permission(actor: Actor, action: String, org: Organization) if
role matches Role and
has_role(actor, role, org) and
grants_permission(role, action);
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";
"read" if "repository.read" on "organization";
"delete" if "repository.delete" on "organization";
}

Finally, add the generic logic that says that users inherit permissions from any custom role they are assigned.


actor User { }
actor Role { }
resource Organization {
roles = ["admin", "member"];
permissions = [
"read", "add_member", "repository.create",
"repository.read", "repository.delete"
];
# role hierarchy:
# admins inherit all permissions that members have
"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";
}
# A custom role is defined by the permissions it grants
has_permission(actor: Actor, action: String, org: Organization) if
role matches Role and
has_role(actor, role, org) and
grants_permission(role, action);
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";
"read" if "repository.read" on "organization";
"delete" if "repository.delete" on "organization";
}

First, we need to create a new Role type to represent our custom roles.

When a user creates a new custom role, we will store metadata about the role in our application database and track it with a unique ID. But you grant permissions to the custom role by writing facts to Oso Cloud.

We need to declare the permissions that we want to be configurable at the organization level and then inherit those permissions on repositories.

Custom roles exposes permissions to users in a new way. You'll want to be careful about exposing too much flexibility and too many permissions.

Instead divide permissions into two groups:

  • Direct actions on the resource that your app checks in oso.authorize calls.
  • User-facing permissions that can be assigned to roles and manipulated by your application users.

For example, there might be three different internal checks for updating a repository's metadata (description, etc.), pushing code, and creating PRs. But you might initially expose these as a single "write" permission to users.

Finally, add the generic logic that says that users inherit permissions from any custom role they are assigned.


actor User { }
actor Role { }
resource Organization {
roles = ["admin", "member"];
permissions = [
"read", "add_member", "repository.create",
"repository.read", "repository.delete"
];
# role hierarchy:
# admins inherit all permissions that members have
"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";
}
# A custom role is defined by the permissions it grants
has_permission(actor: Actor, action: String, org: Organization) if
role matches Role and
has_role(actor, role, org) and
grants_permission(role, action);
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";
"read" if "repository.read" on "organization";
"delete" if "repository.delete" on "organization";
}

Test it works

As our test case, imagine our customer wants to create a "repository admin" role. This would be a user who has full administrative permissions on repositories but not on the organization itself.

So we assign permissions to the role and then grant it to Alice.

And we can then check that Alice only has the permissions granted by the role.


test "custom roles grant the permissions they are assigned" {
setup {
# repository admins can create + delete repositories
# but don't have full admin permissions on the organization
grants_permission(Role{"repo-admin"}, "repository.read");
grants_permission(Role{"repo-admin"}, "repository.create");
grants_permission(Role{"repo-admin"}, "repository.delete");
has_role(User{"alice"}, Role{"repo-admin"}, Organization{"acme"});
has_relation(Repository{"anvil"}, "organization", Organization{"acme"});
}
assert allow(User{"alice"}, "repository.create", Organization{"acme"});
assert allow(User{"alice"}, "read", Repository{"anvil"});
assert allow(User{"alice"}, "delete", Repository{"anvil"});
assert_not allow(User{"alice"}, "add_member", Organization{"acme"});
}