Skip to main content

Create a permission model

This section guides you through creating your first permission model using the Ory Permission Language (OPL).

What is a permission model?

A permission model is a set of rules that define which relations are checked in the database during a permission check.

Permission checks are answered based on:

  • The data available in the database, for example: User:Bob is owner of Document:X
  • Permission rules, for example: "All owners of a document can view it".

When you ask Ory Permissions: is User:Bob allowed to view on Document:X, the system checks up how Bob could have the view permission, and then checks if Bob is owner of the document X. The permission model tells Ory Permissions what to check in the database.

How to define a permission model

Designing a permission model is a complex task. Ory Permissions and the Ory Permission Language, as a subset of TypeScript, provide a streamlined approach to permissions with the benefit of being processed by a fast, global permission engine.

Just as there is no single approach for programming, there is no universally applicable guide for constructing a permission model. Nevertheless, the following iterative process can be a good starting point:

  1. Create a list of objects. Objects are the entities that you want to manage access for.
  2. Make a list of relationships each object has to other objects. In a database, those would be associations expressed with foreign keys.
  3. Define each relation in the OPL.
  4. Make a list of permissions that you want to check.
  5. Define each permission in the OPL.
  6. Test your permission model.

Example: document store

To guide you through the process of defining a permission model, this example will be used:

  • A user can be the owner, editor, or viewer of a document.
  • The owner of a document is also an editor of that document.
  • An editor of a document is also a viewer of that document.
  • Documents can be arranged into a hierarchy of folders.
  • Users that can view the parent folder in the hierarchy can also view all folders and documents the parent folder contains.

Create a list of objects and subjects

In the first step, look at the initial list of assumptions and identify all objects and subjects:

  • A user can be the owner, editor, or viewer of a document.
  • The owner of a document is also an editor of that document.
  • An editor of a document is also a viewer of that document.
  • Documents can be arranged into a hierarchy of folders.
  • Users that can view the parent folder in the hierarchy can also view all folders and documents the parent folder contains.

After highlighting, you can identify the subject, User, and two objects: Document and Folder.

This list translates into the first version of the permission model.

permissions-v1.ts
import { Namespace, Context } from "@ory/keto-namespace-types"

class User implements Namespace {}
class Document implements Namespace {}
class Folder implements Namespace {}
tip

In Ory Permissions, objects and subjects you identified are namespaces. They are declared as classes in the Ory Permission Language. The convention is to name these classes like TypeScript classes, starting with a capital letter and and in the singular form, for example Document instead of documents.

Determine relationships between objects

Read through the initial assumptions point by point to determine the list of relationships:

  • The goal is to model user access to documents such that users can be the owner, editor, or viewer of a document.

  • Every owner is also an editor of a document and every editor is also a viewer of a document. Furthermore, documents can be put into a hierarchy of folders. If a user can view the parent folder, they can also view all folders and documents the parent folder contains.

This is the complete matrix of relationships presented on a single diagram:

Define relationships in the OPL

Next, add each relation to the permission config.

tip

In Ory Permissions, relationships are declared inside the corresponding class in the Ory Permission Language. Ory Permissions only has many-to-many relationships between objects and subjects. To reflect this in OPL, pluralize the relation name, for example, viewers instead of viewer.

permissions-v2.ts
import { Namespace, Context } from "@ory/keto-namespace-types"

class User implements Namespace {}

class Document implements Namespace {
related: {
owners: User[]
editors: User[]
viewers: User[]
parents: Folder[]
}
}

class Folder implements Namespace {
related: {
owners: User[]
editors: User[]
viewers: User[]
parents: Folder[]
}
}

List permissions to check for each object

When you perform an action on behalf of a user, you should check for a specific permission, such as view or edit, instead of a relationship, such as owner. The concrete permission is then still checked against the relationships. For example, if owners can view a file and the view permission is checked, then the owners relation is looked up in the relationships database.

Add these permissions to the model to express what the application needs. For our document storage, you have these permissions:

  • view a document if the user is a viewer, editor, or owner of the document; or if the user can view the parent folder
  • edit a document if the user is a editor, or owner of the document; or if the user can edit the parent folder
  • delete a document if the user is an owner of the document; or if the user can delete the parent folder
  • share a document if the user is an owner of the document; or if the user can share the parent folder
  • delete a folder if the user is an owner of the folder; or if the user can delete the parent folder
  • share a folder if the user is an owner of the folder; or if the user can share the parent folder

Define permissions in the OPL

The permissions are expressed in the OPL as TypeScript functions that take a context containing the subject and that answer permission checks based on the relationships the object has to the subject.

tip

The permissions from the description are declared as functions inside the permits property of the corresponding class in the OPL.

