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