Ory Permission Language
Ory Keto (Permissions) uses a relationship-based access control model (ReBAC) in which permissions are derived from the relationships between objects and subjects stored in the system. Ory Permission Language (OPL) is the TypeScript-based language you use to define those relationships and the permission rules that use them to control access.
You use OPL to define a schema that includes permissions 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
In OPL, each class defines a namespace, which represents 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. For example, File:readme is the object, User:alice and
User:bob are the subjects, and viewers and owners are the relations.
User:alice is in viewers of File:readme
User:bob is in owners of File:readme
Multiple subject types
Use union type when a relation can hold subjects of different types:
viewers: (User | Group)[]
This allows writing relation 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 relation 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 permission functions, which return a boolean and are 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, which is 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
this.related.<relation>.includes(ctx.subject) checks whether the subject is directly in relation <relation>.
Inherited membership
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.
Combine 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),
}
Call 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 for a permissions schema model
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)),
}
}