Concepts
Modeling Building Blocks

Modeling Building Blocks

When you add authorization to your app, you'll be writing an authorization model. That model is a way to present the permissions in your app as concepts that will be familiar to your users. For instance, you can tell a particular user, "you have an admin role, which means you can both read and edit documents." The concept is clear to that user, and your implementation also has a clear structure.

This guide will follow our usual model of GitClub — a source code collaboration platform modeled after GitHub and GitLab. See the example application here.

Building Blocks

You don't need to design your authorization model from scratch. All apps use a combination of common authorization schemes. We call these Building Blocks.

This guide introduces a few Building Blocks and presents real-world Patterns that combine these Building Blocks in ways applications can use.

Actors and Resources

All authorization comes down to "who can act on what".

Actors are the who. Most of the time, this is a User, but you might also want to model actions taken by internal ServiceAccounts, or distinguish external users from InternalUsers.

In Oso, you add actors to your model with an actor block:

actor User { }

Resources are the what. Typically, resources are your data models, or application objects that actors are interacting with. For example, in GitClub, users interact with the data models: Organization, Repository, Issue, etc. Most of our authorization models revolve around determining whether a User is allowed to perform some action on one of these.

In Oso, you add resources to your model with resource blocks:

resource Repository {
    ...
}

You'll see the actor and resource types show up when adding data to Oso Cloud.

For example, to tell Oso that the actor "alice" (a User) is an admin, we use the tell API:

CLINodePythonGoRuby
oso-cloud tell is_admin User:alice

Roles

Roles are groups of permissions that can be assigned to users. Most apps have roles. You've seen roles like "member," "owner," or "admin" in other apps. In authorization terms, a role is a set of permissions given to a specific actor (like a user) on a specific resource (like a repository). A user with the "member" role may only have "read" and "write" permissions, but an "admin" may also have a "delete" permission.

Roles are a very general building block that you can use to represent many relationships between actors and resources. Most authorization use cases can be modeled using roles, so it's the first Building Block you should reach for.

Is Oso, you can add roles with resource blocks:

resource Repository {
    permissions = ["read", "edit", "delete"]
    roles = ["reader", "editor", "admin"];

    # permission assignments
    "read" if "reader";
    "edit" if "editor";
    "delete" if "admin";

    # role implications
    "reader" if "editor";
    "editor" if "admin";
}

Role declarations

You define roles by first adding them to resource blocks in your policy

roles = ["admin", "member"];

Permission assignments

Assign permissions to roles by using the shorthand syntax seen above:

"edit" if "editor";

or by writing the equivalent long-form:

has_permission(actor: Actor, "edit", repo: Repository) if
    has_role(actor, "editor", repo);

Role implications

Roles can imply other roles, which means that anyone with a role will additionally have all permissions granted by the implied role.

Add role implications by using the shorthand syntax seen above:

"editor" if "admin";

or by writing the equivalent long-form:

has_role(actor: Actor, "editor", repo: Repository) if
    has_role(actor, "admin", repo);

Role assignments

Assigning roles to users is done by adding data, e.g.

CLINodePythonGoRuby
oso-cloud tell has_role User:alice reader Repository:foo

In Oso, role assignments use the has_role fact, which is a built-in convention in the Polar language. While you hypothetically could assign roles using your own facts like my_has_role(User:alice, "reader", Repository:foo), you would not be able to use resource block syntax to define permission assignments.

Resource Hierarchies

Resources often exist in hierarchies. In those cases, access flows from resources to their subresources. For example, in GitClub your permissions on an Issue are often determined by your role on the parent Repository.

Create hierarchies in Oso by using resource blocks:

resource Organization {
    ...
}

resource Repository {
    ...

    # declares that repositories have an "organization"
    # relationship with organizations
    relations = {
        organization: Organization,
    };

    # role implication using the parent organization
    "reader" if "member" on "organization";
}

The pieces that make up resource hierarchies are:

