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 (they look a bit like function calls). Each argument either references a resource in your application (by its type and identifier), or a literal value, such as a string.

💡

Facts can have up to 5 arguments, but the vast majority of will have 1-3.

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_object, like has_role, is_public. This way you can "read" a fact like "<first arg> <predicate> <remaining args>", e.g. "Alice has_role admin". 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 fact exists:


# Allow users to read any public repository
has_permission(user: User, "read", repo: Repository) if
# Look for an "is_public" fact with the specified repository as its argument.
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 an 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 values 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);

For more information about representing application data as facts, read Mapping relational data to facts

Where should you store authorization-relevant data?

Most data should stay in your application databases. We recommend only storing authorization-relevant data directly as facts in Oso Cloud if:

  • It affects authorization for multiple services in your application or
  • Your Local Authorization queries are slow
    💡

    Facts are optimized for authorization decisions, so lookups in Oso Cloud are often faster than an analogous query in a relational or NoSQL database

Typically this includes things like organization-level roles and global attributes.

Although there's no hard-and-fast rule that dictates how to manage every specific piece of authorization-relevant data, there are some guidelines you can follow to help you decide:

If the datathen you should
Affects authorization for a single serviceleave it in the application database
Changes frequentlyleave it in the application database
Has high cardinalityleave it in the application database
Affects authorization for multiple servicesstore it in Oso Cloud
Comes from the environmentsend it as context at request time

Some examples of each follow.

Data you should leave in your application database

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

  • Only affects authorization for a single service
    • the owner of an issue
    • relationships between objects managed by the service
  • Changes frequently
    • CI job IDs
  • Has high cardinality
    • files in a large catalog

Data you should store in Oso Cloud

You should store in Oso Cloud only data that is necessary to perform authorization for 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.

Data you should send to Oso Cloud with each authorization request

You may have data that isn't stored in your database, but still affects authorization. You can incorporate this data 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. 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.

Context fact syntax is language-dependent. For more details, check the relevant SDK docs.

Managing facts that you store in Oso Cloud

It's important to keep any data that you store in Oso Cloud in sync with your application database. Oso Cloud exposes a fact management API with HTTP endpoints that let you create and delete facts. Your applications call these APIs via the appropriate SDK on any user action that changes authorization-related data that you choose to centralize.

Some examples of scenarios where a GitHub-style application might insert or delete facts:

  • When an admin invites a collaborator to a repository, the app inserts a has_role fact, like has_role(User{"some_coder"}, "collaborator", Repo{"anvils"}).
  • When a user leaves an organization, the app deletes the has_role fact linking them with that organization.

The fact management API also provides endpoints for updating and deleting facts in bulk. You can use these endpoints to initialize facts from an existing database, or to do periodic asynchronous synchronization.

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 use 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.

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 don'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 in your application database or centralize it in Oso Cloud, whichever makes more sense for 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 →