Oso Cloud Data Model

Facts: the Oso Cloud Data Model

This document discusses Oso Cloud’s data model, the fact. It will cover topics such as:

  • What is a fact?
  • Why do facts exist?
  • What facts should you store in Oso Cloud?
  • What facts should you leave in your application database?
  • How do facts fit into other authorization concepts like RBAC, ABAC, and ReBAC?

Introduction

Authorization decisions require data. Some common examples of data that would matter for authorization in a typical application are:

  • Multitenant roles: Bob has the “admin” role at the Acme Organization.
  • Relationships between resources: The "Anvils" repository belongs to the Acme Organization.
  • Resource attributes: The "Roadmap" repository is publicly accessible.
  • Miscellaneous contextual data: The Acme Organization is on the “hobby” pricing tier.

We call this data authorization-relevant data. Not all data in your application is relevant to authorization, but some is: if there was no authorization-relevant data then all users would have identical permissions, because there would be nothing to differentiate them.

For any authorization service to make a decision, it must have access to authorization-relevant data. In order to determine whether Bob can invite new users to the Acme Organization, for example, an authorization service would need to know all of Bob’s roles.

Oso Cloud is a managed authorization service, so it needs a way to express arbitrary data that might be relevant to authorization decisions.

Facts

Oso Cloud represents data as “facts”. Facts are a flexible data model for representing any authorization-relevant data in your application.

An Oso Cloud fact consists of a name and multiple arguments

Facts consist of a name and arguments. Each argument either references a resource in your application (by its type and identifier), or a literal value, such as a string.

When you’re just starting out, you might only care about storing user roles in Oso Cloud, so has_role facts will be the only type of fact that exist in your Oso Cloud environments:


has_role(User:bob, "admin", Organization:acme)
has_role(User:alice, "member", Organization:oso)
has_role(User:2341231, "member", Organization:1231)
...

But has_role is just one common example of a fact. You can use facts to express any type of data in your application. Here is how you would write different types of data as facts in Oso Cloud:

  • Multitenant roles: has_role(User:bob, "admin", Organization:acme)
  • Relationships between resources: has_relation(Repository:anvils, "organization", Organization:acme)
  • Resource attributes: is_public(Repository:roadmap)
  • Miscellaneous contextual data: has_pricing_tier(Organization:acme, "hobby")
💡

It is common for fact names (also called “predicates”) to take the form verb_subject, like has_role, is_public, has_parent, is_active, etc. This makes it a bit clearer that the fact describes its first argument. This is just a convention, however — you can name facts whatever you like!

Using facts in a policy

When your application sends an authorization request, Oso Cloud combines your facts with your policy (written in the Polar language) to determine the result. In other words, your policy defines how your facts should affect authorization decisions.

While writing Polar code, you can reference facts directly in your policy’s authorization rules. Here’s a rule that grants users read permission on any repository repo for which an is_public(repo) fact exists:


# Allow users to read any public repository
has_permission(user: User, "read", repo: Repository) if
# Look for is_public(Repository) facts
is_public(repo);

When asked whether a user can read a repository, the above policy logic says: “yes — if there exists a fact is_public with that repository as its argument”.

Facts can also have the same name as other rules that you write — in this way, the Oso policy engine can be thought of as deriving facts from other facts. For example, we can derive has_role facts for all users marked with a is_superadmin fact. These “derived” has_role facts behave like any other has_role facts that might have been written to Oso Cloud:


# Grant all superadmins the admin role on all organizations
has_role(user: User, "admin", _: Organization) if
is_superadmin(user);
has_permission(user: User, "delete", org: Organization) if
# This will look for `has_role` facts AND rules
has_role(user, "admin", org);

Referencing your application data

Facts reference data in your application using type and identifier pairs, which we often write as User:bob or Organization:acme or Repository:123. These can be any combination of strings: in communicating with Oso Cloud, your applications decide which type names and identifier names to use for particular objects.

Oso Cloud uses the types in these references for policy evaluation. A fact like is_public(Repository:roadmap) will only match in a policy rule if the argument is of type Repository:


has_permission(user: User, "read", repo: Repository) if
# `repo` has type `Repository`, so only look for facts
# that have an argument with the `Repository` type
is_public(repo);

What data should you store as facts in Oso Cloud?

You should store only data that is necessary to perform authorization and is needed by multiple services:

  • If you’re using roles to determine permissions, you should store has_role facts to indicate which users have which roles on which organizations or resources.
  • If you’re using attributes that have global meaning in your application, such as a superadmin flag or banned users, you should store facts such as is_superadmin or is_banned.