Assign Permissions

To let authorization logic flow through the resource hierarchy by using the shorthand syntax seen above:

"reader" if "member" on "organization";

or by writing the long-form version:

has_role(actor: Actor, "reader", repo: Repository) if
    org matches Organization and
    has_relation(repo, "organization", org) and
    has_role(actor, "member", org);

Inserting relationship data

Create the hierarchy in Oso by telling us the relevant data:

CLINodePythonGoRuby
oso-cloud tell has_relation Repository:anvil organization Organization:acme

Rather than store all relationship data in Oso Cloud, you can also provide it at authorization-time by using Context Facts.

Actor Hierarchies

Like resources, actors also have hierarchies. If you're a member of a working group, you'll have all of the permissions that the group has.

Create actor hierarchies by relating multiple actors:

actor User { }
actor Group { }

has_role(user: User, role: String, resource: Resource) if
    group matches Group and
    has_group(user, group) and
    has_role(group, role, resource);

Attributes

Attributes describe information about actors and resources. An attribute on a particular actor or resource provides information about that actor or resource. Information from attributes on a specific actor and on a specific resource can then be used to determine whether to give the actor a specific permission on that resource.

In Oso, attributes are represented by having arbitrary facts.

You can tell Oso anything you want! And then use that as an attribute in a policy.

For example, we could say that users can only comment on open issues that they can read:

has_permission(user: User, "comment", issue: Issue) if
    issue_status(issue, "open") and
    has_permission(user, "read", issue);

where we used the issue_status facts as a way to communicate that attribute about the issue.

Finally, we need to tell Oso about that piece of data:

CLINodePythonGoRuby
oso-cloud tell issue_status Issue:490 open

For attributes that reflect application state, it is often better to keep these stored in the application and provide to Oso Cloud at authorization-time by using Context Facts.

However, if you need this application state to be shared between multiple services for authorization, then Oso Cloud is a good place to store the state.

Patterns

Authorization models in real-world applications usually combine several Building Blocks, and there are some common Patterns of how the Building Blocks go together in applications.

Multitenancy

Summary: Your app may host many companies' data. Much of that data will be resources, like documents or code. In that case, it's essential to restrict access to only members of the organization that those resources belong to.

You associate users with one or many organizations through the Roles building block, and grant access to resources within the organization with Resource Hierarchies.

Challenges: Choosing the right roles is a design problem that requires careful thought. You need a small number of meaningful roles in your app. A good starting place is to stick with "admin" and "member".

In GitClub: All users have roles in the organizations they belong to. Users have the "admin" role in organizations they create and "member" in organizations they join.

Use Cases: Nearly all B2B apps start with organization roles. This is one of the simplest ways to build multitenant applications.

Advanced Uses: Users may have roles in many different organizations, like in GitClub. Thankfully, Oso Cloud supports this by default! You can assign users to any number of organizations when inserting has_role facts.

Oso Policy

actor User { }

resource Organization {
    roles = ["admin", "member"];

    "member" if "admin";
}

resource Repository {
    permissions = ["read", "create", "update", "delete"];
    relations = {
        organization: Organization,
    };

    "read"   if "member" on "organization";
    "create" if "admin"  on "organization";
    "update" if "admin"  on "organization";
    "delete" if "admin"  on "organization";
}

CLI Example

oso-cloud tell has_relation Repository:anvil organization Organization:acme
oso-cloud tell has_role User:alice member Organization:acme
oso-cloud authorize User:alice read Repository:anvil

Sharing

Summary: Often, someone will need to grant access on a resource to a specific person. To achieve this we use the Roles building block on that resource. We'll often want to also use roles to control who is allowed to share a resource.

Challenges: When resources are shared with lots of users, you'll often want to be able to see who a resource is shared with.

In GitClub: Repository admins can invite users as external collaborators on a repository. We do this by assigning those users roles on the repository.

