Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions docs/keto/guides/migrating-to-subject-sets.mdx
Original file line number Diff line number Diff line change
@@ -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:**

<CodeTabs sampleId="subject-id-to-subjectset-migration/00-write-subject-id" version="master" />

**Checking permissions with subject IDs:**

<CodeTabs sampleId="subject-id-to-subjectset-migration/01-check-subject-id" version="master" />

## 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.

<CodeTabs sampleId="subject-id-to-subjectset-migration/02-write-both" version="master" />

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.

<CodeTabs sampleId="subject-id-to-subjectset-migration/03-migrate-existing" version="master" />

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.

<CodeTabs sampleId="subject-id-to-subjectset-migration/04-check-subject-set" version="master" />

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.

<CodeTabs sampleId="subject-id-to-subjectset-migration/05-delete-subject-id" version="master" />

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).
147 changes: 147 additions & 0 deletions docs/keto/guides/strict-mode.mdx
Original file line number Diff line number Diff line change
@@ -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<Group, "members">)[]
```

The same applies in reverse: if `viewers` is declared as `SubjectSet<Group, "members">[]` 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.
2 changes: 2 additions & 0 deletions sidebars-network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,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",
],
},
],
Expand Down
Loading