keto icon indicating copy to clipboard operation
keto copied to clipboard

OPL traverse.(...) not working

Open DanielPFSeddi opened this issue 2 years ago • 9 comments

Preflight checklist

Describe the bug

I'm trying to check the permits using the traverse.(...) functionality with a very simple OPL model, but it always returns "allowed": false.

This is my OPL model:

import { Namespace, Context } from '@ory/keto-namespace-types';

class User implements Namespace {}

class Document implements Namespace {
  related: {
    viewers: User[];
    parents: Folder[];
  };

  permits = {
    view: (ctx: Context): boolean => this.related.parents.traverse((p) => p.permits.view(ctx)),
  };
}

class Folder implements Namespace {
  related: {
    viewers: User[];
  };

  permits = {
    view: (ctx: Context): boolean => this.related.viewers.includes(ctx.subject),
  };
}

And these are the relation-tuples that I have:

{
    "relation_tuples": [
        {
            "namespace": "Folder",
            "object": "folder_1",
            "relation": "viewers",
            "subject_id": "user_1"
        },
        {
            "namespace": "Document",
            "object": "document_1",
            "relation": "parents",
            "subject_id": "folder_1"
        }
    ],
    "next_page_token": ""
}

So, Folder:folder_1 is parent of Document:document_1, and User:user_1 is viewer of Folder:folder_1. I would expect that User:user_1 can view Document:document_1, but it returns false.

Reproducing the bug

When making the following request on Postman:

http://localhost:10006/relation-tuples/check?namespace=Document&object=document_1&subject_id=user_1&relation=view

I'm getting:

{
    "allowed": false
}

and I expect it to be true. I've also tried adding an extra query parameter like max-depth=10, but I'm getting the same result

Just for checking, when I make the following request:

http://localhost:10006/relation-tuples/check?namespace=Folder&object=folder_1&subject_id=user_1&relation=view

I'm getting:

{
    "allowed": true
}

Which is the espected return value, since User:user_1 is viewer of Folder:folder_1

Relevant log output

No response

Relevant configuration

I'm using a docker image: oryd/keto:latest (service_name=Ory Keto service_version=v0.11.1-alpha.0).

I'm mapping the ports:

- 10006:4466 //read
- 10007:4467 //write

Version

service_version=v0.11.1-alpha.0

On which operating system are you observing this issue?

Windows

In which environment are you deploying?

Docker Compose

Additional Context

No response

DanielPFSeddi avatar May 22 '23 11:05 DanielPFSeddi

Wait.... I've been struggling with something similar. My feeling is that the current version of keto is totally broken for anything other than one step removed.

bendoerr avatar May 23 '23 13:05 bendoerr

Hey, sorry to hear that there's an issue. The primary engineers are currently in completely different contexts but we'll try to get this sorted soon. We're currently swamped with some other work, but obviously it's not acceptable to have such faults.

aeneasr avatar May 25 '23 07:05 aeneasr

@aeneasr any updates here? It's been over a month now.

ChristianSch avatar Jun 28 '23 09:06 ChristianSch

Unfortunately our engineers are still busy with other work from customers and we do not have capacity to work on this at the moment.

aeneasr avatar Jul 04 '23 08:07 aeneasr

I just stumbled over this issue and I had a similar problem, however, I don't know if this was supposed to work or the documentation is outdated but if you specify the namespace in the tuples it should work just fine:

[
  {
    "namespace": "Folder",
    "object": "folder_1",
    "relation": "viewers",
    "subject_set": {
      "namespace": "User",
      "object": "user_1"
    }
  },
  {
    "namespace": "Document",
    "object": "document_1",
    "relation": "parents",
    "subject_set": {
      "namespace": "Folder",
      "object": "folder_1"
    }
  }
]

When then calling

keto check 'User:user_1' view Document document_1

instead of

keto check user_1 view Document document_1

it will return Allowed

TimDiekmann avatar Aug 21 '23 15:08 TimDiekmann

Any updates? This seems like core functionality of OPL...

kaiba42 avatar Sep 06 '23 19:09 kaiba42

Unfortunately our engineers are still busy with other work from customers and we do not have capacity to work on this at the moment.

aeneasr avatar Sep 07 '23 05:09 aeneasr

This bug actually makes OPL completely useless. Please prioritize this bug as soon as possible.

nmapx avatar Sep 14 '23 11:09 nmapx

I just stumbled over this issue and I had a similar problem, however, I don't know if this was supposed to work or the documentation is outdated but if you specify the namespace in the tuples it should work just fine:

[
  {
    "namespace": "Folder",
    "object": "folder_1",
    "relation": "viewers",
    "subject_set": {
      "namespace": "User",
      "object": "user_1"
    }
  },
  {
    "namespace": "Document",
    "object": "document_1",
    "relation": "parents",
    "subject_set": {
      "namespace": "Folder",
      "object": "folder_1"
    }
  }
]

When then calling

keto check 'User:user_1' view Document document_1

instead of

keto check user_1 view Document document_1

it will return Allowed

This is the answer ^

Think of it as a sort of strict type. Problem with the approach you have tried is illustrated simply by adding another namespace to the viewers relation:

class User implements Namespace {}

class Guests implements Namespace {}

