Skip to content
Draft
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
186 changes: 186 additions & 0 deletions docs/keto/guides/migrating-to-subject-sets.mdx
Original file line number Diff line number Diff line change
@@ -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"

<Tabs>
<TabItem value="go" label="Go">

```go
payload := ory.CreateRelationshipBody{
Namespace: &namespace,
Object: &object,
Relation: &relation,
SubjectId: &subjectId, // plain string, no namespace
}
```

</TabItem>
<TabItem value="python" label="Python">

```python
body = ory_client.CreateRelationshipBody(
namespace="File",
object="readme",
relation="viewers",
subject_id="user_5", # plain string, no namespace
)
```

</TabItem>
</Tabs>

**After (subject set):**

<Tabs>
<TabItem value="go" label="Go">

```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,
},
}
```

</TabItem>
<TabItem value="python" label="Python">

```python
body = ory_client.CreateRelationshipBody(
namespace="File",
object="readme",
relation="viewers",
subject_set=ory_client.SubjectSet(
namespace="User",
object="user_5",
relation="",
),
)
```

</TabItem>
</Tabs>

### Checking permissions

**Before (subject ID):**

<Tabs>
<TabItem value="go" label="Go">

```go
check, _, err := ory.PermissionApi.CheckPermission(ctx).
Namespace(namespace).
Object(object).
Relation(relation).
SubjectId(subjectId). // plain string, no namespace
Execute()
```

</TabItem>
<TabItem value="python" label="Python">

```python
api_instance.check_permission(
namespace="File",
object="readme",
relation="viewers",
subject_id="user_5", # plain string, no namespace
)
```

</TabItem>
</Tabs>

**After (subject set):**

<Tabs>
<TabItem value="go" label="Go">

```go
check, _, err := ory.PermissionApi.CheckPermission(ctx).
Namespace(namespace).
Object(object).
Relation(relation).
SubjectSetNamespace("User").
SubjectSetObject("user_5").
SubjectSetRelation("").
Execute()
```

</TabItem>
<TabItem value="python" label="Python">

```python
api_instance.check_permission(
namespace="File",
object="readme",
relation="viewers",
subject_set_namespace="User",
subject_set_object="user_5",
subject_set_relation="",
)
```

</TabItem>
</Tabs>

## 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.
144 changes: 144 additions & 0 deletions docs/keto/guides/strict-mode.mdx
Original file line number Diff line number Diff line change
@@ -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<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 @@ -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",
],
},
],
Expand Down
Loading