docs: rewrite OPL as practical guide#2588
Conversation
There was a problem hiding this comment.
Pull request overview
This PR rewrites the Ory Permission Language (OPL) reference page from a formal specification into a practical how-to guide, introducing core modeling concepts (namespaces, relations, subject-set references, and permits) via short examples.
Changes:
- Replaced the EBNF/spec-style content with a step-by-step, example-driven guide.
- Added focused examples for unions,
SubjectSet<...>references,includes,traverse, boolean operators, and permission composition. - Consolidated into a single “complete example” schema showing direct, group-based, and inherited access patterns.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| restricted: (ctx: Context) => | ||
| this.related.allowlist.includes(ctx.subject) && | ||
| !this.related.blocklist.includes(ctx.subject), | ||
| ``` |
| edit: (ctx: Context) => this.related.owners.includes(ctx.subject), | ||
| admin: (ctx: Context) => this.permits.edit(ctx) && this.related.admins.includes(ctx.subject), |
| ClassDecl = "class" identifier "implements" "Namespace" "{" ClassSpec "}" . | ||
| ClassSpec = [ RelationDecls ] | [ PermissionDefns] . | ||
| ``` | ||
| ### Subject-set references |
There was a problem hiding this comment.
I propose to completely omit this section, as subject sets are not really useful currently. There is no specific reason to use them, right?
There was a problem hiding this comment.
good point. Based on how we implement things, currently subjectsets are faster than traverse calls. In a place that traverse would cause 100 direct check calls, SubjectSet<> would make 1 call and get the answer.
But that's an implementation details, and technically, we can make optimizations to make them equally performant.
I think that's the only difference. Otherwise, Traverse is more future-proof as we can easily update/change the relationships form .members.includes(.) to .admin.includes(.).
There was a problem hiding this comment.
Okay, see this answer: https://github.com/orgs/authzed/discussions/2851#discussioncomment-15608216
This is also one difference, though i don't think anyone would care about it.
There was a problem hiding this comment.
I see, but the use-case is very narrow.
IMO the only relevant use-case is where the relation is dynamic, so the user can choose whether they want to grant permissions to all group members, only group admins, ... That requires the relation to not be in the OPL though, or allow narrowing it down to multiple relations. The use-case is not supported really rn.
There was a problem hiding this comment.
The final example makes use SubjectSets, and while changing it to avoid using SubjectSets, i had difficulty. I think there's still some value in mentioning this. Also, people can be migrating from spicedb or something else to keto, and they have a working schema in their mind that uses SubjectSet. I think it's worth mentioning this.
| ``` | ||
|
|
||
| Note that all relations are defined as array types `T[]` because there are naturally only many-to-many relations in Keto. | ||
| A subject can now be either a `User` directly, or any member of the `Group:engineering`. |
There was a problem hiding this comment.
Not clear to me what this means.
There was a problem hiding this comment.
i improved the wording as:
This means a viewer can be either a `User` directly, or any subject in the `members` relation of a `Group`. You can then write a tuple that grants access to a whole group at once:
| related: { | ||
| members: (User | Group)[] | ||
| members: (User | SubjectSet<Group, "members">)[] | ||
| } |
There was a problem hiding this comment.
While the subject set works, I don't think it is the correct way of doing this. Having the parents explicitly here makes it a lot clearer and allows the relation to be reused in different permissions if one wants to add them (think admins of groups, ...)
| related: { | |
| members: (User | Group)[] | |
| members: (User | SubjectSet<Group, "members">)[] | |
| } | |
| related: { | |
| members: User[] | |
| parents: Group[] | |
| } | |
| permits = { | |
| isMember: (ctx: Context) => this.related.members.includes(ctx.subject) || this.related.parents.traverse((parent) => parent.permits.isMember(ctx)) |
There was a problem hiding this comment.
I admit that it is not so nice in the other namespaces without the planned type assertions, but I think it is the better way of writing permissions.
There was a problem hiding this comment.
moving to separate User and Group relations, this cascades to other namespaces as well; now we can't do:
class Folder implements Namespace {
related: {
viewers: (User | SubjectSet<Group, "members">)[]
}
instead we have to do:
userViewers: User[]
groupViewers: Group[]
and have a permit that does the OR logic. same goes for File, but more complex to read, bc it has viewers and owners relations, so it will become viewerUsers, viewerGroups, ownerUsers, ownerGroups.
We can remove the inheritance logic, and have simpler example to avoid this.
I guess this is "simplicity" that SubjectSets brings.
| This schema models: | ||
|
|
||
| - Direct access via `viewers` and `owners` | ||
| - Group-based access via `SubjectSet<Group, "members">` | ||
| - Inherited access from parent folders via `traverse` |
There was a problem hiding this comment.
Consider adding some of these details as comments, so it is more clear what you refer to. Consider adding more comments to the full example in general.
|
@zepatrik i replied to your comments. There are few things to resolve about subjectsets and final example. It's worth noting that reader ideally should read the other pages such as Namespaces/Objects/Subjects before jumping into OPL page. So, some understanding of the Namespace/Object/Subject should exist while reading this. This page is only for OPL reference. Not 1 page Keto guide. Though, the other pages also need improvement, and overall sidebar needs improvements. As a reader, i alway preferred 1 long page docs compared to small separate pages. |
This PR rewrites OPL into a practical how-to guide covering namespaces, relations, subject-set references, and permits, each section with examples