Skip to main content

Ory Permission Language

Ory Permissions uses a relationship-based access control model (ReBAC): permissions are derived from the relationships between objects and subjects stored in the system. OPL is the TypeScript-based language you use to define those relationships and the permission rules that use them to control access.

You write OPL to define things like "who can view a file", "whether a group member inherits access", or "whether owning a folder grants access to its contents". The schema you define is evaluated by the Ory Permissions engine at check time.

Namespaces

Each class in OPL defines a namespace — a type of object in your system, such as a file, folder, organization, or user.

class User implements Namespace {}
class Group implements Namespace {}
class File implements Namespace {}

Every class must implement Namespace.

Relations

In OPL, object refers to the thing being accessed (for example, a File), and subject refers to the entity requesting access (for example, a User). Relations always run in one direction: a subject is in a relation of an object. The related block on a class (the object) declares which relations it can have and what subject types are allowed in each.

import { Namespace } from "@ory/keto-namespace-types"
class User implements Namespace {}

class File implements Namespace {
related: {
viewers: User[]
owners: User[]
}
}

Relations are always arrays because an object can have many subjects. This declaration allows creating relationships like:

User:alice is in viewers of File:readme
User:bob is in owners of File:readme

Here, File:readme is the object, User:alice and User:bob are the subjects, and viewers and owners are the relations.

Multiple subject types

Use a union when a relation can hold subjects of different types:

viewers: (User | Group)[]

This allows writing tuples with either a User or a Group as the subject:

User:alice is in viewers of File:readme
Group:engineering is in viewers of File:readme

Subject-set references

Sometimes you want a relation to include not just individual subjects, but all members of another relation. SubjectSet<T, R> lets you do this — it refers to all subjects in relation R on namespace T.

For example, to allow either individual users or all members of a group to view a file:

class User implements Namespace {}

class Group implements Namespace {
related: {
members: User[]
}
}

class File implements Namespace {
related: {
viewers: (User | SubjectSet<Group, "members">)[]
}
}

This means a viewer can be either a User directly, or any subject in the members relation of a Group. You can then write a tuple that grants access to a whole group at once:

members of Group:engineering is in viewers of File:readme

This means: every subject in the members relation of Group:engineering is a viewer of File:readme.

Permits

The permits block defines permissions — functions that return a boolean, evaluated when a permission check is made. While relations model real-world associations, permissions define application-specific rules built on top of them.

Each permission function receives a Context object as its argument. ctx.subject refers to the entity whose access is being checked — the same subject used in relation tuples.

class User implements Namespace {}

class File implements Namespace {
related: {
viewers: User[]
owners: User[]
}
permits = {
view: (ctx: Context) => this.related.viewers.includes(ctx.subject) || this.related.owners.includes(ctx.subject),
edit: (ctx: Context) => this.related.owners.includes(ctx.subject),
}
}

To check membership within a permission function, OPL provides two methods: includes for direct membership, and traverse for inherited membership through another relation.

Direct membership: includes

this.related.<relation>.includes(ctx.subject) checks whether the subject is directly in relation <relation>.

Inherited membership: traverse

this.related.<relation>.traverse(fn) takes a function and calls it for each object in <relation>. It returns true if the function returns true for any of them.

The function receives each object in the relation and can check either a relation (g.related.<relation>.includes(...)) or call another permission (g.permits.<permission>(ctx)).

class Group implements Namespace {
related: {
members: User[]
}
}

class File implements Namespace {
related: {
viewerGroups: Group[]
}
permits = {
view: (ctx: Context) => this.related.viewerGroups.traverse((g) => g.related.members.includes(ctx.subject)),
}
}

view is granted if the subject is a member of any group in viewerGroups.

Boolean operators

Combine checks with ||, &&, and !:

permits = {
view: (ctx: Context) => this.related.viewers.includes(ctx.subject) || this.related.owners.includes(ctx.subject),

restricted: (ctx: Context) => this.related.allowlist.includes(ctx.subject) && !this.related.blocklist.includes(ctx.subject),
}

Calling another permission

A permission can call another permission defined on the same namespace:

isAdmin: (ctx: Context) => this.related.admins.includes(ctx.subject),
edit: (ctx: Context) => this.permits.isAdmin(ctx) || this.related.owners.includes(ctx.subject),

Complete example

class User implements Namespace {}

class Group implements Namespace {
related: {
members: (User | SubjectSet<Group, "members">)[] // a member can be a User, or all members of another Group (enables nested groups)
}
}

class Folder implements Namespace {
related: {
viewers: (User | SubjectSet<Group, "members">)[] // a viewer can be a User directly, or all members of a Group
}
permits = {
view: (ctx: Context) => this.related.viewers.includes(ctx.subject),
}
}

class File implements Namespace {
related: {
parents: Folder[] // the Folders this file is nested under, used to inherit permissions
viewers: (User | SubjectSet<Group, "members">)[]
owners: (User | SubjectSet<Group, "members">)[]
}
permits = {
view: (ctx: Context) =>
this.related.viewers.includes(ctx.subject) ||
this.related.owners.includes(ctx.subject) ||
this.related.parents.traverse((p) => p.permits.view(ctx)), // grants view if the subject has view permission on any parent Folder
edit: (ctx: Context) => this.related.owners.includes(ctx.subject),
}
}

This schema models:

  • A File can have individual users or group members as viewers or owners
  • A Group can contain other groups, enabling nested membership
  • A File can inherit view permission from its parent Folder