billions-leather-15882
07/30/2025, 10:51 PM-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)
brief-egg-61061
07/30/2025, 10:51 PMbrief-egg-61061
07/30/2025, 10:51 PM{"allowed": false}
when you expect true
. Let's analyze the model and the data: ### OPL Model Review - Plan.includes_feature: 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: 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: 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: 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: {
"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: 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: 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.billions-leather-15882
07/30/2025, 10:56 PMbrief-egg-61061
07/30/2025, 10:57 PMbrief-egg-61061
07/30/2025, 10:57 PM{"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: json {"namespace":"Plan","object":"planId","relation":"features","subject_id":"Feature:featId"}
And your check uses: --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)billions-leather-15882
07/30/2025, 11:00 PMbrief-egg-61061
07/30/2025, 11:00 PMbrief-egg-61061
07/30/2025, 11:00 PMbeebb63c5cd4a58b218a792027b34d231735dc05
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).billions-leather-15882
07/30/2025, 11:01 PMbrief-egg-61061
07/30/2025, 11:01 PMbrief-egg-61061
07/30/2025, 11:01 PMtraverse
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.billions-leather-15882
07/30/2025, 11:01 PMbrief-egg-61061
07/30/2025, 11:01 PMbrief-egg-61061
07/30/2025, 11:02 PMtraverse
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: 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: 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.billions-leather-15882
07/30/2025, 11:32 PMbrief-egg-61061
07/30/2025, 11:32 PMbrief-egg-61061
07/30/2025, 11:32 PMtypescript 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: 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: 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).billions-leather-15882
07/30/2025, 11:38 PMGET <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...)brief-egg-61061
07/30/2025, 11:38 PMbrief-egg-61061
07/30/2025, 11:39 PMrelation
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).