Use Cases: Collaborative apps use this pattern. For instance, a Google doc has one owner and can be shared with other users or groups. Apps with file-sharing features will need to use this pattern too.

Advanced Uses: You may need to share a resource with different levels of permission (like "share and grant reading permission" and "share and grant editing permission"). Thankfully, this is very straightforward to implement!

Oso Policy

actor User { }

resource Repository {
    roles = ["reader", "admin"];
    permissions = ["read", "invite"];

    "read" if "reader";
    "invite" if "admin";
}

CLI Example

# Bob is an admin of Repository:anvil so Bob can give Alice the reader role on Repository:anvil
oso-cloud tell has_role User:bob "admin" Repository:anvil
oso-cloud authorize User:bob "invite" Repository:anvil
# The app would insert this on Bob's behalf
oso-cloud tell has_role User:alice "reader" Repository:anvil
oso-cloud authorize User:alice "read" Repository:anvil

Ownership

Summary: Many applications will want to grant additional permissions to the "owner" of a resource. This might be some fixed piece of application-specific data, like the person who opened an issue, or wrote a comment. Or ownership might be something that can be transferred between users. You can implement these scenarios by treating them as a kind of Role.

Challenges: Storing ownership data in Oso Cloud requires you to insert these facts whenever resources are created. If that's unrealistic in your application, you can instead send ownership information as a Context Fact in a request.

In GitClub: Most users are allowed to open issues in a GitClub Repository. However, to close an issue you must either be a maintainer of the repository, or the person who opened it.

This latter case is an example of ownership.

Use Cases: Many collaborative apps use this pattern to associate data with the person who created it.

Advanced Uses: You might want to be able to transfer ownership. This would be an update to remove the role from one person, and assign to another.

Oso Policy

actor User { }

resource Issue {
    roles = ["reader", "admin", "creator"];
    permissions = ["read", "comment", "close"];

    "read" if "reader";
    "comment" if "reader";

    "close" if "creator";
    "close" if "admin";

    "reader" if "admin";
}

CLI Example

# Alice created issue #537
oso-cloud tell has_role User:alice "creator" Issue:537

# Alice can close her own issue
oso-cloud authorize User:alice "close" Issue:537

Groups

Summary: If your customer has a lot of users, it'll be easier for them if they can grant access to entire groups of users, rather than assign them individually.

In Oso you model groups using the Actor Hierarchy building block. With that building block in place, members of a group inherit the group's role assignments.

Challenges: A user needs to inherit all roles from the groups that user belongs to. Make sure that your users have both their individual roles and the roles of their groups.

In GitClub: Members in a GitClub Organization can be assigned to teams. Those teams can be granted permissions on repositories. Each member of a team then has that team's permissions on those repositories.

Use Cases: Groups are useful for collaborative apps like Notion and Google Docs. Any app with an idea of a "team" will probably need groups. Content management systems may need groups, too.

Advanced Uses: You may need nested groups, where a group is itself part of a group. You can do this by writing recursive rules.

Sometimes, you'll assign group membership by a user's external identity, like in Google Groups. You can do this by using Context Facts.

Oso Policy

actor User { }

# A group is a kind of actor
actor Group { }

resource Repository {
    roles = ["reader"];
    permissions = ["read"];

    "read" if "reader";
}

# Actors inherit roles from groups
has_role(user: User, role: String, resource: Resource) if
  has_group(user, group) and
  has_role(group, role, resource);

# Nested groups
has_group(actor: Actor, group: Group) if
    g matches Group and
    has_group(actor, g) and
    has_group(g, group);

CLI Example

oso-cloud tell has_role Group:anvil-readers member Repository:anvil
oso-cloud tell has_group User:alice Group:anvil-readers
oso-cloud authorize User:alice read Repository:anvil

Files/Folders

Summary: If you're granted access to a folder, by default you'll often also have access to that folder's contents. In this example, anyone with a role on a folder also has the same role on any file/folder it contains.

