Polar Polymorphism (extends
)
Polar's type system offers subtype
polymorphism (opens in a new tab) through its extends
feature.
Using polymorphism/extends
, Polar lets you write single rules that can apply
to many types.
Actor
and Resource
types
All users of Polar encounter polymorphism, usually without realizing it, because
types declared as resource
extend the abstract Resource
type, and likewise
for actor
and Actor
.
This means that any rule that references Actor
can also accept types declared
as actor
.
Consider this policy snippet:
actor User {};resource Issue {};
To discuss the relationships, we saw that the subtype extends the supertype,
e.g. User
extends Actor
. The snippet above produces the following
relationships:
Supertype | Subtype |
---|---|
Actor | User |
Resource | Issue |
You're most likely to encounter Actor
and Resource
types when using
shorthand rules. Polar expands those
shorthand rules into full rules, which are simpler for us to write because they
rely on polymorphism. Said another way, we're able to write single instances of
many shorthand rules and they are automatically extended to support all of the
resource
and actor
types you declare.
For example a shorthand rule that gets expanded to:
has_permission(actor: Actor, action: String, resource: Resource)...
Could be satisfied with a query like:
oso-cloud query has_permission User:alice read Issue:123
Because User
extends Actor
, it can be used anywhere that a rule calls for an
Actor
. And likewise for Issue
and Resource
.
Actor extends resource
The Actor
type extends the Resource
type. This means that parameters that
accept Resource
values will also accept Actor
values.
This also means that you can write shorthand
rules inside actor
resource blocks. Though
not a commonly used feature, this lets you write e.g. roles and permissions over
actor
s in addition to resource
s.
Extending user types
You can leverage this kind of extensibility with your own type using extends
,
whose general form is:
(actor | resource) Subtype extends Supertype {}
Extending your own types provides two features:
This is a powerful tool if you have many types with common rules––you can
instead extend
all of them from a common supertype, and then write the common
rules using supertype variables.
Rule-type polymorphism
This is just a jargon-heavy way of saying that rules referencing a supertype can also be satisfied with subtype values.
As an example consider this policy:
actor User {}resource File { permissions = ["read"]; relations = {owner: User};}resource Document extends File {}has_permission(actor: Actor, "read", file: File) if is_public(file) or has_relation(actor, "owner", file);test "extends" { setup { has_relation(User{"alice"}, "owner", Document{"public.txt"}); is_public(Document{"public.txt"}); has_relation(User{"alice"}, "owner", Document{"private.txt"}); } # Though `has_permission` is defined on `File`, `Document` behaves in the # same way because it extends `File`. assert allow(User{"bob"}, "read", Document{"public.txt"}); assert_not allow(User{"bob"}, "read", Document{"private.txt"}); assert allow(User{"alice"}, "read", Document{"private.txt"});}
Inherited permissions, roles, and relations
In addition to letting you define your own types, resource blocks provide simplified construction of permissions, roles, and relations.
When you extend
a supertype, its subtype also inherits all of the same roles,
permissions, and relations. However, the supertype's roles, permissions, and
relations remain unchanged.
The following policy demonstrates this. Note the line stating that Document extends File
, i.e. Document
is a subtype of File
.
actor User {}resource File { permissions = ["read", "write"]; roles = ["reader", "writer"]; "read" if "reader"; "write" if "writer"; "read" if "write";}resource Document extends File {}test "extends" { setup { has_role(User{"alice"}, "writer", Document{"xyz.doc"}); } # Though roles + permissions are defined on `File`, `Document` behaves in # the same way because it extends `File`. assert allow(User{"alice"}, "read", Document{"xyz.doc"});}
Details
Unification
While Polar accepts subtype values for supertype variables, subtype values are not treated as instances of their supertype, i.e. no instance of subtype unifies with its supertype.
For example, given the following definition:
resource Document extends File {}
The following condition is always false
:
Document{"a"} = File{"a"}
Query type filtering
Despite having a similarly structured syntax, type filtering in queries exclude subtypes.
For example, if you define:
resource Document extends File {}
In a query, if you specify File
as a type filter, Oso will not return any
Document
values, even with a wildcard (_
) e.g.
oso-cloud query has_permission User:alice read File:_
Up next
- Resource blocks to begin creating your own types.
- Rules + facts to understand how Polar can evaluate your data.
Talk to an Oso engineer
If you want to learn more about Polar, schedule a 1x1 with an Oso engineer. We're happy to help.