From 6216c4451ae776b7dee087b0bd3377864be37c56 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Tue, 26 May 2026 08:13:51 +0200 Subject: [PATCH 1/2] docs: add strict mode guide and migration subjectset guide --- .../keto/guides/migrating-to-subject-sets.mdx | 186 ++++++++++++++++++ docs/keto/guides/strict-mode.mdx | 144 ++++++++++++++ sidebars-network.ts | 2 + 3 files changed, 332 insertions(+) create mode 100644 docs/keto/guides/migrating-to-subject-sets.mdx create mode 100644 docs/keto/guides/strict-mode.mdx diff --git a/docs/keto/guides/migrating-to-subject-sets.mdx b/docs/keto/guides/migrating-to-subject-sets.mdx new file mode 100644 index 000000000..a0860b634 --- /dev/null +++ b/docs/keto/guides/migrating-to-subject-sets.mdx @@ -0,0 +1,186 @@ +--- +title: Migrating from subject IDs to subject sets +sidebar_label: Migrate to subject sets +--- + +Early versions of Ory Permissions supported writing tuples where the subject was a plain string with no namespace — for example +`File:readme#viewers@user_5`. These are called **subject IDs**. They predate the +[Ory Permission Language](../reference/ory-permission-language) and have no connection to the namespaces defined in your OPL. + +The current model uses **subject sets** instead: the subject includes a namespace, such as `File:readme#viewers@User:user_5`. +The namespace (`User`) refers to a class defined in your OPL, which lets the engine validate and traverse subjects correctly. + +Subject IDs still work in non-strict mode, but strict mode returns an explicit error when it encounters one — because they have +no namespace, the engine cannot validate them against your OPL. In non-strict mode this produces a silent `allowed: false` +instead, which looks identical to a legitimate denial. + +## What to look for + +Search your application for any place that writes tuples or performs permission checks using the `subject_id` field of the API +client. That field is how subject IDs are passed. + +### Writing tuples + +**Before (subject ID):** + +import Tabs from "@theme/Tabs" +import TabItem from "@theme/TabItem" + + + + +```go +payload := ory.CreateRelationshipBody{ + Namespace: &namespace, + Object: &object, + Relation: &relation, + SubjectId: &subjectId, // plain string, no namespace +} +``` + + + + +```python +body = ory_client.CreateRelationshipBody( + namespace="File", + object="readme", + relation="viewers", + subject_id="user_5", # plain string, no namespace +) +``` + + + + +**After (subject set):** + + + + +```go +subjectNamespace := "User" +subjectObject := "user_5" +subjectRelation := "" + +payload := ory.CreateRelationshipBody{ + Namespace: &namespace, + Object: &object, + Relation: &relation, + SubjectSet: &ory.SubjectSet{ + Namespace: subjectNamespace, + Object: subjectObject, + Relation: subjectRelation, + }, +} +``` + + + + +```python +body = ory_client.CreateRelationshipBody( + namespace="File", + object="readme", + relation="viewers", + subject_set=ory_client.SubjectSet( + namespace="User", + object="user_5", + relation="", + ), +) +``` + + + + +### Checking permissions + +**Before (subject ID):** + + + + +```go +check, _, err := ory.PermissionApi.CheckPermission(ctx). + Namespace(namespace). + Object(object). + Relation(relation). + SubjectId(subjectId). // plain string, no namespace + Execute() +``` + + + + +```python +api_instance.check_permission( + namespace="File", + object="readme", + relation="viewers", + subject_id="user_5", # plain string, no namespace +) +``` + + + + +**After (subject set):** + + + + +```go +check, _, err := ory.PermissionApi.CheckPermission(ctx). + Namespace(namespace). + Object(object). + Relation(relation). + SubjectSetNamespace("User"). + SubjectSetObject("user_5"). + SubjectSetRelation(""). + Execute() +``` + + + + +```python +api_instance.check_permission( + namespace="File", + object="readme", + relation="viewers", + subject_set_namespace="User", + subject_set_object="user_5", + subject_set_relation="", +) +``` + + + + +## Update your OPL + +Every namespace you reference as a subject must be declared in your OPL. If you migrate subjects to `User:user_5`, make sure +your OPL includes a `User` class: + +```ts +class User implements Namespace {} +``` + +And the relation that holds them must declare `User` as a valid subject type: + +```ts +class File implements Namespace { + related: { + viewers: User[] + } +} +``` + +## Migrate existing tuples + +Updating your application code only affects new tuples written going forward. Existing tuples that use subject IDs remain in the +database and will be ignored by strict mode. + +You need to backfill: for each old tuple with a subject ID, write a new tuple with the equivalent subject set, then delete the +old one. Do this before enabling strict mode. diff --git a/docs/keto/guides/strict-mode.mdx b/docs/keto/guides/strict-mode.mdx new file mode 100644 index 000000000..bb7f7b8bd --- /dev/null +++ b/docs/keto/guides/strict-mode.mdx @@ -0,0 +1,144 @@ +--- +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. + +### Tuples written with a subject ID instead of a subject set + +If your application uses the `subject_id` API field to write tuples or perform checks — for example writing +`File:readme#viewers@user_5` with no namespace — strict mode returns an explicit error at check time. Subject IDs have no +connection to your OPL, so the engine cannot validate them. In non-strict mode this produces a silent `allowed: false`, +indistinguishable from a legitimate denial. In strict mode you get an error immediately, because strict mode requires all tuples to be consistent with your OPL. + +This requires a migration: see [Migrating from subject IDs to subject sets](./migrating-to-subject-sets). + +### 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/sidebars-network.ts b/sidebars-network.ts index 56eb11aba..d6df5bbfb 100644 --- a/sidebars-network.ts +++ b/sidebars-network.ts @@ -447,6 +447,8 @@ const networkSidebar = [ "keto/guides/list-api-display-objects", "keto/guides/expand-api-display-who-has-access", "keto/guides/rbac", + "keto/guides/strict-mode", + "keto/guides/migrating-to-subject-sets", ], }, ], From 71f99248c6180361306aeed7655a159f853fc3e8 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Thu, 28 May 2026 17:00:25 +0200 Subject: [PATCH 2/2] update --- .../keto/guides/migrating-to-subject-sets.mdx | 202 +++++------------- docs/keto/guides/strict-mode.mdx | 7 +- 2 files changed, 62 insertions(+), 147 deletions(-) diff --git a/docs/keto/guides/migrating-to-subject-sets.mdx b/docs/keto/guides/migrating-to-subject-sets.mdx index a0860b634..6cffe72bc 100644 --- a/docs/keto/guides/migrating-to-subject-sets.mdx +++ b/docs/keto/guides/migrating-to-subject-sets.mdx @@ -3,184 +3,96 @@ title: Migrating from subject IDs to subject sets sidebar_label: Migrate to subject sets --- -Early versions of Ory Permissions supported writing tuples where the subject was a plain string with no namespace — for example -`File:readme#viewers@user_5`. These are called **subject IDs**. They predate the -[Ory Permission Language](../reference/ory-permission-language) and have no connection to the namespaces defined in your OPL. +import CodeTabs from "@site/src/theme/CodeTabs" -The current model uses **subject sets** instead: the subject includes a namespace, such as `File:readme#viewers@User:user_5`. -The namespace (`User`) refers to a class defined in your OPL, which lets the engine validate and traverse subjects correctly. +Early versions of Ory Permissions allowed writing tuples where the subject was a plain string with no namespace — for example +`File:data.txt#viewer@user_alice`. These are called **subject IDs**. They predate the +[Ory Permission Language](../reference/ory-permission-language) and have no connection to the namespaces in your OPL. -Subject IDs still work in non-strict mode, but strict mode returns an explicit error when it encounters one — because they have -no namespace, the engine cannot validate them against your OPL. In non-strict mode this produces a silent `allowed: false` -instead, which looks identical to a legitimate denial. +The recommended approach is to use **subject sets** instead: a subject that includes a namespace declared in your OPL, such as +`File:data.txt#viewer@User:alice`. The namespace (`User`) ties the subject to a class in your OPL, which lets the engine validate +and traverse subjects correctly and faster. -## What to look for +## Am I affected? -Search your application for any place that writes tuples or performs permission checks using the `subject_id` field of the API -client. That field is how subject IDs are passed. +You are affected if your application uses the `subject_id` field anywhere in the API client — either when writing tuples or when +performing permission checks. -### Writing tuples +**Writing tuples with subject IDs:** -**Before (subject ID):** + -import Tabs from "@theme/Tabs" -import TabItem from "@theme/TabItem" +**Checking permissions with subject IDs:** - - + -```go -payload := ory.CreateRelationshipBody{ - Namespace: &namespace, - Object: &object, - Relation: &relation, - SubjectId: &subjectId, // plain string, no namespace -} -``` +## Update your OPL first - - - -```python -body = ory_client.CreateRelationshipBody( - namespace="File", - object="readme", - relation="viewers", - subject_id="user_5", # plain string, no namespace -) -``` +Before migrating tuples, make sure your OPL declares a namespace for every subject type you use. If your subject IDs follow a +naming convention like `user_alice` or `apikey_ci-bot`, decide which OPL namespace each prefix maps to. - - +For example, `user_` → `User` and `apikey_` → `ApiKey`: -**After (subject set):** - - - - -```go -subjectNamespace := "User" -subjectObject := "user_5" -subjectRelation := "" +```ts +class User implements Namespace {} +class ApiKey implements Namespace {} -payload := ory.CreateRelationshipBody{ - Namespace: &namespace, - Object: &object, - Relation: &relation, - SubjectSet: &ory.SubjectSet{ - Namespace: subjectNamespace, - Object: subjectObject, - Relation: subjectRelation, - }, +class File implements Namespace { + related: { + viewers: (User | ApiKey)[] + } + permits = { + view: (ctx: Context) => this.related.viewers.includes(ctx.subject), + } } ``` - - - -```python -body = ory_client.CreateRelationshipBody( - namespace="File", - object="readme", - relation="viewers", - subject_set=ory_client.SubjectSet( - namespace="User", - object="user_5", - relation="", - ), -) -``` +## Migration steps - - +The following is one recommended migration path that requires no downtime. -### Checking permissions +### Step 1: Dual-write new tuples -**Before (subject ID):** +For every tuple your application writes, write two: one with the subject ID and one with the subject set. This keeps existing +permission checks working while the migration is in progress — both representations are present in the database, so neither check +path is broken. - - + -```go -check, _, err := ory.PermissionApi.CheckPermission(ctx). - Namespace(namespace). - Object(object). - Relation(relation). - SubjectId(subjectId). // plain string, no namespace - Execute() -``` +Deploy this change before moving on. Once deployed, all new tuples have subject set counterparts. - - +### Step 2: Backfill existing tuples -```python -api_instance.check_permission( - namespace="File", - object="readme", - relation="viewers", - subject_id="user_5", # plain string, no namespace -) -``` - - - +Paginate through all existing tuples, filter for ones where `subject_id` is set, determine the target namespace from your naming +convention, and write the subject set equivalent for each. Consider saving the list of processed subject IDs so the backfill is +resumable if interrupted. -**After (subject set):** + - - +After this step, every subject ID tuple has a subject set twin. -```go -check, _, err := ory.PermissionApi.CheckPermission(ctx). - Namespace(namespace). - Object(object). - Relation(relation). - SubjectSetNamespace("User"). - SubjectSetObject("user_5"). - SubjectSetRelation(""). - Execute() -``` +### Step 3: Switch check requests to subject sets - - - -```python -api_instance.check_permission( - namespace="File", - object="readme", - relation="viewers", - subject_set_namespace="User", - subject_set_object="user_5", - subject_set_relation="", -) -``` +Now that every tuple has a subject set counterpart, update all permission checks to use subject set fields instead of +`subject_id`. Both representations are still in the database, so existing checks continue to work during rollout. - - + -## Update your OPL +Deploy this change. Once deployed, all check requests use subject sets. -Every namespace you reference as a subject must be declared in your OPL. If you migrate subjects to `User:user_5`, make sure -your OPL includes a `User` class: +### Step 4: Remove dual writes -```ts -class User implements Namespace {} -``` +Update your write code to emit only the subject set tuple. Remove the subject ID write added in step 1. Once deployed, new writes +produce subject sets only. Subject ID tuples that already exist in the database are cleaned up in step 5. -And the relation that holds them must declare `User` as a valid subject type: +### Step 5: Delete subject ID tuples -```ts -class File implements Namespace { - related: { - viewers: User[] - } -} -``` +Delete all remaining subject ID tuples. This includes the original tuples from before the migration and the subject ID half of any +dual-write tuples created in step 1. -## Migrate existing tuples + -Updating your application code only affects new tuples written going forward. Existing tuples that use subject IDs remain in the -database and will be ignored by strict mode. +If you saved the list of subject IDs during backfill in step 2, you can delete directly from that list. Otherwise, paginate +through tuples again and delete any where `subject_id` is set. -You need to backfill: for each old tuple with a subject ID, write a new tuple with the equivalent subject set, then delete the -old one. Do this before enabling strict mode. +After this step, only subject set tuples remain. Your application is ready for [strict mode](./strict-mode). diff --git a/docs/keto/guides/strict-mode.mdx b/docs/keto/guides/strict-mode.mdx index bb7f7b8bd..1503feb94 100644 --- a/docs/keto/guides/strict-mode.mdx +++ b/docs/keto/guides/strict-mode.mdx @@ -9,7 +9,9 @@ Strict mode makes the Ory Permissions engine treat your [OPL](../reference/ory-p 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**. +Strict mode is enabled by default for all new Ory Network projects created from July 2026 onwards — if you created your project +after that date, strict mode is already on and you don't need to configure anything. For older projects, strict mode is disabled +by default and can be enabled in the Ory Console under **Permissions > Namespaces**. ## Why enable strict mode? @@ -39,7 +41,8 @@ These patterns work in non-strict mode but break after enabling strict mode. If your application uses the `subject_id` API field to write tuples or perform checks — for example writing `File:readme#viewers@user_5` with no namespace — strict mode returns an explicit error at check time. Subject IDs have no connection to your OPL, so the engine cannot validate them. In non-strict mode this produces a silent `allowed: false`, -indistinguishable from a legitimate denial. In strict mode you get an error immediately, because strict mode requires all tuples to be consistent with your OPL. +indistinguishable from a legitimate denial. In strict mode you get an error immediately, because strict mode requires all tuples +to be consistent with your OPL. This requires a migration: see [Migrating from subject IDs to subject sets](./migrating-to-subject-sets).