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:

SupertypeSubtype
ActorUser
ResourceIssue

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 actors in addition to resources.

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

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.

Get started with Oso Cloud →