Challenges: Your users may end up with many different ways to access a file. A role on the file itself, a role on the folder that the file is contained in, a role on the organization that owns the file, and so on. For debugging, your system needs a way to display what roles are granting a user access to a particular file.

In GitClub: GitClub source code is organized into files, folders, and repositories. Suppose we want to implement file-level access control on source code, then we would want the ability to inherit access based on the access you have on parent folders or the repository.

Use Cases: Folders let users assign fine-grained permissions in bulk. This is useful for expense reporting apps, reporting, analytics, and project management. CRM apps may have several folders per customer.

Advanced Uses: You may find you need folders inside of other folders, like a filesystem. Implementing this requires recursive rules.

Oso Policy

actor User { }

resource Repository {
    roles = ["reader", "maintainer"];
}

resource Folder {
    roles = ["reader", "writer"];
    relations = {
        repository: Repository,
        folder: Folder,
    };

    "reader" if "reader" on "repository";
    "reader" if "reader" on "folder";
    "writer" if "maintainer" on "repository";
    "writer" if "writer" on "folder";
}

resource File {
    permissions = ["read", "write"];
    relations = {
        folder: Folder,
    };

    "read"  if "reader" on "folder";
    "write" if "writer"  on "folder";
    "read"  if "write";
}

You can either spell out all the role implications in the shorthand syntax:

"reader" if "reader" on "folder";

or by writing a single long-form rule:

has_role(user: User, role: String, folder: Folder) if
    parent_folder matches Folder and
    has_relation(folder, "folder", parent_folder) and
    has_role(user, role, folder);

CLI Example

# Set up the file structure
oso-cloud tell has_relation File:test.py "folder" Folder:tests
oso-cloud tell has_relation Folder:tests "folder" Folder:python
oso-cloud tell has_relation Folder:python "repository" Repository:anvil-py

# Check file permissions
oso-cloud tell has_role User:alice reader Repository:anvil-py
oso-cloud authorize User:alice read File:test.py

Org Charts

Summary: Many companies need their authorization policy to correspond to their org chart. Organizational charts are a kind of Actor Hierarchy. In this example, managers automatically inherit all the permissions of their subordinates.

Challenges: Sometimes organizations have complex relationships beyond "manager" and "reportee." Org charts aren't always perfect trees, either. You can fill these cases in with other authorization patterns.

In GitClub: GitClub doesn't currently use this pattern. If we were to implement a feature like "managers can see analytics on PRs their employees have submitted," we'd use the org chart pattern.

Use Cases: Human resources systems, recruiting and applicant tracking systems, and customer relationship management systems all require this pattern.

Oso Policy

actor User { }
resource Repository { }

# A manager has permission to do anything their workers can.
has_permission(manager: Actor, action: String, resource: Resource) if
  has_relation(manager, "manager", worker) and
  has_permission(worker, action, resource);

CLI Example

# This shows that granting a permission to Bob grants Eve permission,
# since Eve is Bob's manager's manager.
oso-cloud tell has_relation User:bob "manager" User:alice
oso-cloud tell has_relation User:alice "manager" User:eve
oso-cloud tell has_role User:bob "reader" Repository:anvil
oso-cloud authorize User:eve "read" Repository:anvil

Custom Roles

Summary: Some applications need to allow their users to create new roles with customizable permissions. In that case, rather than statically defining the roles in the policy, the application can create these custom roles on the fly.

Challenges: The UI is the challenge here. Make sure it's clear where a user's permissions come from.

Custom roles typically associate permissions with a single resource -- normally an organization. If you want to allow users to specify granular permissions on child resources (e.g. repository-level permissions), you will need to use Resource Hierarchies to imply those permissions.

Use Cases: Whenever your users need to define their own app's access patterns, consider custom roles. For instance, Discord uses custom roles to allow server admins to define moderator roles for their own communities.

Oso Policy

actor User { }
resource Organization { }

