diff --git a/docs/keto/guides/strict-mode.mdx b/docs/keto/guides/strict-mode.mdx new file mode 100644 index 0000000000..cbc98aef73 --- /dev/null +++ b/docs/keto/guides/strict-mode.mdx @@ -0,0 +1,135 @@ +--- +title: Strict mode for Ory Permissions +sidebar_label: Strict mode +--- + +## What is strict mode? + +Strict mode makes the Ory Permissions engine treat your [OPL](../reference/ory-permission-language) as the single source of truth +during every check. Without strict mode, the engine doesn't use your OPL declarations to filter which tuples it follows — it may +follow subject-set pointers that your OPL doesn't specify. + +Strict mode is disabled by default. Enable it in the Ory Console under **Permissions > Configuration**. + +## Why enable strict mode? + +Strict mode improves both performance and correctness: + +- **Fewer queries.** Ory Keto skips evaluation steps that are impossible given your schema — following undeclared subject-set + pointer types, and direct tuple checks on `permits` rules. +- **No stale grants.** Tuples that reference relations removed from your OPL no longer grant access. +- **Explicit errors when limits are reached.** Ory Permissions enforces depth and width limits to prevent unbounded graph + traversal. In non-strict mode, hitting a limit silently returns `{ "allowed": false }` — identical to a legitimate denial. In + strict mode, the engine returns an explicit error so you can tell the check was cut short. + +| Scenario | Non-strict | Strict | +| ----------------------------- | ---------------------- | ---------------------------------------------------- | +| Limit hit during single check | `{ "allowed": false }` | `422 Unprocessable Entity` with reason | +| Limit hit during batch check | `{ "allowed": false }` | `{ "allowed": false, "error": "max depth reached" }` | + +Ory Network enforces fixed depth and width limits that cannot be changed in the console. If you hit a limit, contact +[Ory support](https://www.ory.com/support) to discuss your use case. + +## Patterns that break in strict mode + +These patterns work in non-strict mode but break after enabling strict mode. + +### Subject-set tuples for undeclared types + +This covers any tuple that points to a subject-set type your OPL doesn't declare for that relation. + +**Example:** `viewers` is declared as `User[]`, but a tuple pointing to a `Group` subject-set was written: + +```ts +class File implements Namespace { + related: { + viewers: User[] // only Users allowed + } +} +``` + +Writing a tuple like this — which assigns a `Group` subject-set to the `viewers` relation — will be ignored in strict mode: + +```bash +keto relation-tuple create Group:engineering#members viewers File:readme +``` + +Declare the type in OPL to keep it working: + +```ts +viewers: (User | SubjectSet)[] +``` + +The same applies in reverse: if `viewers` is declared as `SubjectSet[]` but a direct user tuple was written: + +```keto-tuples +File:readme#viewers@User:alice +``` + +Strict mode ignores it because `User` is not a declared type for that relation. + +### Tuples written directly against permit relations + +**Example:** `canView` is a computed permit, but a tuple was written against it directly: + +```ts +class File implements Namespace { + related: { + editors: User[] + viewers: User[] + } + permits = { + canView: (ctx: Context) => this.related.editors.includes(ctx.subject) || this.related.viewers.includes(ctx.subject), + } +} +``` + +```keto-tuples +File:readme#canView@User:alice +``` + +Strict mode skips direct tuple checks on `permits` rules. Write tuples against `editors` or `viewers` instead. + +### Stale tuples from a renamed or removed relation + +If you renamed or removed a relation in OPL but didn't clean up the old tuples, in rare setups, Ory Keto in non-strict mode still +follows them. Strict mode ignores them immediately. + +## How to check if you're ready + +Audit two things before enabling: + +1. **Tuple writes** — every relation you write tuples against should exist in your OPL, and the subject type should match what the + relation declares. For example, if your application writes: + + ```keto-tuples + Document:readme#editors@User:alice + ``` + + check that the `Document` namespace in your OPL declares an `editors` relation, and that it accepts `User` as a subject type: + + ```ts + class Document implements Namespace { + related: { + editors: User[] + } + } + ``` + +2. **Check requests** — every relation you check should be defined in your OPL. For example, if your application calls: + + ```keto-natural + is User:alice allowed to editors on Document:readme + ``` + + verify that `editors` is declared in the `Document` namespace. + +If both are consistent with your OPL, enabling strict mode produces identical results to non-strict mode — with faster permission +checks. + +See the [Ory Permission Language](../reference/ory-permission-language) guide. + +## Enabling and disabling + +Go to the [Ory Console](https://console.ory.sh), select your project, and navigate to **Permissions > Configuration**. Toggle +**Strict mode** on or off and save. The change takes effect immediately — no restart required, and no data is modified. 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` diff --git a/sidebars-network.ts b/sidebars-network.ts index 56eb11aba6..98ca3064d3 100644 --- a/sidebars-network.ts +++ b/sidebars-network.ts @@ -447,6 +447,7 @@ const networkSidebar = [ "keto/guides/list-api-display-objects", "keto/guides/expand-api-display-who-has-access", "keto/guides/rbac", + "keto/guides/strict-mode", ], }, ],