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 union type 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<N, R> refers to all subjects in relation R on namespace N. It lets you use a specific relation of another
namespace as a subject in a relation tuple.
This is especially useful when you need to distinguish between different roles within the same namespace. For example, suppose a
File can be shared with all members of a group, or just its admins:
class Group implements Namespace {
related: {
members: User[]
admins: User[]
}
}
class File implements Namespace {
related: {
viewers: (User | SubjectSet<Group, "members"> | SubjectSet<Group, "admins">)[]
}
}
When writing tuples, you specify exactly which relation of the group to use as the subject:
members of Group:engineering is in viewers of File:file1
admins of Group:engineering is in viewers of File:file2
File:file1 is accessible to all members of the engineering group, while File:file2 is only accessible to admins.
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
This schema models a file system where files and folders are organized into a hierarchy. Groups have two roles — members and
admins — and access can be granted to each role independently. Folders can be nested inside other folders, and both files and
folders inherit view and edit permissions from their parents. Only owners can edit; members of a group can view but not own.
class User implements Namespace {}
class Group implements Namespace {
related: {
// a member can be a User, or all members of another Group (enables nested groups)
members: (User | SubjectSet<Group, "members">)[]
admins: User[]
}
}
class Folder implements Namespace {
related: {
// parent folders this folder is nested under; view and edit permissions are inherited from them
parents: Folder[]
// viewers can be individual users, group members, or group admins
viewers: (User | SubjectSet<Group, "members"> | SubjectSet<Group, "admins">)[]
// only individual users or group admins can own a folder
owners: (User | SubjectSet<Group, "admins">)[]
}
permits = {
view: (ctx: Context) =>
this.related.viewers.includes(ctx.subject) ||
this.related.owners.includes(ctx.subject) ||
// grants view if the subject has view permission on any parent Folder
this.related.parents.traverse((p) => p.permits.view(ctx)),
edit: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
// grants edit if the subject has edit permission on any parent Folder
this.related.parents.traverse((p) => p.permits.edit(ctx)),
}
}
class File implements Namespace {
related: {
// Folders this file is nested under; view and edit permissions are inherited from them
parents: Folder[]
// viewers can be individual users, group members, or group admins
viewers: (User | SubjectSet<Group, "members"> | SubjectSet<Group, "admins">)[]
// only individual users or group admins can own a file
owners: (User | SubjectSet<Group, "admins">)[]
}
permits = {
view: (ctx: Context) =>
this.related.viewers.includes(ctx.subject) ||
this.related.owners.includes(ctx.subject) ||
// grants view if the subject has view permission on any parent Folder
this.related.parents.traverse((p) => p.permits.view(ctx)),
edit: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
// grants edit if the subject has edit permission on any parent Folder
this.related.parents.traverse((p) => p.permits.edit(ctx)),
}
}
