<@U04UQ68083H> I'm having an issue where the permi...
# ory-copilot
h
@chilly-king-10285 I'm having an issue where the permission check is incorrect when I'm transitive checking via traverse:
Copy code
import { Namespace, Context, SubjectSet } from "@ory/permission-namespace-types"

class User implements Namespace { }

class Tenant implements Namespace {
  related: {
    admins: User[]
    members: User[]
  }

  permits = {
    isAdmin: (ctx: Context): boolean =>
      this.related.admins.includes(ctx.subject),

    isMember: (ctx: Context): boolean =>
      this.related.members.includes(ctx.subject),
  }
}

class Document implements Namespace {
  related: {
    owners: User[]
    tenants: Tenant[]
  }

  permits = {
    read: (ctx: Context): boolean =>
      this.related.tenants.traverse((tenant) => tenant.permits.isMember(ctx)) ||
      this.permits.write(ctx),

    write: (ctx: Context): boolean =>
      this.related.tenants.traverse((tenant) => tenant.permits.isAdmin(ctx))
  }
}
created relationships:
b
A message from kapa.ai
Based on your code and the relationships you described, your OPL model is using the
.traverse
method to check permissions transitively through related tenants. This is a common and supported pattern in Ory Keto/Permissions, as shown in the official documentation and examples, where permissions are checked through parent relationships or group memberships using
.traverse
or
.transitive
methods[Define permissions in the OPL](https://www.ory.sh/docs/keto/modeling/create-permission-model#define-permissions-in-the-opl). However, there are a few important considerations and known issues: 1. Correctness of `.traverse`/`.transitive`: Your usage of
.traverse((tenant) => tenant.permits.isMember(ctx))
and similar patterns is correct according to the OPL specification. This is how you would check if the subject is a member or admin of any related tenant, and thus should have
read
or
write
access to the document. 2. Known Issues with Transitive Checks: There are open issues in the Ory Keto project where
.traverse
or
.transitive
checks do not always work as expected, especially in certain versions or configurations. For example, [this issue](https://github.com/ory/keto/issues/1330) describes a scenario where a user is a viewer of a folder, and a document is a child of that folder, but the transitive permission check returns
false
when it should return
true
. This suggests that there may be bugs or limitations in the current implementation of transitive permission checks. 3. Version and Configuration: Some of these issues are version-dependent. If you are running into unexpected results, check your Ory Keto version and consider testing with the latest release, as fixes and improvements are ongoing. 4. Workarounds: In some cases, users have found that explicitly repeating the transitive logic in each permission (rather than delegating via
this.permits
) can work around the issue. For example, instead of calling
this.permits.write(ctx)
inside
read
, you might inline the logic again, as described in [this issue](https://github.com/ory/keto/issues/1626). Summary: Your OPL model is correct in its use of
.traverse
for transitive permission checks. If you are experiencing incorrect permission results, it is likely due to a known issue in the Ory Keto implementation of transitive checks. You may want to try inlining the logic or upgrading to the latest version. If the problem persists, consider following or contributing to the relevant GitHub issues for updates and workarounds. If you provide the specific relationships you created and the permission check you are running (with expected vs. actual results), I can help analyze further based on the documented behavior and known issues. References: - [OPL traverse not working issue](https://github.com/ory/keto/issues/1330) - [OPL permissions inconsistencies issue](https://github.com/ory/keto/issues/1626) - [Define permissions in the OPL](https://www.ory.sh/docs/keto/modeling/create-permission-model#define-permissions-in-the-opl)
h
I'm using Ory network. If the problem is there, how should I change my model
@chilly-king-10285 I'm using Ory network. If the problem is there, how should I change my model
b
A message from kapa.ai
If you are using Ory Network and encounter issues with transitive permission checks (such as with
.traverse
or calling
this.permits.*
inside another permission), this is a known area where inconsistencies have been reported. Specifically, users have found that chaining permissions (e.g.,
this.permits.write(ctx)
inside another permit) can sometimes yield incorrect results, while inlining the transitive logic directly works as expected. Recommended Change: Instead of delegating to another permit (e.g.,
this.permits.write(ctx)
), directly inline the logic for the transitive check in each permission. For your example, rewrite the
read
and
write
permissions like this:
Copy code
typescript class Document implements Namespace {   related: {     owners: User[]     tenants: Tenant[]   }    permits = {     read: (ctx: Context): boolean =>       this.related.tenants.traverse((tenant) => tenant.permits.isMember(ctx)) ||       this.related.tenants.traverse((tenant) => tenant.permits.isAdmin(ctx)),      write: (ctx: Context): boolean =>       this.related.tenants.traverse((tenant) => tenant.permits.isAdmin(ctx))   } }
This approach avoids chaining through
this.permits.write(ctx)
and instead repeats the logic, which has been shown to work around the current limitations in Ory Network's permission engine[OPL permissions have inconsistencies](https://github.com/ory/keto/issues/1626). Summary: - Inline the transitive logic in each permission instead of calling another permit. - This workaround is necessary due to current inconsistencies in Ory Network's handling of chained permission checks. If you continue to experience issues, monitor the referenced GitHub issue for updates, as improvements are planned for this area.
h
@chilly-king-10285 I encountered the same problem with this model traversal: import { Namespace, Context, SubjectSet } from "@ory/permission-namespace-types" class User implements Namespace { } class Group implements Namespace { related: { members: User[] groups: Group[] } permits = { has: (ctx: Context): boolean => this.related.members.includes(ctx.subject) || this.related.groups.traverse((group) => group.permits.has(ctx)) }; } class Role implements Namespace { related: { members: User[] groups: Group[] } permits = { has: (ctx: Context): boolean => this.related.members.includes(ctx.subject) || this.related.groups.traverse((group) => group.permits.has(ctx)), } } class Feature implements Namespace { related: { owners: User[] readers: Role[] writers: Role[] } permits = { read: (ctx: Context): boolean => this.related.owners.includes(ctx.subject) || this.related.readers.traverse((role) => role.permits.has(ctx)), write: (ctx: Context): boolean => this.related.owners.includes(ctx.subject) || this.related.writers.traverse((role) => role.permits.has(ctx)) } }
b
A message from kapa.ai
Your model uses recursive traversal for group and role membership, and then uses those roles for feature access. This is a supported and documented pattern in Ory Permissions and OPL, where
.traverse
is used to check transitive relationships, such as nested group membership or role assignment. The OPL specification and examples show similar usage for hierarchical and recursive permission checks[Define permissions in the OPL](https://www.ory.sh/docs/keto/modeling/create-permission-model#define-permissions-in-the-opl). However, there are known issues with transitive checks and recursive traversal in Ory Keto and Ory Network, especially when using
.traverse
or chaining permission checks. Users have reported that, in some cases, the expected transitive permission is not granted even though the relationships are set up correctly. This is particularly true for recursive group membership and when chaining permissions (e.g., calling
this.permits.has(ctx)
inside another permission)[OPL traverse not working issue](https://github.com/ory/keto/issues/1330), [OPL permissions have inconsistencies](https://github.com/ory/keto/issues/1626). What you can do: - Inline the logic: If you experience issues, try to avoid chaining permissions (e.g., avoid calling
this.permits.has(ctx)
inside another permission). Instead, inline the logic directly in each permission, as this has been shown to work around some of the current limitations in Ory Network. - Limit recursion depth: Deep or complex recursive traversals may hit performance or correctness issues. If possible, limit the depth or complexity of your group/role hierarchies. - Monitor for updates: These issues are known and being tracked by the Ory team. If you are affected, keep an eye on the referenced GitHub issues for updates and fixes. Summary: Your model is correct according to the OPL specification, but if you encounter incorrect permission results, it is likely due to current limitations in Ory Network's handling of recursive and transitive permission checks. Inlining logic and simplifying relationships can help as a workaround until these issues are resolved in future releases[OPL traverse not working issue](https://github.com/ory/keto/issues/1330), [OPL permissions have inconsistencies](https://github.com/ory/keto/issues/1626).
h
@chilly-king-10285 how to mitigate this problem
b
A message from kapa.ai
To mitigate issues with transitive permission checks and recursive traversal in Ory Network (such as with
.traverse
or chaining permission checks), the best-documented workaround is to inline the transitive logic directly in each permission rather than delegating to another permit (e.g., avoid calling
this.permits.has(ctx)
inside another permit). This approach is recommended because there are known inconsistencies when chaining permissions, as reported by users and acknowledged in Ory's GitHub issues. For example, instead of:
Copy code
typescript read: (ctx: Context): boolean =>   this.related.owners.includes(ctx.subject) ||   this.related.readers.traverse((role) => role.permits.has(ctx)),
You would inline the logic from the
has
permit directly:
Copy code
typescript read: (ctx: Context): boolean =>   this.related.owners.includes(ctx.subject) ||   this.related.readers.traverse(     (role) =>       role.related.members.includes(ctx.subject) ||       role.related.groups.traverse((group) =>         group.related.members.includes(ctx.subject) ||         group.related.groups.traverse(/* ...and so on, as needed... */)       )   ),
This workaround is based on user reports and Ory's own guidance in response to similar issues, where inlining the logic avoids the current limitations of the permission engine in Ory Network[OPL permissions have inconsistencies](https://github.com/ory/keto/issues/1626). Summary of mitigation steps: - Inline transitive logic in each permission instead of chaining permits. - Simplify relationship hierarchies where possible to reduce recursion depth. - Monitor Ory's GitHub issues for updates and fixes to this behavior. If you continue to experience issues, you may want to follow or contribute to the relevant GitHub issues for further updates and guidance from the Ory team.
h
Copy code
permits = {
    read: (ctx: Context): boolean =>
      this.related.owners.includes(ctx.subject) ||
      // this.related.readers.traverse((role) => role.permits.has(ctx)),
      this.related.readers.traverse(
        (role) =>
          role.related.members.includes(ctx.subject) ||
          role.related.groups.traverse((group) =>
            group.related.members.includes(ctx.subject) ||
            group.related.groups.traverse((group) => group.permits.has(ctx))
          )
      ),
this has syntax error @chilly-king-10285
b
A message from kapa.ai
You're correct—your code snippet has a syntax error due to the recursive call to
group.permits.has(ctx)
inside the innermost
.traverse
. In Ory Permission Language (OPL), recursive permission calls like this can lead to infinite recursion or are not supported as written. The OPL specification allows for transitive checks, but recursive expansion must be handled carefully and is subject to the engine's limitations[OPL traverse not working issue](https://github.com/ory/keto/issues/1330). What's wrong: - The innermost
.traverse((group) => group.permits.has(ctx))
is a recursive call to the same permission, which can cause issues both in OPL parsing and in runtime evaluation. - OPL expects the logic to be expressed in a way that avoids unbounded recursion. How to fix: - Limit the recursion depth explicitly if possible. - If you need to support deeply nested groups, consider flattening your group relationships outside of OPL, or limit the nesting in your model. - In OPL, you can only safely express a fixed depth of traversal by unrolling the logic, for example:
Copy code
typescript permits = {   read: (ctx: Context): boolean =>     this.related.owners.includes(ctx.subject) ||     this.related.readers.traverse(       (role) =>         role.related.members.includes(ctx.subject) ||         role.related.groups.traverse(           (group) =>             group.related.members.includes(ctx.subject)             // Do not recurse further, or manually unroll another level if needed         )     ), }
This approach is a workaround for the current limitations in Ory Network and OPL, as recursive permission checks are not fully supported and can lead to errors or unexpected results[OPL permissions have inconsistencies](https://github.com/ory/keto/issues/1626). Summary: - The syntax error is due to unbounded recursion in the permission definition. - Limit recursion depth by unrolling the logic or flattening relationships. - Recursive permission checks are a known limitation in Ory Network and OPL. If you need true recursive group membership, you may need to wait for future improvements in Ory Keto/Network, as the current version does not fully support this pattern.
h
@chilly-king-10285 I use SubjectSet instead of traversal:
Copy code
import { Namespace, Context, SubjectSet } from "@ory/permission-namespace-types"

class User implements Namespace { }

class Group implements Namespace {
  related: {
    members: (User | SubjectSet<Group, "members">)[]
  }
}

class Role implements Namespace {
  related: {
    members: (User | SubjectSet<Group, "members">)[]
  }
}

class Feature implements Namespace {
  related: {
    owners: User[]
    readers: (User | SubjectSet<Role, "members">)[]
    writers: (User | SubjectSet<Role, "members">)[]
  }

  permits = {
    read: (ctx: Context): boolean =>
      this.related.owners.includes(ctx.subject) ||
      this.related.readers.includes(ctx.subject),

    write: (ctx: Context): boolean =>
      this.related.owners.includes(ctx.subject) ||
      this.related.writers.includes(ctx.subject)
  }
}
But the group cannot reference itself. How can I overcome this
b
A message from kapa.ai
You are correct that in Ory Permission Language (OPL), you can use
SubjectSet
to model indirect or nested group membership, such as:
Copy code
typescript members: (User | SubjectSet<Group, "members">)[]
This allows a group to include users directly or to include all members of another group. However, self-referencing (a group referencing itself as a member) is not supported and is explicitly discouraged in the Ory documentation. This is to prevent infinite recursion and cycles in the relationship graph, which Ory Keto does not support or handle gracefully[Subjects: Subject sets](https://www.ory.sh/docs/keto/concepts/subjects). How to overcome this limitation: - Avoid self-referencing groups: You should not create a relationship where a group is a member of itself, either directly or indirectly. This is a fundamental limitation to prevent cycles in the permission graph. - Model group hierarchies without cycles: You can nest groups (A includes B, B includes C), but you must ensure there are no cycles (e.g., C does not include A, and so on). - Flatten group memberships if needed: If you need to ensure a user has all the permissions of nested groups, you may need to flatten the group memberships in your application logic before writing them to Ory Keto, or ensure your group structure is acyclic. Summary: Ory Keto and OPL do not support groups referencing themselves, either directly or through nested
SubjectSet
relationships, to avoid cycles and infinite recursion. You must design your group hierarchy to be acyclic[Subjects: Subject sets](https://www.ory.sh/docs/keto/concepts/subjects). If you need more advanced group logic, you may need to handle cycle detection and flattening outside of Ory Keto before creating the relationships.