# A custom role is defined by the permissions it grants
has_permission(actor: Actor, action: String, org: Organization) if
  role matches Role and
  has_role(actor, role, org) and
  grants_permission(role, action);

CLI Example

# If you have the custom role "viewer", you are allowed to read
oso-cloud tell grants_permission Role:viewer read
# Alice is a viewer
oso-cloud tell has_role User:alice Role:viewer Organization:acme
oso-cloud authorize User:alice read Organization:acme

Default Roles

Summary: Many multitenant applications will use Resource Hierarchies to imply roles on child resources. Although those implications are normally hard-coded, some applications will give users the ability to customize this.

You can achieve this by having a configurable Attribute on the organization which determines what role a user inherits.

In GitClub: Organization admins can choose what repository role a member will inherit by default.

Use Cases: Any application making use of Resource Hierarchies and resource-specific roles at multiple levels of the hierarchy might use this.

Advanced Uses: You might also want to capture that members by default have no role.

Oso Policy

actor User {}

resource Organization {
    roles = ["member", "admin"];
    permissions = ["set_default_role"];

    "set_default_role" if "admin";
}
# Editors of a repository can edit it
resource Repository {
    roles = ["reader", "editor", "admin"];
    permissions = ["write"];
    relations = { organization: Organization };

    "write" if "editor";
}

has_role(actor: Actor, role: String, repo: Repository) if
  org matches Organization and
  has_organization(repo, "organization", org) and
  has_default_role(org, role) and
  has_role(actor, "member", org);

CLI Example

# Members of Acme are editors by default
oso-cloud tell has_default_role Organization:acme editor
# Bob is a member of Acme
oso-cloud tell has_role User:bob member Organization:acme
oso-cloud tell has_relation Repository:anvil organization Organization:acme
oso-cloud authorize User:bob write Repository:anvil

Public or Private Resources

Summary: Some resources are public, meaning they can be read by anyone. A "public" attribute on a resource can be used to indicate that it is public.

In GitClub: The owner of a repository can mark it "public." Public repositories are visible to anyone, but private repositories are only visible to users in the owner's organization.

Use Cases: Apps with shared or collaborative resources—like documents, code, or comments—may need to use a "public" flag.

Oso Policy

actor User { }
resource Repository { }

has_permission(_: Actor, "read", repo: Repository) if
  is_public(repo);

CLI Example

oso-cloud tell is_public Repository:anvil
oso-cloud authorize User:alice read Repository:anvil

User Statuses

Summary: Any additional information about a user, like whether the user's account is active, can be used for additional authorization checks. Such information can be provided as an attribute on the user object.

In GitClub: GitClub doesn't use user statuses.

Use Cases: Apps that require account verification, apps that require paid access to products, and apps grant access based on a user's email all use user statuses.

Oso Policy

actor User { }
resource Repository { }

allow(actor: Actor, action: String, resource: Resource) if
    is_active(actor) and
    has_permission(actor, action, resource);

CLI Example

oso-cloud tell has_permission User:bob delete Repository:legacy
oso-cloud tell is_active User:bob
oso-cloud authorize User:bob delete Repository:legacy

Toggles

Summary: A toggle on a resource can turn on and off certain permissions that a specific role has. The state of the toggle can be provided as an attribute on the resource.

In GitClub: GitClub's "public" flag is a case of a toggle—users can change the public status of repositories they own.

Use Cases: Many apps have "protected" documents or files flagged "important". Document-sharing apps, human resource apps, and infrastructure apps may use this pattern.

Oso Policy

actor User { }
resource Repository { }

has_permission(actor: Actor, "delete", repo: Repository) if
   has_role(actor, "member", repo) and
   is_protected(repo, false);

CLI Example

oso-cloud tell has_role User:alice member Repository:legacy
oso-cloud tell is_protected Repository:legacy Boolean:false
oso-cloud authorize User:alice delete Repository:legacy