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).
Here’s our logic: a user can "read"
a Repository
if they have the Reader
role on it, or they have the Member
or 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.