Skip to main content
Custom roles let your application’s end users define and assign their own roles with specific permissions, enabling organization-specific access control.

Implementation

First, define a new Role type to represent custom roles. When an end user creates a custom role, Oso stores its metadata and unique ID in the application database. End user can grant permissions to the role by writing grants_permission facts to Oso Cloud.
actor User { }
resource 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 "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"});
}
This test verifies that a user assigned a custom “repository admin” role receives only the permissions explicitly granted to that role—such as creating and deleting repositories—without inheriting broader organization-level permissions.

Best practices for modeling

Start by declaring the organization-level permissions you want to make configurable, then inherit those permissions on repositories. To avoid exposing excessive or overly granular access through custom roles, separate permissions into two categories:
  • Low-level actions your application enforces via oso.authorize calls
  • Higher-level, user-facing permissions that can be assigned to roles and managed by end users of your application
For example, while your app may check separate permissions for editing metadata, pushing code, and creating PRs, you might expose a single “write” permission for users to assign.