Polar Tests
In your policy (i.e. the collection of rules for your environment), Polar has a built-in test feature that lets you run unit tests of literal-value queries using a set of facts only visible for the lifetime of the test.
Feature | Description |
---|---|
test | Introduces a test block, which lets you run a set of unit tests |
setup | Declares facts to make accessible for the lifetime of your test |
assert | Succeeds if the expression evaluates to true |
assert_not | Succeeds if the expression evaluates to false |
iff | When used with a variable, causes an assert to succeed if and only if all values of the variable that satisfy the assertion exist in a list provided to the assertion |
allow | By default, Polar includes an alias to has_permission , which is a common shorthand rule |
test fixture | Declare a test fixture block comprising a set of facts that you can reuse across multiple tests |
fixture | Reference a test fixture inside a setup block to include its facts in the setup block |
Here's an annotated pseudo-example demonstrating the syntax:
test fixture fixture_name { # semicolon-separated list of facts ...}test "test name" { setup { fixture fixture_name; # semicolon-separated list of facts ... } # semicolon-separated list of tests of form: # (assert|assert_not) "rule predicate"("comma-separated list of object literals") ...}
Simple example
Here's a simple example policy including tests, relying only on primitive types.
# Rule: A parent and their children are considered family.family(a: String, b: String) if parent(a, b) or parent(b, a);# Create a test so we can introduce temporary facts and make assertionstest "Polar ref family test" { # Set up our temporary facts that exist only for our test's lifetime setup { # Fact: Bernie has two parents, Pat and Morgan parent("Bernie", "Pat"); parent("Bernie", "Morgan"); } # Validate that our policy produces the expected results with our temporary facts assert family("Bernie", "Pat"); assert family("Morgan", "Bernie"); # Our rule does not state that the parents of a child are family assert_not family("Pat", "Morgan"); # Rules without matching parameters are false assert_not family(true, "Morgan");}
Complex example
Here is a more complex example that leverages Polar's abstract types and more closely mirrors how we expect you to use Oso.
# Actors are the who. Most of the time, this is a User# https://www.osohq.com/docs/guides/model-your-apps-authz#actors-and-resourcesactor User {}# This is a resource block that is used for grouping authorization # logic pertaining to a particular type of resource.# A resource represents an application component that we wish to protect.# https://www.osohq.com/docs/reference/glossary#resource-blocksresource Organization { roles = ["viewer", "owner"]; permissions = ["view", "edit"]; # These are permissions for the Organization resource "view" if "viewer"; "edit" if "owner"; # Organization owners inherit all permissions that Organization viewers have "viewer" if "owner";}# This is an example of a different resource block resource Repository { roles = ["viewer", "owner", "contributor"]; permissions = ["view", "edit", "create"]; # This is an example of how we can define the relationship # between resources. Relations are set within the resource block. # This relation is named parent and it says that Repository resource # is related to Organization. # https://www.osohq.com/docs/reference/more/resource-blocks#relation-declarations relations = { parent: Organization }; "view" if "viewer"; "edit" if "contributor"; "create" if "owner"; # contributors are also viewers # owners are also contributors "viewer" if "contributor"; "contributor" if "owner"; # roles are inherited from the parent organization "viewer" if "viewer" on "parent"; "owner" if "owner" on "parent";}# These are examples of how to test the Policy logic.# https://www.osohq.com/docs/guides/policy-teststest "Organization roles and permissions" { # Authorization decisions require data. This is where you can # define the test data. The test data is defined in a format # that Oso Cloud refers to as Facts. # https://www.osohq.com/docs/concepts/oso-cloud-data-model#facts setup { has_role(User{"alice"}, "viewer", Organization{"example"}); has_role(User{"bob"}, "owner", Organization{"example"}); } # This is how we assert that a user is authorized # to perform a particular action or not assert allow(User{"alice"}, "view", Organization{"example"}); assert allow(User{"bob"}, "view", Organization{"example"}); assert_not allow(User{"alice"}, "edit", Organization{"example"}); assert allow(User{"bob"}, "edit", Organization{"example"});}test "Repository roles and permissions" { setup { has_role(User{"alice"}, "viewer", Repository{"example"}); has_role(User{"bob"}, "owner", Repository{"example"}); has_role(User{"charlie"}, "contributor", Repository{"example"}); } assert allow(User{"alice"}, "view", Repository{"example"}); assert allow(User{"bob"}, "view", Repository{"example"}); assert allow(User{"charlie"}, "view", Repository{"example"}); assert_not allow(User{"alice"}, "edit", Repository{"example"}); assert allow(User{"bob"}, "edit", Repository{"example"}); assert allow(User{"charlie"}, "edit", Repository{"example"}); assert_not allow(User{"alice"}, "create", Repository{"example"}); assert allow(User{"bob"}, "create", Repository{"example"}); assert_not allow(User{"charlie"}, "create", Repository{"example"});}test "Repository parent relation" { setup { has_relation(Repository{"example"}, "parent", Organization{"parentOrganization"}); has_role(User{"alice"}, "viewer", Organization{"parentOrganization"}); has_role(User{"bob"}, "owner", Organization{"parentOrganization"}); } assert allow(User{"alice"}, "view", Repository{"example"}); assert allow(User{"bob"}, "view", Repository{"example"}); assert_not allow(User{"charlie"}, "view", Repository{"example"}); assert_not allow(User{"dave"}, "view", Repository{"example"}); assert_not allow(User{"alice"}, "edit", Repository{"example"}); assert allow(User{"bob"}, "edit", Repository{"example"}); assert_not allow(User{"charlie"}, "edit", Repository{"example"}); assert_not allow(User{"dave"}, "edit", Repository{"example"}); assert_not allow(User{"alice"}, "create", Repository{"example"}); assert allow(User{"bob"}, "create", Repository{"example"});}
Reuse data across multiple tests
You can reuse facts across multiple tests by defining a test fixture
block.
Here's an example:
test fixture acme { has_role(User{"alice"}, "admin", Organization{"acme"}); has_role(User{"bob"}, "member", Organization{"acme"}); has_relation(Repository{"foo"}, "organization", Organization{"acme"}); is_private(Repository{"foo"});}test "by default, members cannot read private repositories" { setup { fixture acme; } assert allow(User{"alice"}, "read", Repository{"foo"}); assert_not allow(User{"bob"}, "read", Repository{"foo"});}test "members can read private repositories if they have direct access" { setup { fixture acme; has_role(User{"bob"}, "reader", Repository{"foo"}); } assert allow(User{"bob"}, "read", Repository{"foo"});}
Test multiple permissions in a single assertion
Oso Policy tests support variables in assertions. These allow you to test multiple permissions with a single assert
statement. We'll illustrate this with some examples.
assert
statement.Suppose a Repository
resource that defines 3 roles and 4 permissions.
actor User {}resource Repository { roles = ["member", "owner", "admin"]; permissions = ["read", "write", "archive", "delete"]; "member" if "owner"; "owner" if "admin"; "read" if "member"; "write" if "member"; "archive" if "owner"; "delete" if "admin";}
In order to test this policy exhaustively, you need to test 12 total conditions (4 permissions for each of the 3 roles). In a typical test, that looks like this:
test "parent-child permissions" { setup { # alice is an admin on Repository "cool-repo" has_role(User{"alice"}, "admin", Repository{"cool-repo"}); has_role(User{"bob"}, "owner", Repository{"cool-repo"}); has_role(User{"charlie"}, "member", Repository{"cool-repo"}); } ### ASSERTIONS ### # Alice can perform all actions assert allow(User{"alice"}, "read", Repository{"cool-repo"}); assert allow(User{"alice"}, "write", Repository{"cool-repo"}); assert allow(User{"alice"}, "archive", Repository{"cool-repo"}); assert allow(User{"alice"}, "delete", Repository{"cool-repo"}); # Bob can perform all actions except for "delete" assert allow(User{"bob"}, "read", Repository{"cool-repo"}); assert allow(User{"bob"}, "write", Repository{"cool-repo"}); assert allow(User{"bob"}, "archive", Repository{"cool-repo"}); assert_not allow(User{"bob"}, "delete", Repository{"cool-repo"}); # Charlie can perform the "read" and "write actions, but not "archive" or "delete" assert allow(User{"charlie"}, "read", Repository{"cool-repo"}); assert allow(User{"charlie"}, "write", Repository{"cool-repo"}); assert_not allow(User{"charlie"}, "archive", Repository{"cool-repo"}); assert_not allow(User{"charlie"}, "delete", Repository{"cool-repo"});}
Instead of explicitly specifying each tested value, you can use variables to consolidate multiple assertions into a single statement. You express a variable in an assertion using the same name: Type
syntax that you use for variables elsewhere in Polar.
Using the iff
operator
When used with a variable, the iff
operator allows you to assert that a statement passes if and only if all of the values of variable that satisfy the statement exist in a list that you supply to the assertion.
For example, you can use the iff
operator to reduce the previous test from 12 assertions to 3
test "parent-child permissions" { setup { # alice is an admin on Repository "cool-repo" has_role(User{"alice"}, "admin", Repository{"cool-repo"}); has_role(User{"bob"}, "owner", Repository{"cool-repo"}); has_role(User{"charlie"}, "member", Repository{"cool-repo"}); } ### ASSERTIONS ### # Alice can perform all actions assert allow(User{"alice"}, action: String, Repository{"cool-repo"}) iff action in ["read", "write", "archive", "delete"]; # Bob can perform all actions except for "delete" assert allow(User{"bob"}, action: String, Repository{"cool-repo"}) iff action in ["read", "write", "archive"]; # Charlie can perform the "read" and "write actions, but not "archive" or "delete" assert allow(User{"charlie"}, action: String, Repository{"cool-repo"}) iff action in ["read", "write"];}
There are two changes:
- The specific actions in the original assertions have been replaced by the variable
action: String
- The values of
action
that are expected to satisfy the assertion are defined in a list associated with eachiff action in
statement
This lets you write a single assertion that confirms:
- all actions in the list are allowed and
- all actions that aren't in the list are denied
You can assert that a user has no permissions on a resource by passing an empty list to iff
.
assert allow(User{"diane"}, action: String, Repository{"cool-repo"}) iffaction in [];
You can use variables for any types in your policy, not just strings. For example, this assertion includes a variable user
to confirm that alice
and bob
have the archive
permission, but charlie
does not.
assert allow(user: User, "archive", Repository{"cool-repo"}) iffuser in [User{"alice"}, User{"bob"}];
Using wildcards
You can use a wildcard to assert that any value of a variable satisfies a condition. For example, you may have a global admin role that should always be able to perform any action, whether that action exists today or not. You can use a wildcard to assert this in a single statement, instead of having to remember to add explicit assertions each time you add a new permission.
Here's a version of the above policy that grants the admin
role any permission:
actor User {}resource Repository { roles = ["member", "owner", "admin"]; permissions = ["read", "write", "archive", "delete"]; "member" if "owner"; "owner" if "admin"; permission if "admin"; "read" if "member"; "write" if "member"; "archive" if "owner";}
You can use a wildcard variable to validate that the admin
role is granted any possible permission.
assert allow(User{"alice"}, _action: String, Repository{"cool-repo"});
Note that the previous iff
assertions will now fail, because the new policy allows actions that don't exist in the list. For example, if you invoke the previous assertion for alice
:
assert allow(user: User, "archive", Repository{"cool-repo"}) iffuser in [User{"alice"}, User{"bob"}];
The test will fail with the following message:
Expected the assertion to hold if and only if `action` is in ["read", "write", "archive", "delete"], but it holds when `action` is any String
Up next
- Patterns to understand common ways to model your applications.
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.