Skip to main content

Ory Permission Language

OPL is a TypeScript-based language for defining permission models in Ory Permissions.

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

The related block defines who can be associated with an object. Each entry names a relation and declares what subject types it holds.

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

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

SubjectSet<T, R> refers to the subjects of relation R on namespace T. Use it when a relation can hold subjects from another namespace's relation — for example, a group's members:

viewers: (User | SubjectSet<Group, "members">)[]

This allows writing tuples that point to a group's members rather than a user directly:

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

A subject can now be either a User directly, or any member of the Group:engineering.

Permits

The permits block defines computed relations — boolean functions the engine evaluates during a check.

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),
}
}

Direct membership: includes

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

Following subject-sets: traverse

this.related.x.traverse(g => ...) iterates over the objects in relation x and evaluates the inner expression for each one.

Check via a relation on the related object:

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.

Check via a permission on the related object:

class Group implements Namespace {
related: {
members: User[]
}
permits = {
isMember: (ctx: Context) => this.related.members.includes(ctx.subject),
}
}

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

Same result, but delegating to a named permission on Group instead of accessing the relation directly. Use this when the permission logic on the related namespace is more complex than a single includes check.

Boolean operators

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

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:

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

Complete example

class User implements Namespace {}

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

class Folder implements Namespace {
related: {
viewers: (User | SubjectSet<Group, "members">)[]
}
permits = {
view: (ctx: Context) => this.related.viewers.includes(ctx.subject),
}
}

class File implements Namespace {
related: {
parents: Folder[]
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)),
edit: (ctx: Context) => this.related.owners.includes(ctx.subject),
}
}

This schema models:

  • Direct access via viewers and owners
  • Group-based access via SubjectSet<Group, "members">
  • Inherited access from parent folders via traverse