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
viewersandowners - Group-based access via
SubjectSet<Group, "members"> - Inherited access from parent folders via
traverse
