From 9c2c7aef71c3a99aa581054d868118fcb08627a5 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Tue, 17 Feb 2026 11:47:27 +0100 Subject: [PATCH 01/10] docs: update keto examples to use subjectsets --- docs/guides/permissions/overview.mdx | 41 +- docs/keto/concepts/01_relation-tuples.mdx | 4 +- docs/keto/concepts/05_namespaces.mdx | 10 +- docs/keto/concepts/10_objects.mdx | 2 +- docs/keto/concepts/15_subjects.mdx | 4 +- docs/keto/concepts/20_graph-of-relations.mdx | 46 +- docs/keto/examples/olymp-file-sharing.mdx | 54 +- .../expand-api-display-who-has-access.mdx | 24 +- docs/keto/guides/list-api-display-objects.mdx | 38 +- docs/keto/guides/rbac.mdx | 12 +- docs/keto/quickstart.mdx | 90 +- src/theme/CodeTabs/index.js | 4 +- src/theme/ketoRelationTuplesPrism.js | 125 ++- src/theme/ketoRelationsPermissionsPrism.js | 178 ++-- src/utils/prismDark.mjs | 43 + src/utils/prismLight.mjs | 35 +- ...ketoRelationsPermissionsPrism.test.ts.snap | 932 ++++++++++++++++++ .../prism/ketoRelationTuplesPrism.test.ts | 310 ++++++ .../ketoRelationsPermissionsPrism.test.ts | 71 ++ 19 files changed, 1741 insertions(+), 282 deletions(-) create mode 100644 tests/jest/prism/__snapshots__/ketoRelationsPermissionsPrism.test.ts.snap create mode 100644 tests/jest/prism/ketoRelationTuplesPrism.test.ts create mode 100644 tests/jest/prism/ketoRelationsPermissionsPrism.test.ts diff --git a/docs/guides/permissions/overview.mdx b/docs/guides/permissions/overview.mdx index 5da96957ec..7365ec121b 100644 --- a/docs/guides/permissions/overview.mdx +++ b/docs/guides/permissions/overview.mdx @@ -30,42 +30,48 @@ Read the dedicated documents to learn more about [subjects](/keto/concepts/15_su ::: -Examples of relationships are: +Each relationship consists of: -```keto-relationships -users:user1 is in members of groups:group1 -members of groups:group1 are readers of files:file1 +- **Subject** – The entity that receives the role or permission. +- **Object** – The entity on which the role or permission is granted. + +Examples: + +```keto-natural +User:user1 is in members of Group:group1 +members of Group:group1 are readers of File:file1 ``` -As you can see, the subject of a relationship can either be a specific subject ID, or subjects defined through an -[indirection](https://en.wikipedia.org/wiki/Indirection) (all members of a certain group). The object is referenced by its ID. +As you can see, the subject of a relationship can either be a specific subject ID (e.g. User:user1), or subjects defined through +an [indirection](https://en.wikipedia.org/wiki/Indirection) (e.g. all members of Group:group1). The object is referenced by its ID +(e.g. Group:group1, File:file1). ### Checking permissions Permissions are just another form of relations. Therefore, a permission check is a request to check whether a subject has a certain relation to an object, possibly through one or more indirections. -As a very simple example, let's assume the following tuples exist: +As a very simple example, leat's assume the following tuples exist: -```keto-relationships -users:user1 is in members of groups:group1 -members of groups:group1 are readers of files:file1 +```keto-natural +User:user1 is in members of Group:group1 +members of Group:group1 are readers of File:file1 ``` Based on these tuples, you can run permission checks: - Is `user1` a `reader` of `file1`? - ```keto-relationships - is users:user1 in readers of files:file1? // Yes + ```keto-natural + is User:user1 in readers of File:file1? // Yes ``` - Yes, because `user1` is in `members` of `groups:group1` and all `members` of `groups:group1` are `readers` of `files:file1`. + Yes, because `user1` is in `members` of `Group:group1` and all `members` of `Group:group1` are `readers` of `File:file1`. - Is `user2` a member of `group1`? - ```keto-relationships - is users:user2 in members of groups:group1? // No + ```keto-natural + is users:user2 in members of Group:group1? // No ``` No, because there is no relation between `user2` and `group1`. @@ -156,7 +162,6 @@ ory create relationships relationships.json # Output: # NAMESPACE OBJECT RELATION NAME SUBJECT -# Group developer members patrik # Group developer members User:Patrik # Group developer members User:Henning # Folder keto/ viewers Group:developer#members @@ -172,8 +177,8 @@ With the relationships created, try running queries that illustrate use cases: #### Transitive permissions for objects in the hierarchy -Patrik can view `keto/src/main.go`. This file is in the `keto/src` folder, which is in `keto`. The `keto` directory has the -"developer" group as its "viewers". Patrik is a member of the "developer" group. +Patrik can view `keto/src/main.go`, because this file is in the `keto/src` folder, which is in `keto`. The `keto` directory has +the "developer" group as its "viewers". Patrik is a member of the "developer" group. ``` ory is allowed User:Patrik view File keto/src/main.go diff --git a/docs/keto/concepts/01_relation-tuples.mdx b/docs/keto/concepts/01_relation-tuples.mdx index 8fcb701b29..cfc7dcbf37 100644 --- a/docs/keto/concepts/01_relation-tuples.mdx +++ b/docs/keto/concepts/01_relation-tuples.mdx @@ -17,7 +17,7 @@ configured. You can think of relationships as edges in a graph. For a simple relationship: -```keto-relationships +```keto-natural User:user1 is in members of Group:group1 User:user2 is in readers of Document:readme.txt Folder:src is in parents of Document:package.json @@ -27,7 +27,7 @@ Folder:src is in parents of Document:package.json The [Zanzibar paper](https://research.google/pubs/pub48190/) uses the following notation for relationships: -```keto-relation-tuples +```keto-tuples Group:group1#members@User:user1 ``` diff --git a/docs/keto/concepts/05_namespaces.mdx b/docs/keto/concepts/05_namespaces.mdx index 2803ea556a..a31b2d352f 100644 --- a/docs/keto/concepts/05_namespaces.mdx +++ b/docs/keto/concepts/05_namespaces.mdx @@ -29,7 +29,7 @@ class Folder implements Namespace {} Each namespace holds a set of permissions, which define which relationships are checked. For example, checking a `view` permission for `User:bob` on an `readme.txt` file in the `Document` namespace requires the following relationship lookups: -```keto-relationships +```keto-natural is User:bob in viewers of Document:readme.txt // all viewers can view the document is User:bob in editors of Document:readme.txt // all editors can view the document is User:bob in owners of Document:readme.txt // all owners can view the document @@ -56,8 +56,8 @@ the type they describe. The name should be in [upper camel case](https://wiki.c2 Relationships in a namespace should be named with a plural form word that describes what relation a subject has with an object. Every relationship should translate to an English sentence, for example: -```keto-relationships -Subject is in members of Object +```keto-natural + is in of ``` Relationships are like the edges in a graph connecting subjects and objects. These edges are always @@ -68,7 +68,7 @@ form. - Correct naming ✅ - ```keto-relationships + ```keto-natural User:02a3c847-c903-446a-a34f-dae74b4fab86 is in writers of File:8f427c01-c295-44f3-b43d-49c3a1042f35 User:b8d00059-b803-4123-9d3d-b3613bfe7c1b is in members of Group:43784684-103e-44c0-9d6c-db9fb265f617 File:11488ab9-4ede-479f-add4-f1379da4ae43 is in children of Directory:803a87e9-0da0-486e-bc08-ef559dd8e034 @@ -77,7 +77,7 @@ form. - Incorrect naming ❌ - ```keto-relationships + ```keto-natural // namespace isn't describing homogenous type of objects User:7a012165-7b21-495b-b84b-cf4e1a21b484 is in members of Tenant1Objects:62237c27-19c3-4bb1-9cbc-a5a67372569b diff --git a/docs/keto/concepts/10_objects.mdx b/docs/keto/concepts/10_objects.mdx index c669bc6227..54156167a1 100644 --- a/docs/keto/concepts/10_objects.mdx +++ b/docs/keto/concepts/10_objects.mdx @@ -51,7 +51,7 @@ c4540cf5-6ac4-4007-910b-c5a56aa3d4e6: Ory Permissions has the following relationships: -```keto-relation-tuples +```keto-tuples // Members of the "admins" group are allowed to set a value v > 5 values:f832e1e7-3c97-4cb8-8582-979e63ae2f1d#set_value@(groups:admins#member) diff --git a/docs/keto/concepts/15_subjects.mdx b/docs/keto/concepts/15_subjects.mdx index be7da438ab..a61e90a8fe 100644 --- a/docs/keto/concepts/15_subjects.mdx +++ b/docs/keto/concepts/15_subjects.mdx @@ -61,9 +61,9 @@ c5b6454f-f79c-4a6d-9e1b-b44e04b56009: Ory Permissions understands the following relationship: -```keto-relation-tuples +```keto-tuples // Allow access to TCP port 22 when the request originates from a specific subnet during office hours -tcp/22#access@c5b6454f-f79c-4a6d-9e1b-b44e04b56009 +TcpPort:22#access@UserAttributes:c5b6454f-f79c-4a6d-9e1b-b44e04b56009 ``` The application must map every incoming request to a subject string that represents the attributes of the request. Ory Permissions diff --git a/docs/keto/concepts/20_graph-of-relations.mdx b/docs/keto/concepts/20_graph-of-relations.mdx index 3e2a37f205..ad8299f5be 100644 --- a/docs/keto/concepts/20_graph-of-relations.mdx +++ b/docs/keto/concepts/20_graph-of-relations.mdx @@ -31,24 +31,24 @@ has to be considered. ::: -```keto-relation-tuples -// user1 has access on dir1 -dir1#access@user1 +```keto-tuples +// User:1 has access on Dir:1 +Dir:1#access@User:1 // This is an empty relation. -dir1#child@(file1#) +Dir:1#child@(File:1#) -// Everyone with access to dir1 has access to file1. -file1#access@(dir1#access) +// Everyone with access to Dir:1 has access to File:1. +File:1#access@(Dir:1#access) -// Direct access on file2 was granted. -file2#access@user1 +// Direct access on File:2 was granted. +File:2#access@User:1 -// user2 is owner of file2 -file2#owner@user2 +// User:2 is owner of File:2 +File:2#owner@User:2 -// Owners of file2 have access to it; possibly defined through subject set rewrites. -file2#access@(file2#owner) +// Owners of File:2 have access to it; possibly defined through subject set rewrites. +File:2#access@(File:2#owner) ``` This is represented by the following graph: @@ -65,21 +65,21 @@ Solid edges represent explicitly defined relations, while dotted edges represent chart={` graph TD subgraph obj [Object region] - E[dir1] - A[file1] -->|child| E - G[file2] + E[Dir:1] + A[File:1] -->|child| E + G[File:2] end subgraph subjID [Subject ID region] - F([user1]) - C([user2]) + F([User:1]) + C([User:2]) end - A -->|access| B{{dir1#access}} - B -. file1#access .-> F + A -->|access| B{{Dir:1#access}} + B -. File:1#access .-> F E -->|access| F G -->|access| F G -->|owner| C - G -->|access| H{{file2#owner}} - H -. file2#access .-> C + G -->|access| H{{File:2#owner}} + H -. File:2#access .-> C `} /> ``` @@ -95,5 +95,5 @@ Ory Permissions utilizes the following key properties of the graph of relations: Trying to find a path from an object to a subject will always happen locally. This means that it's only necessary to traverse the nodes that are successors of the object. In typical setups, this means that only a small fraction of the graph has to be - searched, regardless of the outcome. The intuition here is that the relations of user1's files are irrelevant when checking - access to user2's files. + searched, regardless of the outcome. The intuition here is that the relations of User:1's files are irrelevant when checking + access to User:2's files. diff --git a/docs/keto/examples/olymp-file-sharing.mdx b/docs/keto/examples/olymp-file-sharing.mdx index 1728cad064..84c682e2c5 100644 --- a/docs/keto/examples/olymp-file-sharing.mdx +++ b/docs/keto/examples/olymp-file-sharing.mdx @@ -15,50 +15,50 @@ defined, where each `owner` of an object also has `access` to that object. All r ::: -Now, the user identified by its unique username `demeter` wants to upload a file containing the most fertile grounds. The file -gets assigned the UUID `ec788a82-a12e-45a4-b906-3e69f78c94e4`. The application adds the following +Now, the user identified by its unique username `Bob` wants to upload a file containing the most fertile grounds. The file gets +assigned the UUID `ec788a82-a12e-45a4-b906-3e69f78c94e4`. The application adds the following [relationship](../concepts/01_relation-tuples.mdx) to Ory Keto through the [write-API](../concepts/25_api-overview.mdx#write-apis): -```keto-relation-tuples -ec788a82-a12e-45a4-b906-3e69f78c94e4#owner@demeter +```keto-tuples +File:ec788a82-a12e-45a4-b906-3e69f78c94e4#owner@User:Bob ``` -To prepare for an important meeting with the user `athena`, `demeter` wants to share the file with fertile grounds with `athena` -so that they can both read it. Therefore, he opens the "Olymp Library" and is presented with a list of all files he owns. The -application will internally request all [objects](../concepts/10_objects.mdx) (file IDs) with the owner `demeter` by using the +To prepare for an important meeting with the user `Alice`, `Bob` wants to share the file with fertile grounds with `Alice` so that +they can both read it. Therefore, he opens the "Olymp Library" and is presented with a list of all files he owns. The application +will internally request all [objects](../concepts/10_objects.mdx) (file IDs) with the owner `Bob` by using the [list-API](../concepts/25_api-overview.mdx#list-relationships). The response will contain the object `ec788a82-a12e-45a4-b906-3e69f78c94e4`, which the application maps to the file in question. -The user `demeter` will then ask the application to share the file with `athena`. The application will translate that request into -a [write-API request](../concepts/25_api-overview.mdx#write-apis) adding the following relationship to Ory Keto: +The user `Bob` will then ask the application to share the file with `Alice`. The application will translate that request into a +[write-API request](../concepts/25_api-overview.mdx#write-apis) adding the following relationship to Ory Keto: -```keto-relation-tuples -ec788a82-a12e-45a4-b906-3e69f78c94e4#access@athena +```keto-tuples +File:ec788a82-a12e-45a4-b906-3e69f78c94e4#access@User:Alice ``` To confirm the successful operation, the application uses Ory Keto's [expand-API](../concepts/25_api-overview.mdx#expand-subject-sets) to compile a list of everyone who can access the file: -```keto-relation-tuples +```keto-tuples // The following subject set is expanded by Keto -ec788a82-a12e-45a4-b906-3e69f78c94e4#access +File:ec788a82-a12e-45a4-b906-3e69f78c94e4#access ``` which returns the expansion tree -``` -∪ ec788a82-a12e-45a4-b906-3e69f78c94e4#access -├─ ∪ ec788a82-a12e-45a4-b906-3e69f78c94e4#owner -│ ├─ ☘ demeter -├─ ☘ athena +```keto-tuples +∪ File:ec788a82-a12e-45a4-b906-3e69f78c94e4#access +├─ ∪ File:ec788a82-a12e-45a4-b906-3e69f78c94e4#owner +│ ├─ ☘ User:Bob +├─ ☘ User:Alice ``` -The "Olymp Library" can then display this information to `demeter`. +The "Olymp Library" can then display this information to `Bob`. -When `athena` wants to get the file containing fertile grounds, the application uses the -[check-API](../concepts/25_api-overview.mdx#check-relationships) to verify that `athena` has access to the file before it returns -the file. This will allow `demeter` to revoke `athena`'s access at any point by deleting the corresponding relationship. +When `Alice` wants to get the file containing fertile grounds, the application uses the +[check-API](../concepts/25_api-overview.mdx#check-relationships) to verify that `Alice` has access to the file before it returns +the file. This will allow `Bob` to revoke `Alice`'s access at any point by deleting the corresponding relationship. This diagram illustrates the relationships in this example: @@ -68,13 +68,13 @@ import Mermaid from "@site/src/theme/Mermaid" r2 - r2 --- sub2(["athena"]) - style sub1 fill:lightgreen - style sub2 fill:lightgreen + r2 --- sub2(["User:Alice"]) + style sub1 fill:green,color:white + style sub2 fill:green,color:white style r1 fill:darkgreen,color:white style r2 fill:darkgreen,color:white `} diff --git a/docs/keto/guides/expand-api-display-who-has-access.mdx b/docs/keto/guides/expand-api-display-who-has-access.mdx index 8a8552e1b6..bc058404f7 100644 --- a/docs/keto/guides/expand-api-display-who-has-access.mdx +++ b/docs/keto/guides/expand-api-display-who-has-access.mdx @@ -30,21 +30,21 @@ To assist users with managing permissions for their files, the application has t this example, we assume that the application knows the following files and directories: ``` -├─ photos (owner: maureen; shared with laura) - ├─ beach.jpg (owner: maureen) - ├─ mountains.jpg (owner: laura) +├─ photos (owner: Alice; shared with Bob) + ├─ beach.jpg (owner: Alice) + ├─ mountains.jpg (owner: Bob) ``` This is represented in Ory Keto by the following [relationships](../concepts/01_relation-tuples.mdx): -```keto-relation-tuples +```keto-tuples // ownership -directories:/photos#owner@maureen -files:/photos/beach.jpg#owner@maureen -files:/photos/mountains.jpg#owner@laura +directories:/photos#owner@Alice +files:/photos/beach.jpg#owner@Alice +files:/photos/mountains.jpg#owner@Bob -// maureen granted access to /photos to laura -directories:/photos#access@laura +// Alice granted access to /photos to Bob +directories:/photos#access@Bob // the following tuples are defined implicitly through subject set rewrites (not supported yet) directories:/photos#access@(directories:/photos#owner) @@ -58,8 +58,8 @@ directories:/photos#parent@(files:/photos/beach.jpg#_) directories:/photos#parent@(files:/photos/mountains.jpg#_) ``` -The user `maureen` now wants to manage `access` for the file `/photos/beach.jpg`. Therefore, the application uses the expand-API -to get a tree of everyone who has access to that file. +The user `Alice` now wants to manage `access` for the file `/photos/beach.jpg`. Therefore, the application uses the expand-API to +get a tree of everyone who has access to that file. :::note @@ -68,7 +68,7 @@ TLS. ::: - + ### Maximum tree depth diff --git a/docs/keto/guides/list-api-display-objects.mdx b/docs/keto/guides/list-api-display-objects.mdx index b0d869a95e..748ddd8590 100644 --- a/docs/keto/guides/list-api-display-objects.mdx +++ b/docs/keto/guides/list-api-display-objects.mdx @@ -47,20 +47,32 @@ coffee-break: - Patrik ``` -This is represented in Ory Keto by the following [relationships](../concepts/01_relation-tuples.mdx): +This is represented in Ory Keto by the following [OPL Namespace configuration](../index.mdx#ory-permission-language): -```keto-relation-tuples -chats:memes#member@PM -chats:memes#member@Vincent -chats:memes#member@Julia +```ts +class User implements Namespace {} -chats:cars#member@PM -chats:cars#member@Julia +class Chat implements Namespace { + related: { + member: User[] + } +} +``` + +and with the following [relationships](../concepts/01_relation-tuples.mdx): + +```keto-tuples +Chat:memes#member@User:PM +Chat:memes#member@User:Vincent +Chat:memes#member@User:Julia + +Chat:cars#member@User:PM +Chat:cars#member@User:Julia -chats:coffee-break#member@PM -chats:coffee-break#member@Vincent -chats:coffee-break#member@Julia -chats:coffee-break#member@Patrik +Chat:coffee-break#member@User:PM +Chat:coffee-break#member@User:Vincent +Chat:coffee-break#member@User:Julia +Chat:coffee-break#member@User:Patrik ``` The user `PM` now opens the chat application. To display a list of all of `PM`'s chats, the application uses Keto's list API. @@ -72,7 +84,7 @@ TLS. ::: - + As a response, the application gets the list of all chats the user `PM` is a member of. It can then use the information to build the UI. @@ -92,7 +104,7 @@ is allowed to list the members of a group. This step isn't part of this example. In our example, a user wants to see who is a member of the `coffee-break` group: - + ## Application context diff --git a/docs/keto/guides/rbac.mdx b/docs/keto/guides/rbac.mdx index 558a4eb366..0bab9f4bf0 100644 --- a/docs/keto/guides/rbac.mdx +++ b/docs/keto/guides/rbac.mdx @@ -58,13 +58,15 @@ namespaces: name: groups - id: 1 name: reports + - id: 1 + name: reports #... ``` We can have two types of permission to access reports for granularity. Let's assume that we need `edit` and `view` access to the reports. -```keto-relation-tuples +```keto-tuples // View only access for finance department reports:finance#view@(groups:finance#member) // View only access for community department @@ -83,7 +85,7 @@ reports:marketing#view@(groups:admin#member) Let's assume that we have four people in our organization. Lila is CFO and needs access to financial reports, Hadley works in marketing, and Dilan works as a community manager. Neel is an admin of a system and needs to have edit permissions for reports. -```keto-relation-tuples +```keto-tuples groups:finance#member@Lila groups:community#member@Dilan groups:marketing#member@Hadley @@ -94,7 +96,7 @@ groups:admin#member@Neel Let's copy all permissions we created to a `policies.rts` file with the following content. -```keto-relation-tuples +```keto-tuples reports:finance#view@(groups:finance#member) reports:community#view@(groups:community#member) reports:marketing#view@(groups:marketing#member) @@ -114,7 +116,7 @@ Then we can run ```bash keto relation-tuple parse policies.rts --format json | \ - keto relation-tuple create - >/dev/null \ + keto relation-tuple create -f - >/dev/null \ && echo "Successfully created tuple" \ || echo "Encountered error" ``` @@ -132,7 +134,7 @@ Denied Now Dilan decided to also work with marketing. Therefore we need to update his permissions and add him to the marketing group. -```keto-relation-tuples +```keto-tuples groups:marketing#member@Dilan ``` diff --git a/docs/keto/quickstart.mdx b/docs/keto/quickstart.mdx index 9992992c61..1104d69e6b 100644 --- a/docs/keto/quickstart.mdx +++ b/docs/keto/quickstart.mdx @@ -40,22 +40,22 @@ docker-compose -f contrib/cat-videos-example/docker-compose.yml up # output: all initially created relationships -# NAMESPACE OBJECT RELATION NAME SUBJECT -# videos /cats/1.mp4 owner videos:/cats#owner -# videos /cats/1.mp4 view videos:/cats/1.mp4#owner -# videos /cats/1.mp4 view * -# videos /cats/2.mp4 owner videos:/cats#owner -# videos /cats/2.mp4 view videos:/cats/2.mp4#owner -# videos /cats owner cat lady -# videos /cats view videos:/cats#owner +# NAMESPACE OBJECT RELATION NAME SUBJECT +# Video /cats/1.mp4 owner Video:/cats#owner +# Video /cats/1.mp4w view Video:/cats/1.mp4#owner +# Video /cats/1.mp4 view User:* +# Video /cats/2.mp4 owner Video:/cats#owner +# Video /cats/2.mp4 view Video:/cats/2.mp4#owner +# Video /cats owner User:Alice +# Video /cats view Video:/cats#owner ``` ## State of the system -At the current state only one user with the username `cat lady` has added videos. Both videos are in the `/cats` directory owned -by `cat lady`. The file `/cats/1.mp4` can be viewed by anyone (`*`), while `/cats/2.mp4` has no extra sharing options, and can -therefore only be viewed by its owner, `cat lady`. The relationship definitions are located in the -`contrib/cat-videos-example/relation-tuples` directory. +At the current state only one user with the username `Alice` has added videos. Both videos are in the `/cats` directory owned by +`Alice`. The file `/cats/1.mp4` can be viewed by anyone (`*`), while `/cats/2.mp4` has no extra sharing options, and can therefore +only be viewed by its owner, `Alice`. The relationship definitions are located in the `contrib/cat-videos-example/relation-tuples` +directory. ## Simulating the video sharing application @@ -65,7 +65,7 @@ will use the Keto CLI client. If you want to run the Keto CLI within **Docker**, set the alias ```shell -alias keto="docker run -it --network cat-videos-example_default -e KETO_READ_REMOTE=\"keto:4466\" oryd/keto:v0.7.0-alpha.1" +alias keto="docker run -it --network cat-videos-example_default -e KETO_READ_REMOTE=\"keto:4466\" oryd/keto:v25.4.0" ``` in your terminal session. Alternatively, you need to set the remote endpoint so that the Keto CLI knows where to connect to (not @@ -82,7 +82,7 @@ operation should be allowed or denied. ```shell # Is "*" allowed to "view" the object "videos":"/cats/2.mp4"? -keto check "*" view videos /cats/2.mp4 +keto check "User:*" view Video /cats/2.mp4 --insecure-disable-transport-security # output: # Denied @@ -90,51 +90,49 @@ keto check "*" view videos /cats/2.mp4 We already discussed that this request should be denied, but it's always good to see this in action. -Now `cat lady` wants to change some view permissions of `/cats/1.mp4`. For this, the video service application has to show all -users that are allowed to view the video. It uses Keto's [expand-API](./concepts/25_api-overview.mdx#expand-subject-sets) to get -these data: +Now `Alice` wants to change some view permissions of `/cats/1.mp4`. For this, the video service application has to show all users +that are allowed to view the video. It uses Keto's [expand-API](./concepts/25_api-overview.mdx#expand-subject-sets) to get these +data: ```shell # Who is allowed to "view" the object "videos":"/cats/2.mp4"? -keto expand view videos /cats/1.mp4 +keto expand view Video /cats/1.mp4 # output: - -# ∪ videos:/cats/1.mp4#view -# ├─ ∪ videos:/cats/1.mp4#owner -# │ ├─ ∪ videos:/cats#owner -# │ │ ├─ ☘ cat lady️ -# ├─ ☘ *️ ``` -Here we can see the full subject set expansion. The first branch - -```keto-relation-tuples -videos:/cats/1.mp4#view +```keto-tuples +# or :#@Video:/cats/1.mp4#view +# ├──∋ :#@User:*️ +# └──or :#@Video:/cats/1.mp4#owner +# └──or :#@Video:/cats#owner +# └──∋ :#@User:Alice️ ``` -indicates that every owner of the object is allowed to view +Here we can see the full subject set expansion. The second branch -```keto-relation-tuples -videos:/cats/1.mp4#owner +```keto-tuples +Video:/cats/1.mp4#owner ``` -In the next step we see that the object's owners are the owners of `/cats` +indicates that every owner of the object is allowed to view the Object. + +In the next step we see that the object's owners are the owners of `Video:/cats` -```keto-relation-tuples -videos:/cats#owner +```keto-tuples +Video:/cats#owner ``` -We see that `cat lady` is the owner of `/cats`. +On the last line, We see that `Alice` is the owner of `/cats`. -Note that there is no direct relationship that would grant `cat lady` view access on `/cats/1.mp4` as this is indirectly defined -via the ownership relation. +Note that there is no direct relationship that would grant `Alice` view access on `/cats/1.mp4` as this is indirectly defined via +the ownership relation. -The special user `*` on the other hand was directly granted view access on the object, as it's a first-level leaf of the expansion -tree. The following CLI command proves that this is the case: +The special user `User:*` on the other hand was directly granted view access on the object, as it's a first-level leaf of the +expansion tree. The following CLI command proves that this is the case: ```shell -# Is "*" allowed to "view" the object "videos":"/cats/1.mp4"? -keto check "*" view videos /cats/1.mp4 +# Is "*" allowed to "view" the object "Video":"/cats/1.mp4"? +keto check "User:*" view Video /cats/1.mp4 --insecure-disable-transport-security # output: # Allowed @@ -146,21 +144,21 @@ import Mermaid from "@site/src/theme/Mermaid" r2 - r1 ----- sub1(["cat lady"]) + r1 ----- sub1(["Alice"]) r1 -.-> r4 obj1 ---- r4((owner)) r3 --- all(["* (anyone)"]) r4 -.-> r3 - obj1[videos:/cats/1.mp4] --- r3((view)) + obj1[Video:/cats/1.mp4] --- r3((view)) obj2 --- r6((view)) r1 -.-> r5 obj2[videos/cats/2.mp4] ---- r5((owner)) r5 -.-> r6 - style sub1 fill:lightgreen - style all fill:lightgreen + style sub1 fill:green,color:white + style all fill:green,color:white style r1 fill:darkgreen,color:white style r2 fill:darkgreen,color:white style r3 fill:darkgreen,color:white diff --git a/src/theme/CodeTabs/index.js b/src/theme/CodeTabs/index.js index 02a3948bbf..da19a33cd5 100644 --- a/src/theme/CodeTabs/index.js +++ b/src/theme/CodeTabs/index.js @@ -6,7 +6,7 @@ import Tabs from "@theme/Tabs" import TabItem from "@theme/TabItem" import CodeFromRemote from "../CodeFromRemote" -const CodeTabs = ({ sampleId, version }) => ( +const CodeTabs = ({ sampleId, version, outputExt = "txt" }) => ( <> ( diff --git a/src/theme/ketoRelationTuplesPrism.js b/src/theme/ketoRelationTuplesPrism.js index 0326f11a7a..27b6e6ac2e 100644 --- a/src/theme/ketoRelationTuplesPrism.js +++ b/src/theme/ketoRelationTuplesPrism.js @@ -1,60 +1,87 @@ // Copyright © 2022 Ory Corp // SPDX-License-Identifier: Apache-2.0 -const delimiter = { - delimiter: /[:#@()]/, -} - -const namespace = { - pattern: /[^:#@()\n]+:/, - inside: { - ...delimiter, - namespace: /.*/, - }, -} - -const object = { - pattern: /[^:#@()\n]+#/, - inside: { - ...delimiter, - "property-access": /.*/, - }, -} +/** + * Prism language for Keto relation tuples + * + * Format: Object#permit@Subject + * + * Examples: + * - Document:X#owner@User:Bob + * - Group:group1#members@Group:group2 + * - Document:Xyz#viewers@Group:Eng#members + */ -const relation = { - pattern: /[^:#@()\n]+/, -} +export default (prism) => { + // Reusable regex fragments + const namespace = "[A-Za-z][a-zA-Z0-9_-]*" + const id = "[^@#:\\n]+" + const relation = "(?:#[a-z][a-zA-Z0-9_-]*)" -const subjectID = { - pattern: /@[^:#@()\n]+/, - inside: { - ...delimiter, - subject: /.*/, - }, -} + // Shared inside: Namespace:Id + const nsDelimId = { + namespace: new RegExp(`${namespace}(?=:)`), + delimiter: /:/, + id: new RegExp(id), + } -const subjectSet = { - pattern: /@\(([^:#@()\n]+:)?([^:#@()\n]+)#([^:#@()\n]*)\)/, - inside: { - delimiter: /[@()]*/, - namespace, - object, - relation, - }, -} + // Shared inside: subject with relation (Namespace:Id#relation) + const subjectRelInside = { + ...nsDelimId, + id: /[^@#:\n]+(?=#)/, + keyword: /#/, + subjectRelation: /[a-z][a-zA-Z0-9_-]*$/, + } -export default (prism) => - (prism.languages["keto-relation-tuples"] = { + prism.languages["keto-tuples"] = { comment: /\/\/.*(\n|$)/, - "relation-tuple": { - pattern: - /([^:#@()\n]+:)?([^:#@()\n]+)#([^:#@()\n]+)@?((\(([^:#@()\n]+:)?([^:#@()\n]+)#([^:#@()\n]*)\))|([^:#@()\n]+))/, + // Tuple: Ns:Id#permit, optionally followed by @Ns:Id(#relation)? + tuple: { + pattern: new RegExp( + `(? { - prism.languages["keto-relationships"] = { + prism.languages["keto-natural"] = { comment: /\/\/.*(\n|$)/, - relationship: { + // Placeholder-based declarative sentences (match first, more specific) + "natural-placeholder": { + pattern: + /(?=.*<(?:Subject|relation|Object)>)(?:(?:[a-z][a-zA-Z0-9_-]* (?:of|in) )?[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+|) (?:is|are)(?: in)? (?:[a-z][a-zA-Z0-9_-]*|) (?:of|on) (?:[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+|)/, + alias: "natural", + inside: { + // Placeholder + "placeholder-subject": { + pattern: //, + alias: "subject", + }, + // Placeholder + "placeholder-object": { + pattern: //, + alias: "object", + }, + // Placeholder + "placeholder-relation": { + pattern: //, + }, + // Subject: can be "relation of Namespace:Id" or "relation in Namespace:Id" or just "Namespace:Id" (at start) + subject: { + pattern: + /^(?:[a-z][a-zA-Z0-9_-]* (?:of|in) )?[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+/, + inside: { + subjectRelation: /^[a-z][a-zA-Z0-9_-]*(?= (?:of|in))/, + keyword: /\b(?:of|in)\b/, + namespace: /[A-Za-z][a-zA-Z0-9_-]*(?=:)/, + delimiter: /:/, + id: /[^@#:\s]+$/, + }, + }, + // Object: always "Namespace:Id" (at end) - match before permit + object: { + pattern: /[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+$/, + inside: { + namespace: /^[A-Za-z][a-zA-Z0-9_-]*/, + delimiter: /:/, + id: /[^@#:\s]+$/, + }, + }, + // Regular relation word (not placeholder) + subjectRelation: /[a-z][a-zA-Z0-9_-]*(?= (?:of|on))/, + // Keywords - match last + keyword: /\b(?:is|are|in|of|on)\b/, + }, + }, + // Declarative relationship sentences + natural: { pattern: - /\(?([a-zA-Z0-9-_]+\s+o[fn]\s+)?[a-zA-Z0-9-_]+:[a-zA-Z0-9-_]+\)?\s+((is( in)?)|are)\s+[a-zA-Z0-9-_]+\s+o[fn]\s+[a-zA-Z0-9-_]+:[a-zA-Z0-9-_]+/, + /(?:[a-z][a-zA-Z0-9_-]* (?:of|in) )?[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+ (?:is|are)(?: in)? [a-z][a-zA-Z0-9_-]* (?:of|on) [A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+/, inside: { - subject: relationSubject, - "": relationAndObject, + // Subject: can be "relation of Namespace:Id" or "relation in Namespace:Id" or just "Namespace:Id" (at start) + subject: { + pattern: + /^(?:[a-z][a-zA-Z0-9_-]* (?:of|in) )?[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+/, + inside: { + subjectRelation: /^[a-z][a-zA-Z0-9_-]*(?= (?:of|in))/, + keyword: /\b(?:of|in)\b/, + namespace: /[A-Za-z][a-zA-Z0-9_-]*(?=:)/, + delimiter: /:/, + id: /[^@#:\s]+$/, + }, + }, + // Object: always "Namespace:Id" (at end) - match before permit + object: { + pattern: /[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+$/, + inside: { + namespace: /^[A-Za-z][a-zA-Z0-9_-]*/, + delimiter: /:/, + id: /[^@#:\s]+$/, + }, + }, + // Permit (the action/role) - match before keywords + permit: /[a-z][a-zA-Z0-9_-]*(?= (?:of|on))/, + // Keywords - match last + keyword: /\b(?:is|are|in|of|on)\b/, }, }, - "permission-question": { + // Permission question sentences + "natural-check": { pattern: - /(is|are)\s+\(?([a-zA-Z0-9-_]+\s+o[fn]\s+)?[a-zA-Z0-9-_]+:[a-zA-Z0-9-_]+\)?\s+(allowed to|in)\s+[a-zA-Z0-9-_]+\s+o[fn]\s+[a-zA-Z0-9-_]+:[a-zA-Z0-9-_]+\??/, + /(?:is|are) (?:[a-z][a-zA-Z0-9_-]* (?:of|in) )?[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+ (?:allowed to|in) [a-z][a-zA-Z0-9_-]* (?:of|on) [A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+/, inside: { - delimiter: /( allowed to )|\?/, - subject: permissionSubject, - "": relationAndObject, + // Subject (comes after is/are) + subject: { + pattern: + /(?<=(?:is|are) )(?:[a-z][a-zA-Z0-9_-]* (?:of|in) )?[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+/, + inside: { + subjectRelation: /^[a-z][a-zA-Z0-9_-]*(?= (?:of|in))/, + keyword: /\b(?:of|in)\b/, + namespace: /[A-Za-z][a-zA-Z0-9_-]*(?=:)/, + delimiter: /:/, + id: /[^@#:\s]+$/, + }, + }, + // Object (at end) - match before permit + object: { + pattern: /[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+$/, + inside: { + namespace: /^[A-Za-z][a-zA-Z0-9_-]*/, + delimiter: /:/, + id: /[^@#:\s]+$/, + }, + }, + // Permit - match before keywords + permit: /[a-z][a-zA-Z0-9_-]*(?= (?:of|on))/, + // Keywords (including the starting is/are) - match last + keyword: /\b(?:is|are|allowed to|in|of|on)\b/, }, }, } diff --git a/src/utils/prismDark.mjs b/src/utils/prismDark.mjs index 10915ac98f..6ba74f0cd2 100644 --- a/src/utils/prismDark.mjs +++ b/src/utils/prismDark.mjs @@ -75,5 +75,48 @@ export default { color: "#4FC1FF", }, }, + { + languages: ["keto-tuples", "keto-natural"], + types: ["namespace"], + style: { + color: "#47B098", + fontWeight: "bold", + }, + }, + { + languages: ["keto-tuples", "keto-natural"], + types: ["id", "placeholder-subject", "placeholder-object"], + style: { + color: "#DDA0DD", + }, + }, + { + languages: ["keto-tuples", "keto-natural"], + types: ["permit", "placeholder-relation"], + style: { + color: "#ee8800", + }, + }, + { + languages: ["keto-tuples", "keto-natural"], + types: ["keyword"], + style: { + color: "#858585", + }, + }, + { + languages: ["keto-tuples", "keto-natural"], + types: ["subjectRelation"], + style: { + color: "#ee8800", + }, + }, + { + languages: ["keto-tuples", "keto-natural"], + types: ["delimiter"], + style: { + color: "#888888", + }, + }, ], } diff --git a/src/utils/prismLight.mjs b/src/utils/prismLight.mjs index 2ceabe3f73..c49256fb33 100644 --- a/src/utils/prismLight.mjs +++ b/src/utils/prismLight.mjs @@ -97,45 +97,46 @@ export default { }, }, { - languages: ["keto-relation-tuples", "keto-relationships"], + languages: ["keto-tuples", "keto-natural"], types: ["namespace"], style: { - color: "#666", + color: "#036363", + fontWeight: "bold", }, }, { - languages: ["keto-relation-tuples"], - types: ["object"], + languages: ["keto-tuples", "keto-natural"], + types: ["id", "placeholder-subject", "placeholder-object"], style: { - color: "#939", + color: "#993399", }, }, { - languages: ["keto-relation-tuples", "keto-relationships"], - types: ["relation"], + languages: ["keto-tuples", "keto-natural"], + types: ["permit", "placeholder-relation"], style: { - color: "#e80", + color: "#cf222e", }, }, { - languages: ["keto-relation-tuples", "keto-relationships"], - types: ["delimiter"], + languages: ["keto-tuples", "keto-natural"], + types: ["keyword"], style: { - color: "#555", + color: "#868686", }, }, { - languages: ["keto-relation-tuples"], - types: ["subject"], + languages: ["keto-tuples", "keto-natural"], + types: ["subjectRelation"], style: { - color: "#903", + color: "#cf222e", }, }, { - languages: ["keto-relationships"], - types: ["id"], + languages: ["keto-tuples", "keto-natural"], + types: ["delimiter"], style: { - color: "#939", + color: "#555", }, }, ], diff --git a/tests/jest/prism/__snapshots__/ketoRelationsPermissionsPrism.test.ts.snap b/tests/jest/prism/__snapshots__/ketoRelationsPermissionsPrism.test.ts.snap new file mode 100644 index 0000000000..e6739d5ae0 --- /dev/null +++ b/tests/jest/prism/__snapshots__/ketoRelationsPermissionsPrism.test.ts.snap @@ -0,0 +1,932 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ketoRelationsPermissionsPrism declarative: lowercase simple: user:bob is owner of document:x should tokenize: "user:bob is owner of document:x" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "user", + "length": 4, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "bob", + "length": 3, + "type": "id", + }, + ], + "length": 8, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "is", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "owner", + "length": 5, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": "of", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "document", + "length": 8, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "x", + "length": 1, + "type": "id", + }, + ], + "length": 10, + "type": "object", + }, + ], + "length": 31, + "type": "natural", + }, +] +`; + +exports[`ketoRelationsPermissionsPrism declarative: relation as subject with 'are in': viewers of Group:Eng are in readers of Document:Xyz should tokenize: "viewers of Group:Eng are in readers of Document:Xyz" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "viewers", + "length": 7, + "type": "subjectRelation", + }, + " ", + Token { + "alias": undefined, + "content": "of", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "Group", + "length": 5, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Eng", + "length": 3, + "type": "id", + }, + ], + "length": 20, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "are", + "length": 3, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "in", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "readers", + "length": 7, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": "of", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Document", + "length": 8, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Xyz", + "length": 3, + "type": "id", + }, + ], + "length": 12, + "type": "object", + }, + ], + "length": 51, + "type": "natural", + }, +] +`; + +exports[`ketoRelationsPermissionsPrism declarative: relation as subject with 'in': members in Group:Eng are viewers of Document:Xyz should tokenize: "members in Group:Eng are viewers of Document:Xyz" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "members", + "length": 7, + "type": "subjectRelation", + }, + " ", + Token { + "alias": undefined, + "content": "in", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "Group", + "length": 5, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Eng", + "length": 3, + "type": "id", + }, + ], + "length": 20, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "are", + "length": 3, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "viewers", + "length": 7, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": "of", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Document", + "length": 8, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Xyz", + "length": 3, + "type": "id", + }, + ], + "length": 12, + "type": "object", + }, + ], + "length": 48, + "type": "natural", + }, +] +`; + +exports[`ketoRelationsPermissionsPrism declarative: relation as subject: members of Group:Eng are viewers of Document:Xyz should tokenize: "members of Group:Eng are viewers of Document:Xyz" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "members", + "length": 7, + "type": "subjectRelation", + }, + " ", + Token { + "alias": undefined, + "content": "of", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "Group", + "length": 5, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Eng", + "length": 3, + "type": "id", + }, + ], + "length": 20, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "are", + "length": 3, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "viewers", + "length": 7, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": "of", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Document", + "length": 8, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Xyz", + "length": 3, + "type": "id", + }, + ], + "length": 12, + "type": "object", + }, + ], + "length": 48, + "type": "natural", + }, +] +`; + +exports[`ketoRelationsPermissionsPrism declarative: simple with 'on': User:Bob is owner on Document:X should tokenize: "User:Bob is owner on Document:X" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "User", + "length": 4, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Bob", + "length": 3, + "type": "id", + }, + ], + "length": 8, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "is", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "owner", + "length": 5, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": "on", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Document", + "length": 8, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "X", + "length": 1, + "type": "id", + }, + ], + "length": 10, + "type": "object", + }, + ], + "length": 31, + "type": "natural", + }, +] +`; + +exports[`ketoRelationsPermissionsPrism declarative: simple: User:Bob is owner of Document:X should tokenize: "User:Bob is owner of Document:X" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "User", + "length": 4, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Bob", + "length": 3, + "type": "id", + }, + ], + "length": 8, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "is", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "owner", + "length": 5, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": "of", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Document", + "length": 8, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "X", + "length": 1, + "type": "id", + }, + ], + "length": 10, + "type": "object", + }, + ], + "length": 31, + "type": "natural", + }, +] +`; + +exports[`ketoRelationsPermissionsPrism declarative: with 'is in': Group:group2 is in members of Group:group1 should tokenize: "Group:group2 is in members of Group:group1" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Group", + "length": 5, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "group2", + "length": 6, + "type": "id", + }, + ], + "length": 12, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "is", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "in", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "members", + "length": 7, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": "of", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Group", + "length": 5, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "group1", + "length": 6, + "type": "id", + }, + ], + "length": 12, + "type": "object", + }, + ], + "length": 42, + "type": "natural", + }, +] +`; + +exports[`ketoRelationsPermissionsPrism question: question with 'in': is User:Alice in viewers of Document:X should tokenize: "is User:Alice in viewers of Document:X" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "is", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "User", + "length": 4, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Alice", + "length": 5, + "type": "id", + }, + ], + "length": 10, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "in", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "viewers", + "length": 7, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": "of", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Document", + "length": 8, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "X", + "length": 1, + "type": "id", + }, + ], + "length": 10, + "type": "object", + }, + ], + "length": 38, + "type": "natural-check", + }, +] +`; + +exports[`ketoRelationsPermissionsPrism question: question with relation subject: are members of Group:XYZ allowed to view on Document:X should tokenize: "are members of Group:XYZ allowed to view on Document:X" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "are", + "length": 3, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "members", + "length": 7, + "type": "subjectRelation", + }, + " ", + Token { + "alias": undefined, + "content": "of", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "Group", + "length": 5, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "XYZ", + "length": 3, + "type": "id", + }, + ], + "length": 20, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "allowed to", + "length": 10, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "view", + "length": 4, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": "on", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Document", + "length": 8, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "X", + "length": 1, + "type": "id", + }, + ], + "length": 10, + "type": "object", + }, + ], + "length": 54, + "type": "natural-check", + }, +] +`; + +exports[`ketoRelationsPermissionsPrism question: simple question: is User:Bob allowed to view on Document:X should tokenize: "is User:Bob allowed to view on Document:X" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "is", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "User", + "length": 4, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Bob", + "length": 3, + "type": "id", + }, + ], + "length": 8, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "allowed to", + "length": 10, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "view", + "length": 4, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": "on", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Document", + "length": 8, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "X", + "length": 1, + "type": "id", + }, + ], + "length": 10, + "type": "object", + }, + ], + "length": 41, + "type": "natural-check", + }, +] +`; diff --git a/tests/jest/prism/ketoRelationTuplesPrism.test.ts b/tests/jest/prism/ketoRelationTuplesPrism.test.ts new file mode 100644 index 0000000000..aa42a77a38 --- /dev/null +++ b/tests/jest/prism/ketoRelationTuplesPrism.test.ts @@ -0,0 +1,310 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import Prism from "prismjs" +import type { Token } from "prismjs" +import ketoRelationTuplesPrism from "../../../src/theme/ketoRelationTuplesPrism" + +describe("ketoRelationTuplesPrism", () => { + beforeAll(() => { + ketoRelationTuplesPrism(Prism) + }) + + const testCases = [ + { + name: "simple: Document:X#owner@User:Bob", + input: "Document:X#owner@User:Bob", + expected: [ + { + type: "tuple", + content: [ + { + type: "object", + content: [ + { type: "namespace", content: "Document" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "X" }, + ], + }, + { type: "keyword", content: "#" }, + { type: "permit", content: "owner" }, + { type: "keyword", content: "@" }, + { + type: "subject", + content: [ + { type: "namespace", content: "User" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "Bob" }, + ], + }, + ], + }, + ] as Array, + }, + { + name: "simple lowercase: document:x#owner@user:bob", + input: "document:x#owner@user:bob", + expected: [ + { + type: "tuple", + content: [ + { + type: "object", + content: [ + { type: "namespace", content: "document" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "x" }, + ], + }, + { type: "keyword", content: "#" }, + { type: "permit", content: "owner" }, + { type: "keyword", content: "@" }, + { + type: "subject", + content: [ + { type: "namespace", content: "user" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "bob" }, + ], + }, + ], + }, + ] as Array, + }, + { + name: "simple: Group:group1#members@Group:group2", + input: "Group:group1#members@Group:group2", + expected: [ + { + type: "tuple", + content: [ + { + type: "object", + content: [ + { type: "namespace", content: "Group" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "group1" }, + ], + }, + { type: "keyword", content: "#" }, + { type: "permit", content: "members" }, + { type: "keyword", content: "@" }, + { + type: "subject", + content: [ + { type: "namespace", content: "Group" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "group2" }, + ], + }, + ], + }, + ] as Array, + }, + { + name: "subject with relation: Document:Xyz#viewers@Group:Eng#members", + input: "Document:Xyz#viewers@Group:Eng#members", + expected: [ + { + type: "tuple", + content: [ + { + type: "object", + content: [ + { type: "namespace", content: "Document" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "Xyz" }, + ], + }, + { type: "keyword", content: "#" }, + { type: "permit", content: "viewers" }, + { type: "keyword", content: "@" }, + { + type: "subject", + content: [ + { type: "namespace", content: "Group" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "Eng" }, + { type: "keyword", content: "#" }, + { type: "subjectRelation", content: "members" }, + ], + }, + ], + }, + ] as Array, + }, + { + name: "subject with relation: Document:Xyz#readers@Group:Eng#viewers", + input: "Document:Xyz#readers@Group:Eng#viewers", + expected: [ + { + type: "tuple", + content: [ + { + type: "object", + content: [ + { type: "namespace", content: "Document" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "Xyz" }, + ], + }, + { type: "keyword", content: "#" }, + { type: "permit", content: "readers" }, + { type: "keyword", content: "@" }, + { + type: "subject", + content: [ + { type: "namespace", content: "Group" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "Eng" }, + { type: "keyword", content: "#" }, + { type: "subjectRelation", content: "viewers" }, + ], + }, + ], + }, + ] as Array, + }, + { + name: "with special characters: File:doc-1_v2#read@User:alice_123", + input: "File:doc-1_v2#read@User:alice_123", + expected: [ + { + type: "tuple", + content: [ + { + type: "object", + content: [ + { type: "namespace", content: "File" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "doc-1_v2" }, + ], + }, + { type: "keyword", content: "#" }, + { type: "permit", content: "read" }, + { type: "keyword", content: "@" }, + { + type: "subject", + content: [ + { type: "namespace", content: "User" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "alice_123" }, + ], + }, + ], + }, + ] as Array, + }, + { + name: "partial: Document:X#owner (no subject)", + input: "Document:X#owner", + expected: [ + { + type: "tuple", + content: [ + { + type: "object", + content: [ + { type: "namespace", content: "Document" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "X" }, + ], + }, + { type: "keyword", content: "#" }, + { type: "permit", content: "owner" }, + ], + }, + ] as Array, + }, + { + name: "partial: Group:group1#members (no subject)", + input: "Group:group1#members", + expected: [ + { + type: "tuple", + content: [ + { + type: "object", + content: [ + { type: "namespace", content: "Group" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "group1" }, + ], + }, + { type: "keyword", content: "#" }, + { type: "permit", content: "members" }, + ], + }, + ] as Array, + }, + { + name: "standalone object: Document:X", + input: "Document:X", + expected: [ + { + type: "object", + content: [ + { type: "namespace", content: "Document" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "X" }, + ], + }, + ] as Array, + }, + { + name: "standalone subject: @User:Bob", + input: "@User:Bob", + expected: [ + { + type: "subject", + content: [ + { type: "keyword", content: "@" }, + { type: "namespace", content: "User" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "Bob" }, + ], + }, + ] as Array, + }, + { + name: "standalone subject with relation: @Group:Eng#members", + input: "@Group:Eng#members", + expected: [ + { + type: "subject", + content: [ + { type: "keyword", content: "@" }, + { type: "namespace", content: "Group" }, + { type: "delimiter", content: ":" }, + { type: "id", content: "Eng" }, + { type: "keyword", content: "#" }, + { type: "subjectRelation", content: "members" }, + ], + }, + ] as Array, + }, + ] + + // Helper to extract only type and content from tokens + function simplifyToken(token: string | Token): any { + if (typeof token === "string") { + return token + } + // Use alias if available, otherwise use type + const type = token.alias || token.type + return { + type, + content: Array.isArray(token.content) + ? token.content.map(simplifyToken) + : token.content, + } + } + + describe.each(testCases)("tuple: $name", ({ input, expected }) => { + it(`should tokenize: "${input}"`, () => { + const tokens = Prism.tokenize(input, Prism.languages["keto-tuples"]) + const simplified = tokens.map(simplifyToken) + expect(simplified).toEqual(expected) + }) + }) +}) diff --git a/tests/jest/prism/ketoRelationsPermissionsPrism.test.ts b/tests/jest/prism/ketoRelationsPermissionsPrism.test.ts new file mode 100644 index 0000000000..5c1ef2888b --- /dev/null +++ b/tests/jest/prism/ketoRelationsPermissionsPrism.test.ts @@ -0,0 +1,71 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import Prism from "prismjs" +import ketoRelationsPermissionsPrism from "../../../src/theme/ketoRelationsPermissionsPrism" + +describe("ketoRelationsPermissionsPrism", () => { + beforeAll(() => { + ketoRelationsPermissionsPrism(Prism) + }) + + const declarativeTestCases = [ + { + name: "lowercase simple: user:bob is owner of document:x", + input: "user:bob is owner of document:x", + }, + { + name: "simple: User:Bob is owner of Document:X", + input: "User:Bob is owner of Document:X", + }, + { + name: "simple with 'on': User:Bob is owner on Document:X", + input: "User:Bob is owner on Document:X", + }, + { + name: "with 'is in': Group:group2 is in members of Group:group1", + input: "Group:group2 is in members of Group:group1", + }, + { + name: "relation as subject: members of Group:Eng are viewers of Document:Xyz", + input: "members of Group:Eng are viewers of Document:Xyz", + }, + { + name: "relation as subject with 'are in': viewers of Group:Eng are in readers of Document:Xyz", + input: "viewers of Group:Eng are in readers of Document:Xyz", + }, + { + name: "relation as subject with 'in': members in Group:Eng are viewers of Document:Xyz", + input: "members in Group:Eng are viewers of Document:Xyz", + }, + ] + + const questionTestCases = [ + { + name: "simple question: is User:Bob allowed to view on Document:X", + input: "is User:Bob allowed to view on Document:X", + }, + { + name: "question with 'in': is User:Alice in viewers of Document:X", + input: "is User:Alice in viewers of Document:X", + }, + { + name: "question with relation subject: are members of Group:XYZ allowed to view on Document:X", + input: "are members of Group:XYZ allowed to view on Document:X", + }, + ] + + describe.each(declarativeTestCases)("declarative: $name", ({ input }) => { + it(`should tokenize: "${input}"`, () => { + const tokens = Prism.tokenize(input, Prism.languages["keto-natural"]) + expect(tokens).toMatchSnapshot() + }) + }) + + describe.each(questionTestCases)("question: $name", ({ input }) => { + it(`should tokenize: "${input}"`, () => { + const tokens = Prism.tokenize(input, Prism.languages["keto-natural"]) + expect(tokens).toMatchSnapshot() + }) + }) +}) From dbc5f212cbf06e78ba241ab4e7379d4459256541 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Tue, 17 Feb 2026 12:00:24 +0100 Subject: [PATCH 02/10] fix typo --- docs/keto/quickstart.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/keto/quickstart.mdx b/docs/keto/quickstart.mdx index 1104d69e6b..5e8d626350 100644 --- a/docs/keto/quickstart.mdx +++ b/docs/keto/quickstart.mdx @@ -42,7 +42,7 @@ docker-compose -f contrib/cat-videos-example/docker-compose.yml up # NAMESPACE OBJECT RELATION NAME SUBJECT # Video /cats/1.mp4 owner Video:/cats#owner -# Video /cats/1.mp4w view Video:/cats/1.mp4#owner +# Video /cats/1.mp4 view Video:/cats/1.mp4#owner # Video /cats/1.mp4 view User:* # Video /cats/2.mp4 owner Video:/cats#owner # Video /cats/2.mp4 view Video:/cats/2.mp4#owner From cc83943e06d0a5010fdc4ca4928d24c83783efc5 Mon Sep 17 00:00:00 2001 From: Davud Safarov Date: Tue, 17 Feb 2026 13:05:37 +0100 Subject: [PATCH 03/10] Update docs/guides/permissions/overview.mdx Co-authored-by: Arne Luenser --- docs/guides/permissions/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/permissions/overview.mdx b/docs/guides/permissions/overview.mdx index 7365ec121b..9b713ee8d7 100644 --- a/docs/guides/permissions/overview.mdx +++ b/docs/guides/permissions/overview.mdx @@ -43,7 +43,7 @@ members of Group:group1 are readers of File:file1 ``` As you can see, the subject of a relationship can either be a specific subject ID (e.g. User:user1), or subjects defined through -an [indirection](https://en.wikipedia.org/wiki/Indirection) (e.g. all members of Group:group1). The object is referenced by its ID +an indirection (e.g. all members of Group:group1). The object is referenced by its ID (e.g. Group:group1, File:file1). ### Checking permissions From 68a6fc44a02f262a98aebdee7093be138be13cc7 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Tue, 17 Feb 2026 13:13:35 +0100 Subject: [PATCH 04/10] format --- docs/guides/permissions/overview.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/guides/permissions/overview.mdx b/docs/guides/permissions/overview.mdx index 9b713ee8d7..2e65a5af8e 100644 --- a/docs/guides/permissions/overview.mdx +++ b/docs/guides/permissions/overview.mdx @@ -43,8 +43,7 @@ members of Group:group1 are readers of File:file1 ``` As you can see, the subject of a relationship can either be a specific subject ID (e.g. User:user1), or subjects defined through -an indirection (e.g. all members of Group:group1). The object is referenced by its ID -(e.g. Group:group1, File:file1). +an indirection (e.g. all members of Group:group1). The object is referenced by its ID (e.g. Group:group1, File:file1). ### Checking permissions From 2abc3958c5617cac86802c7eacbaa253f9fad012 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Tue, 17 Feb 2026 14:48:21 +0100 Subject: [PATCH 05/10] review comments --- docs/guides/permissions/overview.mdx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/guides/permissions/overview.mdx b/docs/guides/permissions/overview.mdx index 2e65a5af8e..2aa1ec54d9 100644 --- a/docs/guides/permissions/overview.mdx +++ b/docs/guides/permissions/overview.mdx @@ -34,23 +34,24 @@ Each relationship consists of: - **Subject** – The entity that receives the role or permission. - **Object** – The entity on which the role or permission is granted. +- **Relation** – The relationship between Subject and Object. Examples: ```keto-natural User:user1 is in members of Group:group1 -members of Group:group1 are readers of File:file1 +admins of Group:group1 are readers of File:file1 ``` As you can see, the subject of a relationship can either be a specific subject ID (e.g. User:user1), or subjects defined through -an indirection (e.g. all members of Group:group1). The object is referenced by its ID (e.g. Group:group1, File:file1). +an indirection (e.g. all admins of Group:group1). The object is referenced by its ID (e.g. Group:group1, File:file1). ### Checking permissions Permissions are just another form of relations. Therefore, a permission check is a request to check whether a subject has a certain relation to an object, possibly through one or more indirections. -As a very simple example, leat's assume the following tuples exist: +As a very simple example, let's assume the following tuples exist: ```keto-natural User:user1 is in members of Group:group1 @@ -59,7 +60,7 @@ members of Group:group1 are readers of File:file1 Based on these tuples, you can run permission checks: -- Is `user1` a `reader` of `file1`? +- Is `User:user1` a `reader` of `File:file1`? ```keto-natural is User:user1 in readers of File:file1? // Yes @@ -67,13 +68,13 @@ Based on these tuples, you can run permission checks: Yes, because `user1` is in `members` of `Group:group1` and all `members` of `Group:group1` are `readers` of `File:file1`. -- Is `user2` a member of `group1`? +- Is `User:user2` a member of `Group:group1`? ```keto-natural is users:user2 in members of Group:group1? // No ``` - No, because there is no relation between `user2` and `group1`. + No, because there is no relation between `User:user2` and `Group:group1`. ## Example From 52e00df8da7fc0dbff871cc26b8265cd4e6befa0 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Wed, 18 Feb 2026 14:42:38 +0100 Subject: [PATCH 06/10] add missing disable-transport flag. improve prism --- docs/keto/quickstart.mdx | 2 +- src/theme/ketoRelationsPermissionsPrism.js | 14 +- ...ketoRelationsPermissionsPrism.test.ts.snap | 182 ++++++++++++++++++ .../ketoRelationsPermissionsPrism.test.ts | 8 + 4 files changed, 199 insertions(+), 7 deletions(-) diff --git a/docs/keto/quickstart.mdx b/docs/keto/quickstart.mdx index 5e8d626350..b41b8f8d92 100644 --- a/docs/keto/quickstart.mdx +++ b/docs/keto/quickstart.mdx @@ -96,7 +96,7 @@ data: ```shell # Who is allowed to "view" the object "videos":"/cats/2.mp4"? -keto expand view Video /cats/1.mp4 +keto expand view Video /cats/1.mp4 --insecure-disable-transport-security # output: ``` diff --git a/src/theme/ketoRelationsPermissionsPrism.js b/src/theme/ketoRelationsPermissionsPrism.js index d66698cb7b..1597d24a07 100644 --- a/src/theme/ketoRelationsPermissionsPrism.js +++ b/src/theme/ketoRelationsPermissionsPrism.js @@ -9,6 +9,8 @@ * - Group:group2 is in members of Group:group1 * - members of Group:Eng are viewers of Document:Xyz * - viewers of Group:Eng are in readers of Document:Xyz + * - User:Bob is allowed to read Document:X + * - members of Group:Eng is allowed to read Document:X * * Question sentences: * - is User:Bob allowed to view on Document:X @@ -69,7 +71,7 @@ export default (prism) => { // Declarative relationship sentences natural: { pattern: - /(?:[a-z][a-zA-Z0-9_-]* (?:of|in) )?[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+ (?:is|are)(?: in)? [a-z][a-zA-Z0-9_-]* (?:of|on) [A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+/, + /(?:[a-z][a-zA-Z0-9_-]* (?:of|in) )?[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+ (?:(?:is|are)(?: in)? [a-z][a-zA-Z0-9_-]* (?:of|on)|(?:is|are) allowed to [a-z][a-zA-Z0-9_-]*) [A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+/, inside: { // Subject: can be "relation of Namespace:Id" or "relation in Namespace:Id" or just "Namespace:Id" (at start) subject: { @@ -83,7 +85,7 @@ export default (prism) => { id: /[^@#:\s]+$/, }, }, - // Object: always "Namespace:Id" (at end) - match before permit + // Object: always "Namespace:Id" (at end) object: { pattern: /[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+$/, inside: { @@ -92,10 +94,10 @@ export default (prism) => { id: /[^@#:\s]+$/, }, }, - // Permit (the action/role) - match before keywords - permit: /[a-z][a-zA-Z0-9_-]*(?= (?:of|on))/, - // Keywords - match last - keyword: /\b(?:is|are|in|of|on)\b/, + // Keywords - match before permit so permit only matches the remaining word + keyword: /\b(?:is|are|allowed to|in|of|on)\b/, + // Permit (the action/role) - matches any remaining lowercase word + permit: /[a-z][a-zA-Z0-9_-]*/, }, }, // Permission question sentences diff --git a/tests/jest/prism/__snapshots__/ketoRelationsPermissionsPrism.test.ts.snap b/tests/jest/prism/__snapshots__/ketoRelationsPermissionsPrism.test.ts.snap index e6739d5ae0..79e3b28010 100644 --- a/tests/jest/prism/__snapshots__/ketoRelationsPermissionsPrism.test.ts.snap +++ b/tests/jest/prism/__snapshots__/ketoRelationsPermissionsPrism.test.ts.snap @@ -930,3 +930,185 @@ exports[`ketoRelationsPermissionsPrism question: simple question: is User:Bob al }, ] `; + +exports[`ketoRelationsPermissionsPrism declarative: allowed to simple: User:Bob is allowed to read Document:X should tokenize: "User:Bob is allowed to read Document:X" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "User", + "length": 4, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Bob", + "length": 3, + "type": "id", + }, + ], + "length": 8, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "is", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "allowed to", + "length": 10, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "read", + "length": 4, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Document", + "length": 8, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "X", + "length": 1, + "type": "id", + }, + ], + "length": 10, + "type": "object", + }, + ], + "length": 38, + "type": "natural", + }, +] +`; + +exports[`ketoRelationsPermissionsPrism declarative: allowed to with subject relation: members of Group:Eng is allowed to read Document:X should tokenize: "members of Group:Eng is allowed to read Document:X" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "members", + "length": 7, + "type": "subjectRelation", + }, + " ", + Token { + "alias": undefined, + "content": "of", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "Group", + "length": 5, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Eng", + "length": 3, + "type": "id", + }, + ], + "length": 20, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "is", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "allowed to", + "length": 10, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "read", + "length": 4, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Document", + "length": 8, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "X", + "length": 1, + "type": "id", + }, + ], + "length": 10, + "type": "object", + }, + ], + "length": 50, + "type": "natural", + }, +] +`; diff --git a/tests/jest/prism/ketoRelationsPermissionsPrism.test.ts b/tests/jest/prism/ketoRelationsPermissionsPrism.test.ts index 5c1ef2888b..1fff4b115e 100644 --- a/tests/jest/prism/ketoRelationsPermissionsPrism.test.ts +++ b/tests/jest/prism/ketoRelationsPermissionsPrism.test.ts @@ -38,6 +38,14 @@ describe("ketoRelationsPermissionsPrism", () => { name: "relation as subject with 'in': members in Group:Eng are viewers of Document:Xyz", input: "members in Group:Eng are viewers of Document:Xyz", }, + { + name: "allowed to simple: User:Bob is allowed to read Document:X", + input: "User:Bob is allowed to read Document:X", + }, + { + name: "allowed to with subject relation: members of Group:Eng is allowed to read Document:X", + input: "members of Group:Eng is allowed to read Document:X", + }, ] const questionTestCases = [ From 44c0ee23565575c1cdf151d844bf48900f65ea40 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Fri, 20 Feb 2026 09:45:52 +0100 Subject: [PATCH 07/10] review comments --- docs/guides/permissions/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/permissions/overview.mdx b/docs/guides/permissions/overview.mdx index 2aa1ec54d9..353d85d310 100644 --- a/docs/guides/permissions/overview.mdx +++ b/docs/guides/permissions/overview.mdx @@ -71,7 +71,7 @@ Based on these tuples, you can run permission checks: - Is `User:user2` a member of `Group:group1`? ```keto-natural - is users:user2 in members of Group:group1? // No + is User:user2 in members of Group:group1? // No ``` No, because there is no relation between `User:user2` and `Group:group1`. From b7494c1b5d91ad7ad77ce8226b0d30494d2f2f51 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Fri, 20 Feb 2026 10:26:38 +0100 Subject: [PATCH 08/10] address review comments --- docs/keto/concepts/15_subjects.mdx | 6 +- docs/keto/quickstart.mdx | 16 ++-- src/theme/ketoRelationsPermissionsPrism.js | 4 +- ...ketoRelationsPermissionsPrism.test.ts.snap | 91 +++++++++++++++++++ .../ketoRelationsPermissionsPrism.test.ts | 4 + 5 files changed, 108 insertions(+), 13 deletions(-) diff --git a/docs/keto/concepts/15_subjects.mdx b/docs/keto/concepts/15_subjects.mdx index a61e90a8fe..5c1e7afd74 100644 --- a/docs/keto/concepts/15_subjects.mdx +++ b/docs/keto/concepts/15_subjects.mdx @@ -61,9 +61,9 @@ c5b6454f-f79c-4a6d-9e1b-b44e04b56009: Ory Permissions understands the following relationship: -```keto-tuples -// Allow access to TCP port 22 when the request originates from a specific subnet during office hours -TcpPort:22#access@UserAttributes:c5b6454f-f79c-4a6d-9e1b-b44e04b56009 + +```keto-natural +UserAttributes:c5b6454f-f79c-4a6d-9e1b-b44e04b56009 is allowed to access TcpPort:22 ``` The application must map every incoming request to a subject string that represents the attributes of the request. Ory Permissions diff --git a/docs/keto/quickstart.mdx b/docs/keto/quickstart.mdx index b41b8f8d92..b74d9e9c89 100644 --- a/docs/keto/quickstart.mdx +++ b/docs/keto/quickstart.mdx @@ -52,9 +52,9 @@ docker-compose -f contrib/cat-videos-example/docker-compose.yml up ## State of the system -At the current state only one user with the username `Alice` has added videos. Both videos are in the `/cats` directory owned by -`Alice`. The file `/cats/1.mp4` can be viewed by anyone (`*`), while `/cats/2.mp4` has no extra sharing options, and can therefore -only be viewed by its owner, `Alice`. The relationship definitions are located in the `contrib/cat-videos-example/relation-tuples` +At the current state only `User:Alice` has added videos. Both videos are in the `/cats` directory owned by +`User:Alice`. The file `File:/cats/1.mp4` can be viewed by anyone (`User:*`), while `Video:/cats/2.mp4` has no extra sharing options, and can therefore +only be viewed by its owner, `User:Alice`. The relationship definitions are located in the `contrib/cat-videos-example/relation-tuples` directory. ## Simulating the video sharing application @@ -90,7 +90,7 @@ keto check "User:*" view Video /cats/2.mp4 --insecure-disable-transport-security We already discussed that this request should be denied, but it's always good to see this in action. -Now `Alice` wants to change some view permissions of `/cats/1.mp4`. For this, the video service application has to show all users +Now `User:Alice` wants to change some view permissions of `Video:/cats/1.mp4`. For this, the video service application has to show all users that are allowed to view the video. It uses Keto's [expand-API](./concepts/25_api-overview.mdx#expand-subject-sets) to get these data: @@ -122,7 +122,7 @@ In the next step we see that the object's owners are the owners of `Video:/cats` Video:/cats#owner ``` -On the last line, We see that `Alice` is the owner of `/cats`. +On the last line, we see that `User:Alice` is the owner of `Video:/cats`. Note that there is no direct relationship that would grant `Alice` view access on `/cats/1.mp4` as this is indirectly defined via the ownership relation. @@ -147,15 +147,15 @@ flowchart TD obj[Video:/cats] --- r1((owner)) obj --- r2((view)) r1 -.-> r2 - r1 ----- sub1(["Alice"]) + r1 ----- sub1(["User:Alice"]) r1 -.-> r4 obj1 ---- r4((owner)) - r3 --- all(["* (anyone)"]) + r3 --- all(["User:* (anyone)"]) r4 -.-> r3 obj1[Video:/cats/1.mp4] --- r3((view)) obj2 --- r6((view)) r1 -.-> r5 - obj2[videos/cats/2.mp4] ---- r5((owner)) + obj2[Video:/cats/2.mp4] ---- r5((owner)) r5 -.-> r6 style sub1 fill:green,color:white style all fill:green,color:white diff --git a/src/theme/ketoRelationsPermissionsPrism.js b/src/theme/ketoRelationsPermissionsPrism.js index 1597d24a07..564f5f5925 100644 --- a/src/theme/ketoRelationsPermissionsPrism.js +++ b/src/theme/ketoRelationsPermissionsPrism.js @@ -71,7 +71,7 @@ export default (prism) => { // Declarative relationship sentences natural: { pattern: - /(?:[a-z][a-zA-Z0-9_-]* (?:of|in) )?[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+ (?:(?:is|are)(?: in)? [a-z][a-zA-Z0-9_-]* (?:of|on)|(?:is|are) allowed to [a-z][a-zA-Z0-9_-]*) [A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+/, + /(?:[a-z][a-zA-Z0-9_-]* (?:of|in) )?[A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+ (?:(?:is|are)(?: in)? [a-z][a-zA-Z0-9_-]* (?:of|on)|(?:is|are) allowed to [a-z][a-zA-Z0-9_-]*(?: to)?) [A-Za-z][a-zA-Z0-9_-]*:[^@#:\s]+/, inside: { // Subject: can be "relation of Namespace:Id" or "relation in Namespace:Id" or just "Namespace:Id" (at start) subject: { @@ -95,7 +95,7 @@ export default (prism) => { }, }, // Keywords - match before permit so permit only matches the remaining word - keyword: /\b(?:is|are|allowed to|in|of|on)\b/, + keyword: /\b(?:is|are|allowed to|to|in|of|on)\b/, // Permit (the action/role) - matches any remaining lowercase word permit: /[a-z][a-zA-Z0-9_-]*/, }, diff --git a/tests/jest/prism/__snapshots__/ketoRelationsPermissionsPrism.test.ts.snap b/tests/jest/prism/__snapshots__/ketoRelationsPermissionsPrism.test.ts.snap index 79e3b28010..8d4b38eafe 100644 --- a/tests/jest/prism/__snapshots__/ketoRelationsPermissionsPrism.test.ts.snap +++ b/tests/jest/prism/__snapshots__/ketoRelationsPermissionsPrism.test.ts.snap @@ -1112,3 +1112,94 @@ exports[`ketoRelationsPermissionsPrism declarative: allowed to with subject rela }, ] `; + +exports[`ketoRelationsPermissionsPrism declarative: allowed to with 'to': User:Bob is allowed to access to Document:X should tokenize: "User:Bob is allowed to access to Document:X" 1`] = ` +[ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "User", + "length": 4, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "Bob", + "length": 3, + "type": "id", + }, + ], + "length": 8, + "type": "subject", + }, + " ", + Token { + "alias": undefined, + "content": "is", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "allowed to", + "length": 10, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": "access", + "length": 6, + "type": "permit", + }, + " ", + Token { + "alias": undefined, + "content": "to", + "length": 2, + "type": "keyword", + }, + " ", + Token { + "alias": undefined, + "content": [ + Token { + "alias": undefined, + "content": "Document", + "length": 8, + "type": "namespace", + }, + Token { + "alias": undefined, + "content": ":", + "length": 1, + "type": "delimiter", + }, + Token { + "alias": undefined, + "content": "X", + "length": 1, + "type": "id", + }, + ], + "length": 10, + "type": "object", + }, + ], + "length": 43, + "type": "natural", + }, +] +`; diff --git a/tests/jest/prism/ketoRelationsPermissionsPrism.test.ts b/tests/jest/prism/ketoRelationsPermissionsPrism.test.ts index 1fff4b115e..fc693b083c 100644 --- a/tests/jest/prism/ketoRelationsPermissionsPrism.test.ts +++ b/tests/jest/prism/ketoRelationsPermissionsPrism.test.ts @@ -46,6 +46,10 @@ describe("ketoRelationsPermissionsPrism", () => { name: "allowed to with subject relation: members of Group:Eng is allowed to read Document:X", input: "members of Group:Eng is allowed to read Document:X", }, + { + name: "allowed to with 'to': User:Bob is allowed to access to Document:X", + input: "User:Bob is allowed to access to Document:X", + }, ] const questionTestCases = [ From 09ea844832dc96ff85c7fe7644a32e6591655481 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Fri, 20 Feb 2026 10:41:18 +0100 Subject: [PATCH 09/10] address review comments --- docs/keto/concepts/15_subjects.mdx | 1 - docs/keto/concepts/20_graph-of-relations.mdx | 7 ------- docs/keto/quickstart.mdx | 14 +++++++------- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/docs/keto/concepts/15_subjects.mdx b/docs/keto/concepts/15_subjects.mdx index 5c1e7afd74..cf1289a03c 100644 --- a/docs/keto/concepts/15_subjects.mdx +++ b/docs/keto/concepts/15_subjects.mdx @@ -61,7 +61,6 @@ c5b6454f-f79c-4a6d-9e1b-b44e04b56009: Ory Permissions understands the following relationship: - ```keto-natural UserAttributes:c5b6454f-f79c-4a6d-9e1b-b44e04b56009 is allowed to access TcpPort:22 ``` diff --git a/docs/keto/concepts/20_graph-of-relations.mdx b/docs/keto/concepts/20_graph-of-relations.mdx index ad8299f5be..cf8354a056 100644 --- a/docs/keto/concepts/20_graph-of-relations.mdx +++ b/docs/keto/concepts/20_graph-of-relations.mdx @@ -24,13 +24,6 @@ Edges are directed and represent the relation between an object and subject. The following example translates a view relationships into a graph of relations. -:::note - -This example omits the [namespace](./05_namespaces.mdx) from all data to improve readability. In practice, the namespace always -has to be considered. - -::: - ```keto-tuples // User:1 has access on Dir:1 Dir:1#access@User:1 diff --git a/docs/keto/quickstart.mdx b/docs/keto/quickstart.mdx index b74d9e9c89..bf5d69fba8 100644 --- a/docs/keto/quickstart.mdx +++ b/docs/keto/quickstart.mdx @@ -52,10 +52,10 @@ docker-compose -f contrib/cat-videos-example/docker-compose.yml up ## State of the system -At the current state only `User:Alice` has added videos. Both videos are in the `/cats` directory owned by -`User:Alice`. The file `File:/cats/1.mp4` can be viewed by anyone (`User:*`), while `Video:/cats/2.mp4` has no extra sharing options, and can therefore -only be viewed by its owner, `User:Alice`. The relationship definitions are located in the `contrib/cat-videos-example/relation-tuples` -directory. +At the current state only `User:Alice` has added videos. Both videos are in the `/cats` directory owned by `User:Alice`. The file +`File:/cats/1.mp4` can be viewed by anyone (`User:*`), while `Video:/cats/2.mp4` has no extra sharing options, and can therefore +only be viewed by its owner, `User:Alice`. The relationship definitions are located in the +`contrib/cat-videos-example/relation-tuples` directory. ## Simulating the video sharing application @@ -90,9 +90,9 @@ keto check "User:*" view Video /cats/2.mp4 --insecure-disable-transport-security We already discussed that this request should be denied, but it's always good to see this in action. -Now `User:Alice` wants to change some view permissions of `Video:/cats/1.mp4`. For this, the video service application has to show all users -that are allowed to view the video. It uses Keto's [expand-API](./concepts/25_api-overview.mdx#expand-subject-sets) to get these -data: +Now `User:Alice` wants to change some view permissions of `Video:/cats/1.mp4`. For this, the video service application has to show +all users that are allowed to view the video. It uses Keto's [expand-API](./concepts/25_api-overview.mdx#expand-subject-sets) to +get these data: ```shell # Who is allowed to "view" the object "videos":"/cats/2.mp4"? From a971ac25c54dc06e38d1c4110e16de6e3e7d7977 Mon Sep 17 00:00:00 2001 From: DavudSafarli Date: Fri, 20 Feb 2026 10:59:49 +0100 Subject: [PATCH 10/10] remove -f flag --- docs/keto/guides/rbac.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/keto/guides/rbac.mdx b/docs/keto/guides/rbac.mdx index 0bab9f4bf0..c20183245a 100644 --- a/docs/keto/guides/rbac.mdx +++ b/docs/keto/guides/rbac.mdx @@ -58,8 +58,6 @@ namespaces: name: groups - id: 1 name: reports - - id: 1 - name: reports #... ``` @@ -116,7 +114,7 @@ Then we can run ```bash keto relation-tuple parse policies.rts --format json | \ - keto relation-tuple create -f - >/dev/null \ + keto relation-tuple create - >/dev/null \ && echo "Successfully created tuple" \ || echo "Encountered error" ```