Skip to main content
Conditional roles enable advanced access control schemes including default roles and toggles.

Default roles

In this example, organizations want members to automatically inherit a role on all repositories. Different organizations choose different default roles: some grant “viewer”, others grant “admin”. Without conditional roles, you’d sync has_role(User{"Alice"}, <default role>, Repository{"Anvils"}) for every user-repository combination. That’s a lot of facts. Instead, send one fact like has_default_role(Organization{"acme"}, "editor") to set a default role for their members to inherit on all of their repositories.

Implementation

Add a custom has_role rule that grants the organization-configured default role on repositories to all members of the repository’s organization.
actor User {}

resource Organization {
  roles = ["member", "admin"];
  permissions = ["set_default_role"];

  "set_default_role" if "admin";
}

resource Repository {
  roles = ["reader", "editor", "admin"];
  permissions = ["write"];
  relations = { organization: Organization };

  "write" if "editor";
}

has_role(actor: Actor, role: String, repo: Repository) if
  org matches Organization and
  has_relation(repo, "organization", org) and
  has_default_role(org, role) and
  has_role(actor, "member", org);

test "default org role grants permission to org members" {
  setup {
    has_default_role(Organization{"acme"}, "editor");
    has_role(User{"alice"}, "member", Organization{"acme"});
    has_relation(Repository{"anvil"}, "organization", Organization{"acme"});
  }

  assert has_role(User{"alice"}, "editor", Repository{"anvil"});
  assert allow(User{"alice"}, "write", Repository{"anvil"});
}
The ACME organization decided “editor” should be the default role on their repositories. ACME members like Alice can write to all ACME repositories.

Toggles

You can conditionally inherit roles by specifying attributes on the resource itself instead of on a related resource. A common example is a toggle on the resource. For example, a setting that specifies if the resource is “protected” and restricts access accordingly.

Implement the logic

Instead of inheriting all roles unconditionally (role if role on "organization") or a default role on all repositories, only allow users to inherit roles on repositories that aren’t marked as “protected”.
actor User { }

resource Organization {
  roles = ["admin", "member"];
  permissions = [
    "read", "add_member", "repository.create",
  ];

  # role hierarchy:
  # admins inherit all member permissions
  "member" if "admin";

  # organization 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,
  };

  "admin" if "admin" on "organization";

  # admins inherit all member permissions
  "member" if "admin";

  "read" if "member";
  "delete" if "admin";
}

# like `role if role on "organization"`
# but with an additional condition `is_protected`
has_role(actor: Actor, role: String, repository: Repository) if
  not is_protected(repository) and
  org matches Organization and
  has_relation(repository, "organization", org) and
  has_role(actor, role, org);

test "organization members can only read repositories that are not protected" {
  setup {
    has_role(User{"alice"}, "member", Organization{"acme"});
    has_relation(Repository{"anvil"}, "organization", Organization{"acme"});
    has_relation(Repository{"bar"}, "organization", Organization{"acme"});
    is_protected(Repository{"bar"});
    has_relation(Repository{"foo"}, "organization", Organization{"acme"});
    is_protected(Repository{"foo"});
    # grant Alice explicit access to foo
    has_role(User{"alice"}, "member", Repository{"foo"});
  }

  assert has_role(User{"alice"}, "member", Repository{"anvil"});
  assert allow(User{"alice"}, "read", Repository{"anvil"});
  assert_not allow(User{"alice"}, "read", Repository{"bar"});
  assert allow(User{"alice"}, "read", Repository{"foo"});
}

test "org admins can unconditionally read and delete repositories" {
  setup {
    has_role(User{"alice"}, "admin", Organization{"acme"});
    has_relation(Repository{"anvil"}, "organization", Organization{"acme"});
    is_protected(Repository{"anvil"});
  }

  assert allow(User{"alice"}, "read", Repository{"anvil"});
  assert allow(User{"alice"}, "delete", Repository{"anvil"});
}
The tests cover four cases:
  1. As an organization member, Alice can read unprotected repositories.
  2. As an organization member, Alice can not read protected repositories.
  3. As an organization member, Alice can read a protected repository if she’s explicitly invited to it.
  4. As an organization admin, Alice can read and delete all repositories regardless of protected status.

Combine default roles and toggles

The two pieces of logic combine together perfectly:
has_role(actor: Actor, role: String, repository: Repository) if
  not is_protected(repository) and
  org matches Organization and
  has_relation(repository, "organization", org) and
  has_default_role(org, role) and
  has_role(actor, "member", org);
This rule grants organization members the default repository role on all non-protected repositories.

Next steps

Conditional roles enable advanced authorization patterns while minimizing fact management. Consider these approaches for your application:
  • Default roles when you need organization-wide permissions with minimal data sync
  • Toggles when resources need protection levels or feature flags
  • Combinations when you need both default inheritance and conditional access
For more complex scenarios, explore attribute-based access control patterns or learn about role hierarchies.