class Document implements Namespace {
    related: {
        viewers: (User | Guests)[];
        parents: Folder[];
    };
 ...

This behaviour must be a part of the design, if namespace is not defined system should create it in same namespace the object is located at.

76creates avatar Sep 20 '23 21:09 76creates

I faced the same issue and the solution given in the previous comments works. All of it in Ory Network btw, not the open-source Keto.

There is in inconsistency in the API that is not mentioned here, because you cannot DELETE the relation tuple that you defined with the namespace anymore. You can only PATCH them to remove it with the PATCH API endpoint.

The delete endpoint will give you an error that the relation tuple misses a subjectSet.relation property. The patch endpoint will not do that.

We are customers of Ory Network, and I would expect this to be addressed accordingly in the documentation! We found this out just by pure luck and wasted some unnecessary time on it.

janiskemper avatar Feb 27 '25 10:02 janiskemper

Hello, I wanted to give an update that we are planning to do significant amount of work on this project soon as (a) because of problems like this one and (b) an increase in activity from a variety of companies :)

Is this issue blocking for anyone at this moment?

aeneasr avatar Feb 27 '25 14:02 aeneasr

This does not work on self-hosted keto, maybe just an ory-network feature for now

abhayverma47 avatar May 22 '25 13:05 abhayverma47

This worked for me on self-hosted keto: https://github.com/ory/keto/issues/1330#issuecomment-1686584096

The key insight for me was that parent relationships for checks that use traversal require subject_set in the tuples themselves in addition to the traverse definition in the OPL schema.

My keto.yml for my docker compose development environment, mounting my permissions.ts using a volume:

serve:
  read:
    host: 0.0.0.0
    port: 4466
  write:
    host: 0.0.0.0
    port: 4467
  metrics:
    host: 0.0.0.0
    port: 4468

# Global OPL configuration - classes in permissions.ts become namespaces
namespaces:
  location: file:///etc/config/keto/permissions.ts

dsn: postgres://keto:keto_secret@keto-db:5432/keto

log:
  level: trace
  format: json

My permissions.ts OPL schema:

import { Namespace, Context } from "@ory/permission-namespace-types"

class User implements Namespace {}

// FollowUpBoss CRM entities
class FollowUpBossPerson implements Namespace {
  related: {
    owners: User[]
    editors: User[]
    viewers: User[]
  }

  permits = {
    view: (ctx: Context): boolean =>
      this.related.viewers.includes(ctx.subject) ||
      this.related.editors.includes(ctx.subject) ||
      this.related.owners.includes(ctx.subject),
    edit: (ctx: Context): boolean =>
      this.related.editors.includes(ctx.subject) ||
      this.related.owners.includes(ctx.subject)
  }
}

class FollowUpBossDeal implements Namespace {
  related: {
    owners: User[]
    editors: User[]
    viewers: User[]
    parents: FollowUpBossPerson[]
  }

  permits = {
    view: (ctx: Context): boolean =>
      this.related.viewers.includes(ctx.subject) ||
      this.related.editors.includes(ctx.subject) ||
      this.related.owners.includes(ctx.subject) ||
      this.related.parents.traverse((parent) => parent.permits.view(ctx)),
    edit: (ctx: Context): boolean =>
      this.related.editors.includes(ctx.subject) ||
      this.related.owners.includes(ctx.subject) ||
      this.related.parents.traverse((parent) => parent.permits.edit(ctx))
  }
}

An example tuple set testing traverse functionality:

{
  "relation_tuples": [
    {
      "namespace": "FollowUpBossPerson",
      "object": "FollowUpBossPerson:636",
      "relation": "editors",
      "subject_id": "eefa251f-cff2-468f-9ffb-15ff9dc32b46"
    },
    {
      "namespace": "FollowUpBossDeal",
      "object": "FollowUpBossDeal:1793",
      "relation": "editors",
      "subject_id": "FollowUpBossPerson:636"
    },
    {
      "namespace": "FollowUpBossDeal",
      "object": "FollowUpBossDeal:1793",
      "relation": "parents",
      "subject_set": {
        "namespace": "FollowUpBossPerson",
        "object": "FollowUpBossPerson:636",
        "relation": ""
      }
    },
    ...
  ]
}

Successful checks:

❯ curl -X POST "http://localhost:4466/relation-tuples/check" \
                                                        -H "Content-Type: application/json" \
                                                        -d '{
                                                      "namespace": "FollowUpBossDeal",
                                                      "object": "FollowUpBossDeal:1793",
                                                      "relation": "view",
                                                      "subject_id": "FollowUpBossPerson:636"
                                                    }'
{"allowed":true}

❯ curl -X POST "http://localhost:4466/relation-tuples/check" \
                                                        -H "Content-Type: application/json" \
                                                        -d '{
                                                      "namespace": "FollowUpBossDeal",
                                                      "object": "FollowUpBossDeal:1793",
                                                      "relation": "view",
                                                      "subject_id": "eefa251f-cff2-468f-9ffb-15ff9dc32b46"
                                                    }'
{"allowed":true}

Note that I'm prefixing the namespace name in the ID's for the objects because the ID's originate from a 3rd party system I don't control and use incrementing integers as ID's and objects must be unique. There's a possibility if I extend the system and use union types for relations that collisions could occur.

robert-baldwin avatar Aug 01 '25 19:08 robert-baldwin

To my understanding, if you need traversal, the relationship subject must be a subject set. Otherwise, it doesn't work.

Tbh, this does get confusing sometimes. Because the use of subject_set and subject_id overlaps. Imo, it's easier to define all relationships using subject_set(s) giving an empty relation as mentioned in https://github.com/ory/keto/issues/1330#issuecomment-1728463163. However, I do not know if this will affect performance or not but it seems to be a unified way to handle things right now.

siraphobk avatar Oct 24 '25 08:10 siraphobk