From 6b60f2d7c5ba3e7c7672dcb2214aea216b140718 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Sun, 10 May 2026 21:25:43 +0200 Subject: [PATCH 1/5] chore: clarify rbac example --- docs/keto/guides/rbac.mdx | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/keto/guides/rbac.mdx b/docs/keto/guides/rbac.mdx index 8f7bdc823..623109dd1 100644 --- a/docs/keto/guides/rbac.mdx +++ b/docs/keto/guides/rbac.mdx @@ -16,7 +16,8 @@ A reporting application might define a fixed set of permissions: Users do not receive these permissions directly. Instead, users are assigned to roles, and roles are granted permissions. Permissions are application-defined. Roles are application data represented in Ory Keto through relationship tuples, so roles and -role assignments can be created, updated, and deleted without changing the OPL schema. +role assignments can be created, updated, and deleted without changing the +[Ory Permission Language](../../../../docs/keto/reference/ory-permission-language) (OPL) schema. ## OPL schema @@ -77,6 +78,27 @@ user can hold different roles in different organizations. In a non-multi-tenant app, use a single fixed object such as `Organization:main`. In a multi-tenant app, each tenant gets its own `Organization` object. +`Organization` is the namespace name chosen for this guide. You can name it to match your domain — `Workspace`, `Project`, or +`Tenant` all work the same way. The name has no special meaning in Ory Keto; it is just a label for the scope object. + +## Object ID guidance + +Object IDs in Ory Keto are global — there is no built-in namespacing by tenant or project. Two tuples that reference `Role:admin` +refer to the same object. This applies to any namespace: `Role`, `Organization`, `User`, or any custom namespace you define. + +To avoid collisions, object IDs must be unique within their namespace. The safest approach is to use a stable unique ID from your +application database: + +```text +Role:d390f817-209a-4e26-a69b-b67eddc45eda +``` + +What to avoid is using plain labels like `admin` or `viewer` as role IDs without any scoping. Two different organizations both +creating a `viewer` role would write their tuples to the same `Role:viewer` object, silently sharing role membership and +permission grants across tenants. + +The examples in this guide use the format `Role:{org_id}/{label}` — for example `Role:org_123/admin` — for readability. + ## How the model works Permissions are modeled as relations on `Organization`. Each relation holds the set of roles that have been granted that @@ -346,18 +368,6 @@ The application is responsible for: - Preventing inheritance across organizations - Preventing removal of the last administrator, if the product requires one -## Role ID guidance - -Role IDs must be globally unique. The simplest way to guarantee this is to use the stable role ID from your application database: - -```text -Role:01HZY3K7J8K2D9WQ7Y1A4F8X9B -``` - -What to avoid is using human-readable labels like `admin` or `viewer` as role IDs directly. These are not unique across tenants. -In a multi-tenant app, `Role:admin` would refer to the same role object for every organization, causing role assignments and -permission grants to be shared across tenants. - ## Large permission sets This model keeps permissions in OPL because permissions are application-defined actions. A permission usually corresponds to a From 748201b3e65458286b314e041ed7068ef044ec35 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Sun, 10 May 2026 22:59:20 +0200 Subject: [PATCH 2/5] copilot review --- docs/keto/guides/rbac.mdx | 61 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/docs/keto/guides/rbac.mdx b/docs/keto/guides/rbac.mdx index 623109dd1..f4ae81ec6 100644 --- a/docs/keto/guides/rbac.mdx +++ b/docs/keto/guides/rbac.mdx @@ -17,7 +17,7 @@ A reporting application might define a fixed set of permissions: Users do not receive these permissions directly. Instead, users are assigned to roles, and roles are granted permissions. Permissions are application-defined. Roles are application data represented in Ory Keto through relationship tuples, so roles and role assignments can be created, updated, and deleted without changing the -[Ory Permission Language](../../../../docs/keto/reference/ory-permission-language) (OPL) schema. +[Ory Permission Language](../reference/ory-permission-language) (OPL) schema. ## OPL schema @@ -83,8 +83,8 @@ In a non-multi-tenant app, use a single fixed object such as `Organization:main` ## Object ID guidance -Object IDs in Ory Keto are global — there is no built-in namespacing by tenant or project. Two tuples that reference `Role:admin` -refer to the same object. This applies to any namespace: `Role`, `Organization`, `User`, or any custom namespace you define. +Object IDs in Ory Keto are global. Two tuples that reference `Role:admin` refer to the same object. This applies to any namespace: +`Role`, `Organization`, `User`, or any custom namespace you define. To avoid collisions, object IDs must be unique within their namespace. The safest approach is to use a stable unique ID from your application database: @@ -387,3 +387,58 @@ authorization can check. If the application has hundreds of fixed permissions, the OPL schema will be large but remains correct and predictable. This tradeoff keeps permission checks scoped and explicit while still allowing roles to be managed dynamically. + +## Relationship diagram + +The diagram below shows the relationships written in the examples above. Users connect to roles through `members`. Roles connect +to permission relations on `Organization`. Dotted arrows show role inheritance. + +```mdx-code-block +import Mermaid from "@site/src/theme/Mermaid" + +|inherits| re + end + + subgraph org["Organization:org_123"] + direction LR + rv(["reports.view"]) + rc(["reports.create"]) + redit(["reports.edit"]) + rd(["reports.delete"]) + rman(["roles.manage"]) + mi(["members.invite"]) + end + + alice -->|members| admin + bob -->|members| viewer + eve -->|members| re + charlie -->|members| rm + + admin --> rv & rc & redit & rd & rman & mi + viewer --> rv + re --> rv & rc & redit + rm --> rd + + style alice fill:lightgreen + style bob fill:lightgreen + style eve fill:lightgreen + style charlie fill:lightgreen +`} +/> +``` From 213ae7f404e34abcd4755bb5ec4d3c37c6c9e7c5 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Mon, 11 May 2026 09:23:59 +0200 Subject: [PATCH 3/5] use correct relation name --- docs/keto/guides/rbac.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/keto/guides/rbac.mdx b/docs/keto/guides/rbac.mdx index f4ae81ec6..86bddbabc 100644 --- a/docs/keto/guides/rbac.mdx +++ b/docs/keto/guides/rbac.mdx @@ -390,8 +390,8 @@ tradeoff keeps permission checks scoped and explicit while still allowing roles ## Relationship diagram -The diagram below shows the relationships written in the examples above. Users connect to roles through `members`. Roles connect -to permission relations on `Organization`. Dotted arrows show role inheritance. +The diagram below shows the relationships written in the examples above. Users connect to roles through the `members` relation. +Roles connect to permission relations on `Organization`. Dotted arrows show the `inheritors` relation. ```mdx-code-block import Mermaid from "@site/src/theme/Mermaid" @@ -412,7 +412,7 @@ flowchart TD viewer["Role:org_123/viewer"] re["Role:org_123/report_editor"] rm["Role:org_123/report_manager"] - rm -.->|inherits| re + rm -.->|inheritors| re end subgraph org["Organization:org_123"] From d8cf483ac3e3548a19b22b77b84d93d44babd1be Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Mon, 11 May 2026 09:45:08 +0200 Subject: [PATCH 4/5] explain Organization object better --- docs/keto/guides/rbac.mdx | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/keto/guides/rbac.mdx b/docs/keto/guides/rbac.mdx index 86bddbabc..9b4e947d6 100644 --- a/docs/keto/guides/rbac.mdx +++ b/docs/keto/guides/rbac.mdx @@ -65,21 +65,23 @@ class Organization implements Namespace { ## Authorization scope -This example uses `Organization` as the scope for every RBAC decision. Every permission check includes both the subject and the -organization: +In Ory Keto, every check is scoped to an object — for example, `Document:file1` or `Folder:reports`. That works well for +per-resource permissions, but RBAC with dynamic roles typically works differently: if Alice can create reports, she can create +them anywhere in the app. The permission is app-wide, not tied to a specific file. + +This guide uses `Organization:org_123` as that app-wide scope. Every permission check is made against the organization object: ```keto-natural is User:alice allowed to viewReports on Organization:org_123 ``` -Without a scope, checks are global — "can Alice edit reports?" — with no way to express boundaries. With `Organization`, the same -user can hold different roles in different organizations. +Alice either has the `viewReports` permission on `org_123` or she doesn't — no per-resource logic needed. -In a non-multi-tenant app, use a single fixed object such as `Organization:main`. In a multi-tenant app, each tenant gets its own -`Organization` object. +In a single-tenant app, one fixed object such as `Organization:main` can be used. In a multi-tenant app, each tenant gets its own +`Organization` object, so Alice can be an admin in one organization and a viewer in another. -`Organization` is the namespace name chosen for this guide. You can name it to match your domain — `Workspace`, `Project`, or -`Tenant` all work the same way. The name has no special meaning in Ory Keto; it is just a label for the scope object. +`Organization` is just the name chosen for this guide. You can use any name that fits your domain — `Workspace`, `Project`, or +`Tenant` all work the same way. ## Object ID guidance @@ -296,17 +298,17 @@ class Role implements Namespace { ``` The `inheritors` relation is declared on the parent role and lists every role that inherits it. When Ory Keto evaluates -`viewer.isMember`, it checks viewer's direct members first, then walks each role in `inheritors` and checks those too. Members of -inheriting roles therefore pass any permission check that goes through viewer. +`report_editor.isMember`, it checks report_editor's direct members first, then walks each role in `inheritors` and checks those +too. Members of inheriting roles therefore pass any permission check that goes through report_editor. -This change allows us creating relationship that can make report_editor an inheritor of viewer: +For example, making `report_manager` an inheritor of `report_editor`: ```keto-natural -Role:org_123/report_editor is in inheritors of Role:org_123/viewer +Role:org_123/report_manager is in inheritors of Role:org_123/report_editor ``` -Which means **report_editor** inherits **viewer** — members of report_editor are treated as members of viewer for all permission -checks. If `reports.view` is granted to viewer, then report_editor members can also view reports without an explicit grant. +means the members of report_manager are treated as members of report_editor for all permission checks. If `reports.create` is +granted to report_editor, then report_manager members can also create reports without an explicit grant. ### Example From bc82c1e7bbf90c87828354edd36b57a1eb6f6477 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Mon, 11 May 2026 16:28:14 +0200 Subject: [PATCH 5/5] move rbac to guides section in sidebar --- sidebars-network.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sidebars-network.ts b/sidebars-network.ts index 7ca10c161..56eb11aba 100644 --- a/sidebars-network.ts +++ b/sidebars-network.ts @@ -429,7 +429,6 @@ const networkSidebar = [ id: "keto/index", }, items: [ - "keto/guides/rbac", { type: "autogenerated", dirName: "keto/concepts", @@ -447,6 +446,7 @@ const networkSidebar = [ "keto/modeling/create-permission-model", "keto/guides/list-api-display-objects", "keto/guides/expand-api-display-who-has-access", + "keto/guides/rbac", ], }, ],