Oso Cloud is not designed to store all data in your application. You should not store any more information in Oso Cloud than is necessary to perform authorization. While you might store a fact to express User:123's role at an organization, you would not store facts indicating their email, their name, or their address.

What authorization data should you leave in your application database?

You should leave data in your application database if its primary purpose is to support application functionality:

  • If the data changes frequently, synchronizing it between your database and Oso cloud can create extra overhead and traffic.
  • If you use the data to generate large lists, it can be inefficient to use your authorization service to filter them.
  • If the data only affects authorization for a single service, it doesn't need to be synchronized to a centralized service.
  • If your policy relies on relationships between objects, those relationships are often best modeled as foreign keys in your application database.

Using local application data at authorization time

To use local application data at authorization time, you provide a yaml configuration file to the Oso Cloud client when you initialize it in your application. This file tells the Oso Cloud client how to convert your local data to facts. For example, you might have relationships between objects that only affect authorization in one service, such as the relationship between issues and repositories in a GitHub-style app. Rather than storing all issue → repository relationships inside of Oso Cloud, you could instead tell the client how to determine those relationships from local data.


facts:
has_relation(Issue:_, String:parent, Repository:_):
query: SELECT id, repository FROM issues
sql_types:
Issue: UUID
Repository: UUID

This example tells Oso that has_relation facts that associate issues with their parent repositories are resolved by running a query (SELECT id, repository FROM issues) against your application's database, rather than by looking them up directly in Oso Cloud.

Sending context facts to Oso Cloud at authorization time

You may want to send some authorization-relevant data to Oso Cloud with each authorization request, for instance, data that isn't stored in your application database. This is possible by using context facts, which exist only for the duration of a request.

Situations where context facts make sense include:

  • Information from an external source, like an identity provider (IDP). In your system there may be roles or permissions that exist solely on a user’s authentication token from an identity provider (such as a JWT) — in these cases, you can pass that extra information using context facts like is_admin(user). This lets you avoid synchronizing your IDP information with Oso Cloud.
  • Request-specific context: if you want to protect resources based on ephemeral request properties like IP addresses or time of day, you can pass them as context facts such as is_weekend() or request_came_from_eu().

Facts vs. RBAC, ABAC, ReBAC

Many traditional authorization systems rely on a particular type of data:

  • With role-based access control (RBAC), authorization data takes the form of role assignments, like user bob is an admin for this organization. Roles make it easy to grant a common set of permissions at once — an admin role might imply permission to read, write, and delete.
  • With attribute-based access control (ABAC), authorization data is often structured as an object (e.g. as JSON) and access is granted by comparing the attributes of resources to those of actors (i.e. does actor.org = resource.org?).
  • With relationship-based access control (ReBAC), authorization data takes the form of arbitrary relationships, like document A belongs to folder B. This can support very complex models, but can make simpler things (like role-based and attribute-based access) quite difficult to build.

Facts can express any of these models, and more. You are not forced to choose between RBAC or ABAC or ReBAC when you start using Oso Cloud. With facts, you can build an RBAC system that uses a couple attributes here and there (like is_public). You can start with basic global roles, and over time add complexity that looks more like relationship-based access control.

For examples of the variety of the models supported by facts, check out the modeling building blocks — many of the examples define custom facts used to model a particular use-case, like user groups, public resources, or user-customizable roles.

Facts do not constrain you to one authorization model. While Polar offers resource blocks that make it trivial to write RBAC-like policies, you can use any combination of facts to build any authorization model your applications need. The facts you use can change over time as your application and authorization model grow in complexity.

Summary

  • Facts are how you represent your authorization data in Oso Cloud. They have a name and arguments, and the arguments can reference objects in your application using typed IDs, like User:123.
  • By integrating with Polar policies, facts let you write arbitrary authorization logic over your data.
  • Facts are designed to be flexible. You shouldn't have to fit all of your authorization-relevant data into one specific model (like RBAC, ABAC, or ReBAC systems require).
  • Oso Cloud lets you store your data where it makes sense. Pulling authorization logic out of your application shouldn't also force you to pull your authorization data out of your application.

Talk to an Oso Engineer

If you'd like to learn more about using Oso Cloud in your app or have any questions about this guide, connect with us on Slack. We're happy to help.

Get started with Oso Cloud →