From 3cb1ffbda795b6e3ef2e8e05a5d648499245aa94 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Tue, 26 May 2026 08:14:41 +0200 Subject: [PATCH 1/3] docs: rewrite OPL as practical guide --- .../reference/ory-permission-language.mdx | 339 +++++------------- 1 file changed, 98 insertions(+), 241 deletions(-) diff --git a/docs/keto/reference/ory-permission-language.mdx b/docs/keto/reference/ory-permission-language.mdx index 744ae32b4b..4cc8de4010 100644 --- a/docs/keto/reference/ory-permission-language.mdx +++ b/docs/keto/reference/ory-permission-language.mdx @@ -1,324 +1,181 @@ --- id: ory-permission-language -title: Ory Permission Language specification +title: Ory Permission Language sidebar_label: Ory Permission Language --- -Enforcing fine-grained permissions is a critical building block of mature technology solutions that protect privacy and identity -in the information age. Several proprietary languages used to represent permission already exist, such as Rego or Casbin. Most -permissions are defined by developers who are likely familiar with Web technologies like JavaScript or Typescript. There is a need -for a developer-friendly configuration language for permissions that has a learning curve small enough so that most developers can -understand and use it with minimal effort. To fulfill this need, we defined the permissions configuration language as a subset of -the most common general-purpose programming language: JavaScript/TypeScript. +OPL is a TypeScript-based language for defining permission models in Ory Permissions. -The Ory Permission Language is a syntactical subset of TypeScript. Along with type definitions for the syntax elements of the -language (such as `Namespace` or `Context`), users can get context help from their IDE while writing the configuration. +## Namespaces -## Notation +Each `class` in OPL defines a namespace — a type of object in your system, such as a file, folder, organization, or user. -The syntax is specified using the Extended Backus-Naur Form (EBNF): - -```ebnf -Production = production_name "=" [ Expression ] "." . -Expression = Alternative { "|" Alternative } . -Alternative = Term { Term } . -Term = production_name | token [ "…" token ] | Group | Option | Repetition . -Group = "(" Expression ")" . -Option = "[" Expression "]" . -Repetition = "{" Expression "}" . -``` - -Productions are expressions constructed from terms and the following operators, in increasing precedence: - -```ebnf -| alternation -() grouping -[] option (0 or 1 times) -{} repetition (0 to n times) +```ts +class User implements Namespace {} +class Group implements Namespace {} +class File implements Namespace {} ``` -Lowercase production names are used to identify lexical tokens. Non-terminals are in CamelCase. Lexical tokens are enclosed in -double quotes `""` or single quotes `''`. - -The form `a … b` represents the set of characters from a through b as alternatives. The horizontal ellipsis `…` is also used -elsewhere in the spec to informally denote various enumerations or code snippets that are not further specified. - -## Configuration text representation - -The configuration is encoded in UTF-8. - -## Lexical elements +Every class must implement `Namespace`. -### Comments +## Relations -1. Line comments start with the character sequence `//` and stop at the end of the line. -2. General comments start with the character sequence `/*` and stop with the first subsequent character sequence `*/`. -3. Documentation comments start with the character sequence `/**` and stop with the first subsequent character sequence `*/`. +The `related` block defines who can be associated with an object. Each entry names a relation and declares what subject types it +holds. -### Identifiers - -Identifiers name program entities such as variables and types. An identifier is a sequence of one or more letters and digits. The -first character in an identifier must be a letter. - -```ebnf -identifier = letter { letter | digit } . -digit = "0" … "9" . -letter = "A" … "Z" | "a" … "z" | "_" . +```ts +class File implements Namespace { + related: { + viewers: User[] + owners: User[] + } +} ``` -### String literals +Relations are always arrays because an object can have many subjects. This declaration allows creating relationships like: -String literals represent string constants as sequences of characters. - -```ebnf -string_lit = single_quoted | double_quoted . -single_quoted = "'" identifier "'" . -double_quoted = '"' identifier '"' . +```keto-natural +User:alice is in viewers of File:readme +User:bob is in owners of File:readme ``` -### Keywords - -The configuration language has the following keywords: - -- `class` -- `implements` -- `related` -- `permits` -- `this` -- `ctx` -- `id` -- `imports` -- `exports` -- `as` - -### Builtin Types - -The following types are built in: +### Multiple subject types -- `Context` -- `Namespace` -- `Namespace[]` -- `boolean` -- `string` -- `SubjectSet` +Use a union when a relation can hold subjects of different types: -In TypeScript, they would be defined as follows: - -```typescript -type Context = { subject: never } - -interface Namespace { - related?: { [relation: string]: Namespace[] } - permits?: { [method: string]: (ctx: Context) => boolean } -} +```ts +viewers: (User | Group)[] +``` -interface Array { - includes(element: Namespace): boolean - traverse(iteratorfn: (element: Namespace) => boolean): boolean -} +This allows writing tuples with either a `User` or a `Group` as the subject: -type SubjectSet = A["related"][R] extends Array ? T : never +```keto-natural +User:alice is in viewers of File:readme +Group:engineering is in viewers of File:readme ``` -### Operators - -The following character sequences represent boolean operators: - -| Operator | Signature | Semantic | -| -------- | -------------- | ------------------------------------ | -| `&&` | _x_ `&&` _y_ | true iff. both _x_ and _y_ are true | -| `\|\|` | _x_ `\|\|` _y_ | true iff. either _x_ or _y_ are true | -| `!` | `!` _x_ | true iff. _x_ is false | - -The following character sequences represent miscellaneous operators: - -| Operator | Example | Semantic | -| -------- | ---------------------------------------- | ----------------------------------------------------------------- | -| `(`, `)` | `x()` | call function `x` | -| `[]` | `T[]` | `T` is an array type | -| `<`, `>` | `SubjectSet` | Type reference to the "members" relation of the "Group" namespace | -| `{`, `}` | `class x {}` | Scope delimiters | -| `=>` | `list.transitive(el => f(el))` | Lambda definition token. | -| `.` | `this.x` | Property traversal token | -| `:` | `relation: type` | Relation/type separator token | -| `=` | `permits = {...}` | Assignment token | -| `,` | `{x: 1, y: 2}` | Property separator | -| `'` | `'string'` | Single quoted string literal | -| `"` | `"string"` | Double quoted string literal | -| `*` | `import * from @ory/permission-language` | Import glob | -| `\|` | `(User \| Group)[]` | Type union | - -## Statements - -### Type declaration - -The top level of the configuration consists of a list of `class` declarations for each namespace. Each `class` consists of -relation declarations and permission declarations. - -```ebnf -Config = [ ClassDecl ] . -ClassDecl = "class" identifier "implements" "Namespace" "{" ClassSpec "}" . -ClassSpec = [ RelationDecls ] | [ PermissionDefns] . -``` +### Subject-set references -The following example declares the type `User`. +`SubjectSet` 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: ```ts -class User implements Namespace {} +viewers: (User | SubjectSet)[] ``` -### Relation declaration - -The `related` section of type declarations defines relations. Unlike regular TypeScript, `RelationName` must be a unique -identifier used as the relation strings in the tuples. The `TypeName` must be the name of another `type` that is defined above or -below (in TypeScript: a class that implements `Namespace`). +This allows writing tuples that point to a group's members rather than a user directly: -Type unions (`|`) can be used to denote that a relation can have subjects of multiple types, e.g., -`viewers: (User | SubjectSet)[]`, meaning that the subject of the "viewer" relation can be either a "User", or a -subject set "Group#members". - -```ebnf -RelationDecls = "related" "=" "{" { RelationName ":" ArrayType } "}" . -RelationName = identifier . -ArrayType = RelationType | ( "(" RelationType { "|" RelationType } ")" ) "[]" . -RelationType = SubjectType | SubjectSetType . -SubjectType = TypeName . -SubjectSetType = "SubjectSet" "<" TypeName, string_lit ">" . -TypeName = identifier . +```keto-natural +Group:engineering#members is in viewers of File:readme ``` -Note that all relations are defined as array types `T[]` because there are naturally only many-to-many relations in Keto. +A subject can now be either a `User` directly, or any member of the `Group:engineering`. -The following declares a type `Document` with three relations: `owners` and `viewers`, both of which have `users` as subjects. -Additionally, the relation `parent` has type `Document`. +## Permits -```ts -class User {} +The `permits` block defines computed relations — boolean functions the engine evaluates during a check. -class Document { - related = { - parents: Document[] - owners: User[] +```ts +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), } } ``` -### Permission definition - -Permissions are defined as functions within class declarations that take a parameter `ctx` of type `Context` and evaluate to a -boolean `true` or `false`. +### Direct membership: `includes` -The type annotations for `ctx` and the return value are optional. +`this.related.x.includes(ctx.subject)` checks whether the subject is directly in relation `x`. -```ebnf -PermissionDefns = "permits" "=" "{" Permission [ "," Permission ] "}" . -Permission = PermissionSign "=>" PermissionBody . -PermissionSign = PermissionName ":" "(" "ctx" [ ":" "Context" ] ")" [ ":" "boolean" ] . -PermissionName = identifier . -``` +### Following subject-sets: `traverse` -The `ctx` object is a fixed parameter that contains the `subject` for which the permission check should be conducted: +`this.related.x.traverse(g => ...)` iterates over the objects in relation `x` and evaluates the inner expression for each one. ```ts -ctx = { subject: "some_user_id" } -``` +class Group implements Namespace { + related: { + members: User[] + } +} -```ebnf -PermissionBody = ( "(" PermissionBody ")" ) | ( "!" PermissionBody ) | ( PermissionCheck | { Operator PermissionBody } ) . -Operator = "||" | "&&" . -PermissionCheck = TransitiveCheck | IncludesCheck . +class File implements Namespace { + related: { + viewerGroups: Group[] + } + permits = { + view: (ctx: Context) => this.related.viewerGroups.traverse((g) => g.related.members.includes(ctx.subject)), + } +} ``` -The body of a permission check is either one of: +`view` is granted if the subject is a member of any group in `viewerGroups`. -- a `IncludesCheck`, a check that something is in a set, e.g., `this.related.viewers.includes(ctx.subject)`: +### Boolean operators - ```ebnf - IncludesCheck = Var "." "related" "." RelationName "." "includes" "(" "ctx" "." "subject" ")" . - Var = identifier . - ``` +Combine checks with `||`, `&&`, and `!`: -- a `TranstitiveCheck`, a call to a permission on a relation, e.g., `this.related.parents.transitive(p => p.permits.view(ctx))`: - - ```ebnf - TransitiveCheck = "this" "." "related" "." RelationName "." "transitive" "(" Var "=>" ( PermissionCall | IncludesCheck ) ")" . - PermissionCall = Var "." "permits" "." PermissionName "(" "ctx" ")" . - ``` - -## Implementation notes - -`IncludeCheck` and `TransitiveCheck` translate to Zanzibar concepts as follows: - -| Keto Config | Zanzibar AST | -| --------------------------------------------------------- | ------------------------------------------------------------------------------------ | -| `this.related.R.includes(ctx.subject)` | `computed_userset { relation: "R" } }` | -| `this.related.R.transitive(x = x.permits.P(ctx.subject))` | `tuple_to_userset { tupleset { relation: "R" } computed_userset { relation: "P" } }` | +```ts +view: (ctx: Context) => + this.related.viewers.includes(ctx.subject) || + this.related.owners.includes(ctx.subject), -## Type checking +restricted: (ctx: Context) => + this.related.allowlist.includes(ctx.subject) && + !this.related.blocklist.includes(ctx.subject), +``` -The following type checks are performed once the config is fully parsed: +### Calling another permission -- Given a `TypeName` as `X` (e.g., in `RelationDecls`), we check that there exists a class declaration for `X`. -- Given a `SubjectSetType` as `SubjectSet`, we check that `R` is a relation defined for `T`. -- Given an `IncludesCheck` as `this.related.R.includes(ctx.subject)`, we check that - - `R` is a relation defined for the current namespace. -- Given a `TransitiveCheck` as `this.related.R.transitive(x = x.permits.P(ctx.subject))`, we check that - - `R` is a relation defined for the current namespace and that - - `P` is a permission defined for all types referenced by `R`. -- Given a `TransitiveCheck` as `this.related.R.transitive(x = x.related.S.includes(ctx.subject))`, we check that - - `R` is a relation defined for the current namespace and that - - `S` is a relation defined for all types referenced by `R`. +A permission can call another permission defined on the same namespace: -## Examples +```ts +edit: (ctx: Context) => this.related.owners.includes(ctx.subject), +admin: (ctx: Context) => this.permits.edit(ctx) && this.related.admins.includes(ctx.subject), +``` -The config can be type-checked in `strict` mode by TypeScript with the [noLib](https://www.typescriptlang.org/tsconfig#noLib) -option (preventing the standard globals), and the -[strictPropertyInitialization](https://www.typescriptlang.org/tsconfig#strictPropertyInitialization) option (allowing -uninitialized properties). +## Complete example ```ts -class User implements Namespace { - related: { - manager: User[] - } -} +class User implements Namespace {} class Group implements Namespace { related: { - members: (User | Group)[] + members: (User | SubjectSet)[] } } class Folder implements Namespace { related: { - parents: File[] viewers: (User | SubjectSet)[] } - permits = { - view: (ctx: Context): boolean => this.related.viewers.includes(ctx.subject), + view: (ctx: Context) => this.related.viewers.includes(ctx.subject), } } class File implements Namespace { related: { - parents: (File | Folder)[] + parents: Folder[] viewers: (User | SubjectSet)[] owners: (User | SubjectSet)[] - siblings: File[] } - permits = { - view: (ctx: Context): boolean => - this.related.parents.traverse((p) => p.related.viewers.includes(ctx.subject)) || - this.related.parents.traverse((p) => p.permits.view(ctx)) || + view: (ctx: Context) => this.related.viewers.includes(ctx.subject) || - this.related.owners.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), - - rename: (ctx: Context) => this.related.siblings.traverse((s) => s.permits.edit(ctx)), } } ``` + +This schema models: + +- Direct access via `viewers` and `owners` +- Group-based access via `SubjectSet` +- Inherited access from parent folders via `traverse` From 877a3146acf41e0d2ce1f873acced41960560c15 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Tue, 26 May 2026 09:13:27 +0200 Subject: [PATCH 2/3] copilot reviews --- docs/keto/reference/ory-permission-language.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/keto/reference/ory-permission-language.mdx b/docs/keto/reference/ory-permission-language.mdx index 4cc8de4010..8da1fdb698 100644 --- a/docs/keto/reference/ory-permission-language.mdx +++ b/docs/keto/reference/ory-permission-language.mdx @@ -24,6 +24,8 @@ The `related` block defines who can be associated with an object. Each entry nam holds. ```ts +import { Namespace } from "@ory/keto-namespace-types" + class File implements Namespace { related: { viewers: User[] @@ -66,14 +68,14 @@ viewers: (User | SubjectSet)[] This allows writing tuples that point to a group's members rather than a user directly: ```keto-natural -Group:engineering#members is in viewers of File:readme +members of Group:engineering 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. +The `permits` block defines boolean functions the engine evaluates during a check. ```ts class File implements Namespace { From f3cb04c9c577e58e87a1a5cbabd0cca717b4ff39 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Mon, 1 Jun 2026 12:20:34 +0200 Subject: [PATCH 3/3] address review comments --- .../reference/ory-permission-language.mdx | 89 +++++++++++++------ 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/docs/keto/reference/ory-permission-language.mdx b/docs/keto/reference/ory-permission-language.mdx index 8da1fdb698..45cbca2fd2 100644 --- a/docs/keto/reference/ory-permission-language.mdx +++ b/docs/keto/reference/ory-permission-language.mdx @@ -4,7 +4,12 @@ title: Ory Permission Language sidebar_label: Ory Permission Language --- -OPL is a TypeScript-based language for defining permission models in Ory Permissions. +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 @@ -20,11 +25,13 @@ 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. +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. ```ts import { Namespace } from "@ory/keto-namespace-types" +class User implements Namespace {} class File implements Namespace { related: { @@ -41,6 +48,8 @@ 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 a union when a relation can hold subjects of different types: @@ -58,26 +67,47 @@ Group:engineering is in viewers of File:readme ### Subject-set references -`SubjectSet` 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: +Sometimes you want a relation to include not just individual subjects, but all members of another relation. `SubjectSet` +lets you do this — it refers to all subjects in relation `R` on namespace `T`. + +For example, to allow either individual users or all members of a group to view a file: ```ts -viewers: (User | SubjectSet)[] +class User implements Namespace {} + +class Group implements Namespace { + related: { + members: User[] + } +} + +class File implements Namespace { + related: { + viewers: (User | SubjectSet)[] + } +} ``` -This allows writing tuples that point to a group's members rather than a user directly: +This means a viewer can be either a `User` directly, or any subject in the `members` relation of a `Group`. You can then write a +tuple that grants access to a whole group at once: ```keto-natural members of Group:engineering is in viewers of File:readme ``` -A subject can now be either a `User` directly, or any member of the `Group:engineering`. +This means: every subject in the `members` relation of `Group:engineering` is a viewer of `File:readme`. ## Permits -The `permits` block defines boolean functions the engine evaluates during a check. +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. ```ts +class User implements Namespace {} + class File implements Namespace { related: { viewers: User[] @@ -90,13 +120,20 @@ class File implements Namespace { } ``` +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.x.includes(ctx.subject)` checks whether the subject is directly in relation `x`. +`this.related..includes(ctx.subject)` checks whether the subject is directly in relation ``. -### Following subject-sets: `traverse` +### Inherited membership: `traverse` -`this.related.x.traverse(g => ...)` iterates over the objects in relation `x` and evaluates the inner expression for each one. +`this.related..traverse(fn)` takes a function and calls it for each object in ``. 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..includes(...)`) or call +another permission (`g.permits.(ctx)`). ```ts class Group implements Namespace { @@ -122,13 +159,11 @@ class File implements Namespace { Combine checks with `||`, `&&`, and `!`: ```ts -view: (ctx: Context) => - this.related.viewers.includes(ctx.subject) || - this.related.owners.includes(ctx.subject), +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), + restricted: (ctx: Context) => this.related.allowlist.includes(ctx.subject) && !this.related.blocklist.includes(ctx.subject), +} ``` ### Calling another permission @@ -136,8 +171,8 @@ restricted: (ctx: Context) => A permission can call another permission defined on the same namespace: ```ts -edit: (ctx: Context) => this.related.owners.includes(ctx.subject), -admin: (ctx: Context) => this.permits.edit(ctx) && this.related.admins.includes(ctx.subject), +isAdmin: (ctx: Context) => this.related.admins.includes(ctx.subject), +edit: (ctx: Context) => this.permits.isAdmin(ctx) || this.related.owners.includes(ctx.subject), ``` ## Complete example @@ -147,13 +182,13 @@ class User implements Namespace {} class Group implements Namespace { related: { - members: (User | SubjectSet)[] + members: (User | SubjectSet)[] // a member can be a User, or all members of another Group (enables nested groups) } } class Folder implements Namespace { related: { - viewers: (User | SubjectSet)[] + viewers: (User | SubjectSet)[] // a viewer can be a User directly, or all members of a Group } permits = { view: (ctx: Context) => this.related.viewers.includes(ctx.subject), @@ -162,7 +197,7 @@ class Folder implements Namespace { class File implements Namespace { related: { - parents: Folder[] + parents: Folder[] // the Folders this file is nested under, used to inherit permissions viewers: (User | SubjectSet)[] owners: (User | SubjectSet)[] } @@ -170,7 +205,7 @@ class File implements Namespace { view: (ctx: Context) => this.related.viewers.includes(ctx.subject) || this.related.owners.includes(ctx.subject) || - this.related.parents.traverse((p) => p.permits.view(ctx)), + this.related.parents.traverse((p) => p.permits.view(ctx)), // grants view if the subject has view permission on any parent Folder edit: (ctx: Context) => this.related.owners.includes(ctx.subject), } } @@ -178,6 +213,6 @@ class File implements Namespace { This schema models: -- Direct access via `viewers` and `owners` -- Group-based access via `SubjectSet` -- Inherited access from parent folders via `traverse` +- A `File` can have individual users or group members as viewers or owners +- A `Group` can contain other groups, enabling nested membership +- A `File` can inherit view permission from its parent `Folder`