Let's see how the permissions translate into code:

  • view a document if the user is a viewer, editor, or owner of the document or if the user can view the parent folder

    permissions-v3.ts
    import { Namespace, Context } from "@ory/keto-namespace-types"

    class User implements Namespace {}

    class Document implements Namespace {
    related: {
    owners: User[]
    editors: User[]
    viewers: User[]
    parents: Folder[]
    }

    permits = {
    view: (ctx: Context): boolean =>
    this.related.viewers.includes(ctx.subject) ||
    this.related.editors.includes(ctx.subject) ||
    this.related.owners.includes(ctx.subject) ||
    this.related.parents.traverse((parent) => parent.permits.view(ctx)),
    }
    }

    class Folder implements Namespace {
    related: {
    owners: User[]
    editors: User[]
    viewers: User[]
    parents: Folder[]
    }
    }
  • Code for the remaining permissions:

    permissions-v4.ts
    import { Namespace, Context } from "@ory/keto-namespace-types"

    class User implements Namespace {}

    class Document implements Namespace {
    related: {
    owners: User[]
    editors: User[]
    viewers: User[]
    parents: Folder[]
    }

    permits = {
    view: (ctx: Context): boolean =>
    this.related.viewers.includes(ctx.subject) ||
    this.related.editors.includes(ctx.subject) ||
    this.related.owners.includes(ctx.subject) ||
    this.related.parents.traverse((parent) => parent.permits.view(ctx)),
    edit: (ctx: Context): boolean =>
    this.related.editors.includes(ctx.subject) ||
    this.related.owners.includes(ctx.subject) ||
    this.related.parents.traverse((parent) => parent.permits.edit(ctx)),
    delete: (ctx: Context): boolean =>
    this.related.owners.includes(ctx.subject) || this.related.parents.traverse((parent) => parent.permits.delete(ctx)),
    share: (ctx: Context): boolean =>
    this.related.owners.includes(ctx.subject) || this.related.parents.traverse((parent) => parent.permits.share(ctx)),
    }
    }

    class Folder implements Namespace {
    related: {
    owners: User[]
    editors: User[]
    viewers: User[]
    parents: Folder[]
    }

    permits = {
    delete: (ctx: Context): boolean =>
    this.related.owners.includes(ctx.subject) || this.related.parents.traverse((parent) => parent.permits.delete(ctx)),
    share: (ctx: Context): boolean =>
    this.related.owners.includes(ctx.subject) || this.related.parents.traverse((parent) => parent.permits.share(ctx)),
    }
    }

Optional: refactoring

Notice that the permission model you created is hierarchical. This means that everybody who can view can also edit documents, and everybody that can edit can also delete and share.

You can refactor the view permission like this:

  view: (ctx: Context): boolean =>
this.related.viewers.includes(ctx.subject) ||
- this.related.editors.includes(ctx.subject) ||
- this.related.owners.includes(ctx.subject) ||
- this.related.parents.traverse((parent) => parent.permits.view(ctx))
+ this.permits.edit(ctx)

When you apply this to the entire config, you get this code:

permissions-v5.ts
import { Namespace, Context } from "@ory/keto-namespace-types"

class User implements Namespace {}

class Document implements Namespace {
related: {
owners: User[]
editors: User[]
viewers: User[]
parents: Folder[]
}

permits = {
view: (ctx: Context): boolean =>
this.related.viewers.includes(ctx.subject) ||
this.permits.edit(ctx),
edit: (ctx: Context): boolean =>
this.related.editors.includes(ctx.subject) ||
this.permits.share(ctx),
delete: (ctx: Context): boolean =>
this.permits.share(ctx),
share: (ctx: Context): boolean =>
this.related.owners.includes(ctx.subject) || this.related.parents.traverse((parent) => parent.permits.share(ctx)),
}
}

class Folder implements Namespace {
related: {
owners: User[]
editors: User[]
viewers: User[]
parents: Folder[]
}

permits = {
delete: (ctx: Context): boolean =>
this.permits.share(ctx),
share: (ctx: Context): boolean =>
this.related.owners.includes(ctx.subject) || this.related.parents.traverse((parent) => parent.permits.share(ctx)),
}
}

Whether or not this refactoring makes sense in your application depends on your requirements, of course. This kind of refactoring is possible because the permission config is essentially just code. This means that you can apply the same techniques to your permission config as you apply to your code in order to create well-structured and easily-maintainable software systems.

Test permissions

It's important to test your permission model. To test the permissions manually, you can create relationships and check permissions through the API or SDK.

For continuous testing, we recommend the following best practices:

  • Automate testing your permission model. Write a test that inserts the relationships and checks the permissions through the SDK. Use your preferred programming language.
  • For complex permission model changes, use a separate Ory Network project. Each Ory Network project has an isolated permission model, so you can iterate on and test your changes on a test project and deploy the changes only when all tests pass.