Resource Blocks
Resource blocks provide essential features for building authorization logic. They:
- Declare your own custom types, which can be referenced in rules, facts, and facts stored as data.
- Provide built-in features to declare permissions, roles, and relations and write shorthand rules. This greatly simplifies writing RBAC policies.
Polar offers two types of resource blocks, which align with its built-in abstract types:
Type | Description |
---|---|
resource | An object to which you want to permit certain actions. |
actor | An entity to whom you want to grant permissions to perform certain actions. |
Each type you define must be declared as an actor
or resource
.
The simplest form of a block looks like this:
# Actor blockactor User {}# Resource blockresource Repository {}
A more complete block looks like this:
resource Repository { permissions = ["read", "push"]; roles = ["contributor", "maintainer"]; relations = { parent: Organization }; # Shorthand rules ## An actor has the "read" permission if they have the "contributor" role. "read" if "contributor"; ## An actor has the "push" permission if they have the "maintainer" role. "push" if "maintainer"; ## An actor has the "contributor" role if they have the "maintainer" role. "contributor" if "maintainer"; ## An actor has the "maintainer" role if they have the "internal_admin" role ## on the "parent" Organization. "maintainer" if "internal_admin" on "parent";}
The Global Block
Each Polar policy may have up to one global
block, which represents represent
roles and permissions that can be referred to by any
other resource. These are particularly useful for representing global
roles.
The global block has the same syntax as resource blocks, but rather than
differentiating blocks by a resource name, there is only a single global
block:
global { roles = ["admin", "member"]; permissions = ["invite_member", "create_tenant"]; "create_tenant" if "member"; "member" if "admin"; "invite_member" if "admin";}
You can refer to roles and permissions from a global block by using the keyword
global
before the role/permission you want to use:
resource Organization { roles = ["internal_admin"]; permissions = ["read"]; "internal_admin" if global "admin"; "read" if "internal_admin";}
Permissions, roles, and relations
To simplify writing RBAC policies, Polar provides the following features in resource blocks, which you can use when writing shorthand rules.
Feature | Description |
---|---|
permissions | Classifies actions that Actors can perform on this resource. |
roles | Classifies Actor s in relation to this resource. |
relations | Classifies the relationship between this and others resources. |
A few general notes about these features:
- Each resource block may declare each of these features at most once, i.e. you
cannot declare
roles
twice. - Within each resource block, values used among all of these feature must
unique. For example, you cannot declare
roles
with a value"writer"
and declare arelations
named"writer"
. - The feature must be declared to be available in shorthand rules.
Permissions
Permissions classify actions that Actors
can perform on this resource.
permissions
declaration values must be an array of String
s. For example:
resource Repository { permissions = ["read", "push"];}
Rules that expect permission values will expect String
data that matches one
of the values you identify in the declaration. For example:
has_permission(actor:Actor, permission:String, resource:Resource)...
Roles
Roles classify Actor
s in relation to this resource.
roles
declarations must be an array of String
s. For
example:
resource Repository { roles = ["contributor", "maintainer", "admin"];}
Rules and facts that expect roles values will expect String
data that matches one
of the values you identify in the declaration. For example:
has_role(actor:Actor, role:String, resource:Resource)...
Relations
Roles classify the relationship between this and others resources.
relation
declaration must be key-value pairs, whose:
- Key is an unquoted string (i.e. an identifier)
- Value is the type of the related object
For example:
resource Repository { relations = { parent: Organization };}
Rules and facts that expect relation values will expect String
data
representing the relation name (e.g. "parent"
), as well as data representing
the related type (e.g. Organization
). For example:
has_relation(resource: Repository, relation: String, parent: Organization)...
Shorthand Rules
If you include permissions
, roles
, or relations
declarations in your
resource block, you can also write "shorthand" rules.
For example:
resource Repository { permissions = ["read", "push"]; roles = ["contributor", "maintainer"]; relations = { parent: Organization }; # Shorthand rules ## An actor has the "read" permission if they have the "contributor" role. "read" if "contributor"; ## An actor has the "push" permission if they have the "maintainer" role. "push" if "maintainer"; ## An actor has the "contributor" role if they have the "maintainer" role. "contributor" if "maintainer"; ## An actor has the "maintainer" role if they have the "internal_admin" role ## on the "parent" Organization. "maintainer" if "internal_admin" on "parent";}
A shorthand rule has the basic form:
<role or permission> if <expression>;
The left-hand side of the if
statement is the result that is granted if the
right-hand side evaluates to true
.
Shorthand rules then get expanded into full rules when they're loaded via expansion.
Left-hand side expressions
The left-hand side can be one of:
- A role, e.g.,
"contributor"
. - A permission, e.g.,
"read"
. - The
role
orpermission
keywords, which represent any role or permission.
Each of these expand to a rule head, which describes what is being granted.
For example, in the shorthand rule "read" if "contributor";
, the left-hand side is "read"
, which expands to:
has_permission(actor: Actor, "read", resource: Repository) if ...
Notice that since the shorthand rule exists in the Repository
resource block, we know that the resource type is Repository
.
Right-hand side expressions
The right-hand side of the if
statement is an expression that describes when the left hand side is true.
Examples of valid right-hand sides are:
-
A role, e.g.,
"contributor"
. This is commonly used for granting specific permissions to a role like"read" if "contributor"
. -
A permission, e.g.,
"read.issues"
. This is typically used to group common permissions together to reduce repetition, e.g.,"read.issues" if "read"
. -
A role or permission on a related resource, e.g.,
"reader" on "parent"
. This is used for granting roles and permissions across resources — that is, granting a role or permission on one type of resource to a role on a related resource. And the role/permission must be declared as a role/permission on the related resource type, while the relation must be declared on the current resource type. -
Rule call syntax, e.g.,
is_public(resource)
. This is commonly used to check an attribute on a resource. Supported syntax is a rule name followed by a list of arguments. Arguments can either be primitive types (e.g., strings"open"
or numbers0
) or the keywordresource
, which refers to the resource declared by the enclosing resource block.
Shorthand Rule Expansion
Shorthand rules are expanded to full Polar rules when they are loaded. The semantics of this expansion are as follows.
Expansion without relation
$x if $y;=> rule1(actor: Actor, $x, resource: $Type) if rule2(actor, $y, resource);
where rule1
and rule2
are the expansions of $x
and $y
respectively.
If $x
is a permission, then rule1
will be
has_permission
. If $x
is a role, then rule1
will be
has_role
. The same semantics apply for $y
and $rule2
.
The resource argument specializer $Type
is determined by the enclosing block
definition. E.g., if the rule is defined inside of
resource Repository {}
, then $Type
will be Repository
.
For example:
resource Repository { permissions = ["read"]; roles = ["contributor"]; "read" if "contributor"; # Shorthand rule}# Expanded rule# "read" if "contributor" ;# \/ \/has_permission(actor: Actor, "read", resource: Repository) if has_role(actor, "contributor", resource);
Expansion with relation
$x if $y on $z;=> rule1(actor: Actor, $x, resource: $Type) if rule2(actor, $y, related) and has_relation(resource, $z, related);
where rule1
, rule2
, and has_relation
are the expansions of $x
, $y
,
and $z
respectively.
The expansion of $x
to rule1
and $y
to rule2
follow the same semantics
as expansion without relation. $z
must always
be a declared relation on the enclosing resource
type. The has_relation
rule is necessary in order to access the related
object that rule2
references.
For example:
resource Repository { roles = ["admin"]; relations = {parent: Organization}; "admin" if "owner" on "parent";}# Expanded rule# "admin" if "owner"# \/ \/has_role(actor: Actor, "admin", resource: Repository) if has_role(actor, "owner", related) and# on "parent" ||# \/ || has_relation(resource, "parent", related);
Global block expansion
Global blocks expand to reference an instance of the type Global
.
$x if global $y;=> rule1(actor: Actor, $x, resource: $Type) if rule2(actor, $y);
where rule1
and rule2
are the expansions of $x
and $y
respectively.
If $x
is a permission, then rule1
will be
has_permission
. If $x
is a role, then rule1
will be
has_role
. The same semantics apply for $y
and $rule2
.
resource Organization { roles = ["internal_admin"]; permissions = ["read"]; "internal_admin" if global "admin"; "read" if "internal_admin";}# Expanded rule# "internal_admin" if global "admin" ;# \/ \/has_role(actor: Actor, "internal_admin", organization: Organization) if has_role(actor, "admin");
Referencing resources
Whether your resources are identified as actor
or resource
, you refer to
them using their name (e.g. ResourceName
), followed by a literal String
value (e.g. "ident"
) surrounded by curly brackets ({}
), e.g.
ResouceName{"ident"}
This is referred to as the "object literal" syntax.
Polymorphism
Polar uses polymorphism widely within resource blocks.
For example, the Actor
abstract type used alongside the has_role
and
has_permission
predicates can successfully resolve values of any type declared
in an actor
resource block.
Additionally, Actor
is a subtype of Resource
, which means that any variable
of type Resource
can also be of type Actor
. This is why we refer to them as
"resource blocks" and not "actor or resource blocks"; all actor
types are
implicitly also resource
types.
For more information, see our extends
documentation.
actor
vs. resource
Polar uses the convention mentioned above: actor
types represent things to
which you grant permissions and resource
types are those you grant permissions
to. However, actor
types are resource
types, so things get a little confusing.
One guiding principle to use is determining whether something should be an
actor
or a resource
is based on the fully-expanded shorthand rules detailed
above. Both has_permission
and has_role
take a type that extends the Actor
abstract type as its first argument. If you
can conceive of the resource as a thing that can have roles and permissions
assigned to it, modeling it as an actor
is a good idea; otherwise, model it as
a resource
.
Up next
- Rules + facts to understand how Polar can evaluate your data.
- Tests to generate unit tests for your resource blocks.
Talk to an Oso Engineer
If you'd like to learn more about resource blocks and how to use them within your policy, schedule a 1x1 with an Oso engineer. We're happy to help.