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?
- 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.
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 repositoryhas_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 organizationshas_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 the data that is necessary to perform authorization:
- 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 your policy relies on relationships between objects, then you should store
has_relation
facts for each of those relationships. In GitHub for example, admins at an organization inherit admin permissions on all of that organization’s repositories. Such a policy would require storinghas_relation
facts to connect repositories to their organizations. - If your policy uses any attributes on your resources, you’ll need to store those as facts like
is_public
oris_superadmin
.
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.
Sending context facts at authorization time
Sometimes it makes sense not to store all authorization-relevant data in Oso Cloud, and instead send it along with each authorization request. This is possible by using context facts, which exist only for the duration of a request.
There are a number of situations where context facts make sense:
- 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()
orrequest_came_from_eu()
. - Data that only matters in a single app: sometimes it’s not worth centralizing every bit of authorization data from every application. You might have relationships between objects that only affect authorization in one application. Sending context facts to represent this data lets you avoid synchronizing it to Oso Cloud. For example, when authorizing a user to close an issue in a GitHub-style app, you might send the issue’s relationship with it’s repository:
has_relation(issue, "repository", repository)
. That way, you avoid having to store all issue → repository relationships inside of Oso Cloud.
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. Pulling authorization logic out of your application shouldn’t force you to fit all of your authorization-relevant data into one specific format (like RBAC, ABAC, or ReBAC systems do).
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.