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..6cffe72bc --- /dev/null +++ b/docs/keto/guides/migrating-to-subject-sets.mdx @@ -0,0 +1,98 @@ +--- +title: Migrating from subject IDs to subject sets +sidebar_label: Migrate to subject sets +--- + +import CodeTabs from "@site/src/theme/CodeTabs" + +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. + +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. + +## Am I affected? + +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 with subject IDs:** + + + +**Checking permissions with subject IDs:** + + + +## Update your OPL first + +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`: + +```ts +class User implements Namespace {} +class ApiKey implements Namespace {} + +class File implements Namespace { + related: { + viewers: (User | ApiKey)[] + } + permits = { + view: (ctx: Context) => this.related.viewers.includes(ctx.subject), + } +} +``` + +## Migration steps + +The following is one recommended migration path that requires no downtime. + +### Step 1: Dual-write new tuples + +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. + + + +Deploy this change before moving on. Once deployed, all new tuples have subject set counterparts. + +### Step 2: Backfill existing tuples + +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 this step, every subject ID tuple has a subject set twin. + +### Step 3: Switch check requests to subject sets + +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. + + + +Deploy this change. Once deployed, all check requests use subject sets. + +### Step 4: Remove dual writes + +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. + +### Step 5: Delete subject ID tuples + +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. + + + +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. + +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 new file mode 100644 index 000000000..1503feb94 --- /dev/null +++ b/docs/keto/guides/strict-mode.mdx @@ -0,0 +1,147 @@ +--- +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 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? + +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", ], }, ],