<@U04UQ68083H>My keto permissions is not working c...
# ory-copilot
b
@chilly-king-10285My keto permissions is not working correctly or my check is not working correctly. Where is the problem? import { Namespace, SubjectSet, Context } from "@ory/keto-namespace-types" // Represents a feature in the system (e.g., invoicing.create_invoice) class Feature implements Namespace {} // Represents a plan grouping features class Plan implements Namespace { related: { features: Feature[] } permits = { includes_feature: (ctx: Context): boolean => this.related.features.includes(ctx.subject) } } // Represents a subscription for a tenant to a plan and/or features class Subscription implements Namespace { related: { tenant: Tenant[], plan: Plan[], features: Feature[] } permits = { has_feature: (ctx: Context): boolean => // this.related.features.includes(ctx.subject) || this.related.plan.traverse((plan) => plan.permits.includes_feature(ctx)) } } class User implements Namespace {} class Group implements Namespace { related: { members: (User | Group)[] } } class Tenant implements Namespace { related: { owners: User[], subscriptions: Subscription[] } permits = { allows_feature_use: (ctx: Context): boolean => this.related.subscriptions.traverse(sub => sub.permits.has_feature(ctx)) } } / # curl -s "http://localhost:4466/relation-tuples" {"relation_tuples":[{"namespace":"Feature","object":"featId","relation":"self","subject_id":"feature:featId"},{"namespace":"Subscription","object":"subId","relation":"plan","subject_id":"planId"},{"namespace":"Tenant","object":"tenantId","relation":"subscriptions","subject_id":"subId"},{"namespace":"Plan","object":"planId","relation":"features","subject_id":"featId"},{"namespace":"Subscription","object":"subId","relation":"tenant","subject_id":"tenantId"},{"namespace":"Feature","object":"featDirectId","relation":"self","subject_id":"feature:featDirectId"}],"next_page_token":""} / # curl -s "http://localhost:4466/relation-tuples/check/openapi" \
-G \
--data-urlencode "namespace=Tenant" \
--data-urlencode "object=tenantId" \
--data-urlencode "relation=allows_feature_use" \
--data-urlencode "subject_id=featId"
{"allowed":false} It should return allowed:true I want to see if tenant has a feature (in tenant -> subscription -> plan -> feature)
b
A message from kapa.ai
Based on your OPL model and the relation tuples provided, your intention is to check if a tenant has access to a feature via the chain: Tenant → Subscription → Plan → Feature. However, your permission check is returning
{"allowed": false}
when you expect
true
. Let's analyze the model and the data: ### OPL Model Review - Plan.includes_feature:
Copy code
typescript   includes_feature: (ctx: Context): boolean =>     this.related.features.includes(ctx.subject)
This checks if the subject (feature) is directly in the plan's features. - Subscription.has_feature:
Copy code
typescript   has_feature: (ctx: Context): boolean =>     this.related.plan.traverse((plan) => plan.permits.includes_feature(ctx))
This traverses to the plan(s) and checks if the plan includes the feature. - Tenant.allows_feature_use:
Copy code
typescript   allows_feature_use: (ctx: Context): boolean =>     this.related.subscriptions.traverse(sub => sub.permits.has_feature(ctx))
This traverses to the subscriptions and checks if the subscription has the feature. ### Relation Tuples You have the following relevant tuples: -
Tenant:tenantId#subscriptions@subId
-
Subscription:subId#plan@planId
-
Plan:planId#features@featId
### The Problem Your check:
Copy code
GET /relation-tuples/check/openapi?namespace=Tenant&object=tenantId&relation=allows_feature_use&subject_id=featId
returns
{"allowed": false}
. #### Potential Issues 1. Subject Type Mismatch: The
includes_feature
permission expects
ctx.subject
to match the type in
Plan.related.features
, which is
Feature[]
. In your tuples, you have:
Copy code
{
  "namespace": "Plan",
  "object": "planId",
  "relation": "features",
  "subject_id": "featId"
}
This means the subject is just the string
"featId"
. If your OPL or Keto expects a fully qualified subject (like
Feature:featId
), this could cause a mismatch. 2. Relation Name: The relation in the tuple is
"features"
, which matches the OPL. 3. No Direct Feature on Subscription: You commented out the direct feature check in
Subscription.has_feature
. If you want to support both direct and plan-based features, you should include both:
Copy code
typescript    has_feature: (ctx: Context): boolean =>      this.related.features.includes(ctx.subject) ||      this.related.plan.traverse((plan) => plan.permits.includes_feature(ctx))
But for your current test, the plan-based path should be enough. 4. Use of
traverse
: There is a known issue with
traverse
in OPL not always working as expected, especially in older versions of Keto ([see this issue](https://github.com/ory/keto/issues/1330)). If you are using a version where
traverse
is not functioning, this could explain the failure. ### Recommendations - Check Subject Format: Ensure that the subject in your check (
subject_id=featId
) matches what is stored in the tuples and what OPL expects. If your tuples use
"featId"
, your check should use the same. - Keto Version: If you are using Keto v0.11.1-alpha.0 or similar, there are known issues with
traverse
not working as expected. Consider upgrading Keto or, as a workaround, try using
transitive
instead of
traverse
if your use case allows it ([see the issue discussion](https://github.com/ory/keto/issues/1330)). - Debugging: Try a direct check at the Plan level to see if the problem is in the traversal:
Copy code
GET /relation-tuples/check/openapi?namespace=Plan&object=planId&relation=includes_feature&subject_id=featId
If this returns
true
, the problem is with the traversal in Subscription or Tenant. ### Conclusion Given your model and tuples, the most likely cause is a problem with the
traverse
operator in your Keto version, as documented in [this issue](https://github.com/ory/keto/issues/1330). Try upgrading Keto or switching to
transitive
if possible. Also, double-check that your subject IDs are consistent throughout your tuples and checks. If after these steps it still does not work, please provide your Keto version and try the direct Plan-level check to further isolate the issue.
b
@chilly-king-10285 Keto version 0.14.0 And after adding the Feature:featId to the plan, it is still returning allowed false: {"relation_tuples":[{"namespace":"Subscription","object":"subId","relation":"plan","subject_id":"planId"},{"namespace":"Feature","object":"featId","relation":"self","subject_id":"Feature:featId"},{"namespace":"Subscription","object":"subId","relation":"tenant","subject_id":"tenantId"},{"namespace":"Plan","object":"planId","relation":"features","subject_id":"Feature:featId"},{"namespace":"Tenant","object":"tenantId","relation":"subscriptions","subject_id":"subId"},{"namespace":"Feature","object":"featDirectId","relation":"self","subject_id":"Feature:featDirectId"}],"next_page_token":""} / # curl -s "http://localhost:4466/relation-tuples/check/openapi" \ > -G \ > --data-urlencode "namespace=Tenant" \ > --data-urlencode "object=tenantId" \ > --data-urlencode "relation=allows_feature_use" \ > --data-urlencode "subject_id=Feature:featId" {"allowed":false}
b
A message from kapa.ai
Given your OPL model, relation tuples, and the use of Keto v0.14.0, your expectation is correct: the check should return
{"allowed": true}
if the feature is connected to the tenant through the subscription and plan. However, it is returning
{"allowed": false}
. ### Key Observations 1. Subject Format Consistency Your relation tuple for the plan uses:
Copy code
json    {"namespace":"Plan","object":"planId","relation":"features","subject_id":"Feature:featId"}
And your check uses:
Copy code
--data-urlencode "subject_id=Feature:featId"
This is consistent. 2. Model Traversal Your OPL model uses the
traverse
operator to walk from Tenant → Subscription → Plan → Feature. According to the [Keto API and OPL documentation](https://www.ory.sh/docs/keto/concepts/api-overview), this is a supported pattern. 3. Known Issues with Traversal There is a documented issue with the
traverse
operator in OPL, where it does not always work as expected, even in relatively recent versions of Keto. Users have reported that traversals like yours return
allowed: false
even when the data model is correct. This is discussed in [this GitHub issue](https://github.com/ory/keto/issues/1330). ### What the Knowledge Sources Say - The [Keto API overview](https://www.ory.sh/docs/keto/concepts/api-overview) confirms that the check API is the correct way to verify permissions. - The [GitHub issue #1330](https://github.com/ory/keto/issues/1330) specifically describes a similar problem: traversals in OPL models returning
false
when they should return
true
, even with correct relation tuples and subject formats. ### Conclusion Based on the knowledge sources, your OPL model and relation tuples are correct, but there is a known issue with the
traverse
operator in OPL that can cause permission checks to fail even when the data is correct. This issue is present in versions up to and including v0.14.0.
Next Steps: - Monitor the [open issue on GitHub](https://github.com/ory/keto/issues/1330) for updates or workarounds. - If possible, try using the
transitive
operator instead of
traverse
, as suggested in the issue, or simplify your model to avoid deep traversals until the bug is resolved. If you need to support this pattern immediately, you may need to adjust your model or wait for a fix in a future Keto release. [OPL traverse not working issue](https://github.com/ory/keto/issues/1330)
b
@chilly-king-10285 what is the latest keto version?
b
A message from kapa.ai
Based on the available knowledge sources, the latest Ory Keto image tags listed are from July 25, 2025. However, the specific version numbers are not provided in the knowledge sources—only image hashes are shown, such as
beebb63c5cd4a58b218a792027b34d231735dc05
and
097934fff2bda05c808d962a92f52140f80dff83
 [OEL Keto image tags](https://www.ory.sh/docs/self-hosted/oel/oel-keto-image-tags). Additionally, a maintainer comment in a GitHub discussion from June 2025 references v0.14.0 as the latest released version at that time [GitHub discussion: Docker image version](https://github.com/ory/keto/discussions/1744). I'm sorry, but the knowledge sources do not contain explicit information about a newer version beyond v0.14.0 or the exact version numbers for the July 2025 images. For the most up-to-date version, please check the [Ory Keto releases page](https://github.com/ory/keto/releases).
b
@chilly-king-10285 should I create a bug report for that?
b
A message from kapa.ai
Based on the knowledge sources, there is already an open issue regarding problems with the
traverse
operator in OPL not working as expected in Ory Keto, including in version 0.14.0. This issue is documented and being tracked by the Ory team, and other users have reported similar problems [OPL traverse not working](https://github.com/ory/keto/issues/1330). Given this, you do not need to create a new bug report for this specific problem, as it is already known and under discussion. If you have additional details or a reproducible example that is not already covered in the existing issue, you can add a comment to the open issue to help the maintainers. If your case is significantly different or you have found a new bug unrelated to the
traverse
operator, then creating a new bug report would be appropriate. Otherwise, contributing to the existing discussion is the recommended approach.
b
@chilly-king-10285 explain traverse operator and how should I use it?
b
A message from kapa.ai
The
traverse
operator in the Ory Permission Language (OPL) is used to evaluate permissions or relationships across related objects in your permission model. It allows you to "walk" through a relation (which is always an array) and check if any related object grants the desired permission or contains the subject. ### How
traverse
Works - Purpose:
traverse
iterates over all related objects in a relation and applies a function (usually a permission check) to each. If any invocation returns
true
, the overall result is
true
. - Syntax Example:
Copy code
typescript   this.related.parents.traverse((parent) => parent.permits.view(ctx))
This checks if any parent object grants the
view
permission to the subject in the context. - Typical Use Case: You use
traverse
when permissions can be inherited or delegated through relationships, such as folder hierarchies or group memberships. ### Example from the Knowledge Sources In a document store model, you might have:
Copy code
typescript class Document implements Namespace {   related: {     viewers: User[]     parents: Folder[]   }    permits = {     view: (ctx: Context): boolean =>       this.related.viewers.includes(ctx.subject) ||       this.related.parents.traverse((parent) => parent.permits.view(ctx)),   } }
Here, a user can view a document if they are a direct viewer or if any parent folder grants them the view permission. The
traverse
operator is what enables this recursive, inherited permission check [Define permissions in the OPL](https://www.ory.sh/docs/keto/modeling/create-permission-model#define-permissions-in-the-opl). ### How to Use - Use
traverse
on array relations (e.g.,
parents
,
members
, etc.). - Pass a function that checks a permission or relation on each related object. - The function should return a boolean. ### Important Notes - The
traverse
operator is only meaningful on array relations. - There are known issues with the
traverse
operator in some Keto versions, where it may not work as expected in all scenarios [OPL traverse not working issue](https://github.com/ory/keto/issues/1330). If you need to model inherited or delegated permissions,
traverse
is the recommended approach in OPL, but be aware of the current implementation limitations in your Keto version.
b
@chilly-king-10285 I simplified the test: Now I have: class Feature implements Namespace {} // Represents a plan grouping features class Plan implements Namespace { related: { features: Feature[] } permits = { includes_feature: (ctx: Context): boolean => this.related.features.includes(ctx.subject) } } But still: / # curl -s "http://localhost:4466/relation-tuples?namespace=Plan&amp;object=planId&amp;relation=features" {"relation_tuples":[{"namespace":"Plan","object":"planId","relation":"features","subject_set":{"namespace":"Feature","object":"featId","relation":""}}],"next_page_token":""} / # curl -s "http://localhost:4466/relation-tuples?subject_id=featId" {"relation_tuples":[{"namespace":"Feature","object":"featId","relation":"self","subject_id":"featId"}],"next_page_token":""} / # curl -s "http://localhost:4466/relation-tuples/check/openapi" -G --data-urlencode "namespace=Plan" --data-urlencode "object=planId" --data-urlencode "relation=includes_feature" --data-urlencode "su bject_id=feature:featId" {"allowed":false} / # curl -s "http://localhost:4466/relation-tuples/check/openapi" -G --data-urlencode "namespace=Plan" --data-urlencode "object=planId" --data-urlencode "relation=includes_feature" --data-urlencode "su bject_id=featId" {"allowed":false}
b
A message from kapa.ai
Based on your simplified test and the knowledge sources, here’s what’s happening: ### Your Model
Copy code
typescript class Feature implements Namespace {}  class Plan implements Namespace {   related: {     features: Feature[]   }    permits = {     includes_feature: (ctx: Context): boolean =>       this.related.features.includes(ctx.subject)   } }
### Your Data - The
Plan
has a
features
relation to a
Feature
object with
object: featId
. - You are checking if
Plan:planId
includes the feature for subject
featId
or
feature:featId
. ### What the Knowledge Sources Say - The
includes
check in OPL (
this.related.features.includes(ctx.subject)
) expects the subject in the check to match the subject in the relation tuple for
features
([OPL statements reference](https://www.ory.sh/docs/keto/reference/ory-permission-language#statements)). - In your relation tuple, the
features
relation is set as a subject set:
Copy code
json   {"namespace":"Plan","object":"planId","relation":"features","subject_set":{"namespace":"Feature","object":"featId","relation":""}}
This means the subject is not a simple string like
"featId"
or
"feature:featId"
, but a subject set referencing the
Feature
namespace and object. - The check API expects the
subject_id
or
subject_set
to match what is in the relation tuple ([API reference](https://raw.githubusercontent.com/ory/docs/master/docs/reference/api.json)). If you use
subject_id=featId
or
subject_id=feature:featId
, it will not match a subject set. ### Why Your Check Fails - You are passing
subject_id=featId
or
subject_id=feature:featId
, but your relation tuple for
features
is a subject set (
subject_set: {namespace: "Feature", object: "featId", relation: ""}
). - The check will only return
allowed: true
if the subject in the check matches the subject in the relation tuple, including type (subject set vs. subject id). ### How to Fix You need to use the
subject_set
parameter in your check, not
subject_id
. For the
/relation-tuples/check/openapi
endpoint, the request should look like:
Copy code
bash curl -s "<http://localhost:4466/relation-tuples/check/openapi>" \   -G \   --data-urlencode "namespace=Plan" \   --data-urlencode "object=planId" \   --data-urlencode "relation=includes_feature" \   --data-urlencode "subject_set.namespace=Feature" \   --data-urlencode "subject_set.object=featId" \   --data-urlencode "subject_set.relation="
This matches the subject set in your relation tuple and should return
{"allowed": true}
if everything else is correct. --- Summary: Your check is failing because you are using
subject_id
instead of
subject_set
, but your relation tuple for
features
is a subject set. Use the
subject_set
parameters in your check request to match the tuple structure as described in the [API reference](https://raw.githubusercontent.com/ory/docs/master/docs/reference/api.json).
b
@chilly-king-10285 Ok now it works, but it seems that in the PHP client library, there is a bug. $permResult = Ory::permission()->checkPermission( 'Plan', $planId, 'includes_feature', null, 'Feature', $featureCheckId, '', ); Should create that same request as curl, but because the relation is empty string, the PHP library will not put that to the request: [400] Client error:
GET <http://keto:4466/relation-tuples/check/openapi?namespace=Plan&object=planId&relation=includes_feature&subject_set.namespace=Feature&subject_set.object=featId>
resulted in a
400 Bad Request
response: {"error":{"code":400,"status":"Bad Request","message":"incomplete subject, provide \"subject_id\" or a complete \"subjec (truncated...)
b
A message from kapa.ai
Based on the knowledge sources, your analysis is correct: when using a subject set in a permission check, the
relation
field must be included—even if it is an empty string. The API expects a complete subject set with
namespace
,
object
, and
relation
fields. If any of these are missing, Keto will return a
400 Bad Request
with a message like: > "incomplete subject, provide \"subject_id\" or a complete \"subject_set\"" This is confirmed in the API documentation, which specifies that a subject set must include all three fields:
namespace
,
object
, and
relation
(even if the relation is empty) [Ory API Reference – checkPermission](https://raw.githubusercontent.com/ory/docs/master/docs/reference/api.json). If the PHP client omits the
relation
field when it is an empty string, the request will be incomplete and rejected by the API, as you observed. Summary: The PHP client should always include the
relation
field in the
subject_set
part of the request, even if it is an empty string, to comply with the API requirements. This is a client library bug if it omits the field, and it should be reported or fixed to ensure compatibility with the Keto API specification [Ory API Reference – checkPermission](https://raw.githubusercontent.com/ory/docs/master/docs/reference/api.json).