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.
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(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);
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 data | then you should |
---|---|
Affects authorization for a single service | leave it in the application database |
Changes frequently | leave it in the application database |
Has high cardinality | leave it in the application database |
Affects authorization for multiple services | store it in Oso Cloud |
Comes from the environment | send 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
oris_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(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()
.
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, likehas_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.