Reuse Authorization Logic Using the Query API

Authorization decisions are made up of logic, and data that you check against that logic. The data is the specific authorization information that you use as inputs to a decision, like roles and relationships between users and resources. The logic is code that looks through those structures to see if they meet certain conditions. Typically you write that code inline in your application. This is convenient, but makes it hard to re-use your code for the next authorization scenario that comes up.

In Oso, you model authorization logic declaratively. This lets you use it for the first use case you have in mind, as well as future ones too.

Logic the hard way

Let’s say we're building an app to manage git repositories. We have organizations, and those organizations have repositories. Users can have different roles on an organization (e.g., member) and roles on a repository (e.g., reader).

Screen Shot 2022-09-29 at 11.34.49 AM.png

Here’s our logic: a user can "read" a Repository if they have the Reader role on it, or they have the Memberor Admin role on the Organization the repository belongs to. If we wanted to write this in Python, it might look like this:

def can_read_repository(user, repository):
    repo_roles = user.repo_roles(repository.id)
    for repo_role in repo_roles:
        if repo_role.role == "Reader":
            return True
    org_roles = user.org_roles(repository.parent_org.id)
    for org_role in org_roles:
        if org_role.role == "Member" or org_role.role == "Admin":
            return True
    return False

...

if can_read_respository(user, repository):
    return repository
else:
    raise AccessDenied

There’s nothing problematic about this code. It checks to see if the user has the Reader role on the repository or either the Member or Admin role on the repository's parent organization. This code answers the question "Can this specific user read this specific repository?" But if and when we need to ask related questions –  like "What are all the repositories this user can read?" or "What are all the users that can read this repository?" – it won’t help us with that.

To answer those questions, we’d have to write some new code, which might look like this:

def repositories_user_can_read(user):
    repos = []
    repo_roles = user.repo_roles()
    for repo_role in repo_roles:
        if repo_role.role == "Reader":
            repos.append(repo_role.repository_id)
    org_roles = user.org_roles()
    for org_role in org_roles:
        if org_role.role == "Member" or org_role.role == "Admin":
            org_repos = Repositories.get(parent_org_id=org_role.org.id)
            for org_repo in org_repos:
                repos.append(org_repo.id)
    return repos

def users_that_can_read_repository(repository):
    users = []
    repo_roles = repository.roles
    for repo_role in repo_roles:
        if repo_role == "Reader":
            users.append(repo_role.user_id)
    org_roles = repository.parent_org.roles
    for org_role in org_roles:
        if org_role == "Member" or org_role == "Admin":
            users.append(org_role.user_id)
    return users

For each new question we want to ask, we’ll have to write a new function to go through the roles and relations and get the specific kind of answer we need. And anytime we want to update our authorization model like add new roles or new mappings from roles to permissions, we’ll have to come back and refactor each of these functions individually.

Basic Logic in Polar

In Oso Cloud, you model your logic using Polar, our declarative policy language that we built specifically for authorization. The policy for the example from above would look like this:

actor User {}

resource Organization {
    roles = ["Admin", "Member"];
}

resource Repository {
    permissions = ["read"];
    roles = ["Reader"];
    relations = { parent_org: Organization }

    "Read" if "Member" on "parent_org";
    "Read" if "Admin" on "parent_org";
    "read" if "Read"
}

This code builds a rule called allow, which you can query to see if a user is allowed to do an action on a resource.

$ oso-cloud query allow User:steve read Repository:foo
allow(User:steve, String:read, Repository:foo)

This query returns a result if the query passes. Since checking if a user can do an action on a resource is the most common question, Oso Cloud provides a built-in method that wraps allow, called authorize.

We can use the same policy to ask other questions, too, like "What are all the repositories that a user can read?

$ oso-cloud query allow User:steve read Repository:_
allow(User:steve, String:read, Repository:foo)
allow(User:steve, String:read, Repository:bar)
allow(User:steve, String:read, Repository:baz)
...

Or "Who are all the users that can read a repository?”

$ oso-cloud query allow User:_ read Repository:foo
allow(User:steve, String:read, Repository:foo)
allow(User:sam, String:read, Repository:foo)
allow(User:gabe, String:read, Repository:foo)
...

Custom Queries

We can even ask questions that aren’t just variations of allow by writing custom Polar rules. For example, let’s say we wanted to ask, "Who are all the admins for the parent organization of a repository?" First, we would add a new rule to the bottom of the policy to reflect when this should evaluate to true:

admin_on_parent(user: User, repo: Repository) if
    has_relation(repo, "parent_org", org) and
    has_role(user, "Admin", org);

Then we would query Oso Cloud, using a wildcard for the user argument:

oso-cloud query admin_on_parent User:_ Repository:foo
admin_on_parent(User:Sam, Repository:foo)
...

What we’ve done here is ask many different types of questions (needed for different parts of the application) all from authorization logic located in one policy. List endpoints can ask for all the resources a user can see. UI code can ask for all the actions a user can take so it can hide buttons and your admin dashboard can list all the admins of a repo…all from the same policy. And when we need to debug or refactor, we have one place to go.

For a full guide on using queries in Oso Cloud, read the doc.

Want us to remind you?
We'll email you before the event with a friendly reminder.

Write your first policy