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:
- Create a list of objects. Objects are the entities that you want to manage access for.
- Make a list of relationships each object has to other objects. In a database, those would be associations expressed with foreign keys.
- Define each relation in the OPL.
- Make a list of permissions that you want to check.
- Define each permission in the OPL.
- 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 adocument
. - The owner of a
document
is also an editor of thatdocument
. - An editor of a
document
is also a viewer of thatdocument
. Documents
can be arranged into a hierarchy offolders
.Users
that can view the parentfolder
in the hierarchy can also view allfolders
anddocuments
the parentfolder
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.
import { Namespace, Context } from "@ory/keto-namespace-types"
class User implements Namespace {}
class Document implements Namespace {}
class Folder implements Namespace {}
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 todocuments
such thatusers
can be theowner
,editor
, orviewer
of adocument
.Every owner is also an editor of a
document
and every editor is also a viewer of adocument
. Furthermore,documents
can be put into a hierarchy offolders
. If a user can view the parentfolder
, they can also view allfolders
anddocuments
the parentfolder
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.
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
.
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 aviewer
,editor
, orowner
of the document; or if the user canview
the parent folderedit
a document if the user is aeditor
, orowner
of the document; or if the user canedit
the parent folderdelete
a document if the user is anowner
of the document; or if the user candelete
the parent foldershare
a document if the user is anowner
of the document; or if the user canshare
the parent folderdelete
a folder if the user is anowner
of the folder; or if the user candelete
the parent foldershare
a folder if the user is anowner
of the folder; or if the user canshare
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.
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 aviewer
,editor
, orowner
of the document or if the user canview
the parent folderpermissions-v3.tsimport { 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.tsimport { 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:
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.