Skip to content

docs: rewrite OPL as practical guide#2588

Open
DavudSafarli wants to merge 2 commits into
masterfrom
opl-update
Open

docs: rewrite OPL as practical guide#2588
DavudSafarli wants to merge 2 commits into
masterfrom
opl-update

Conversation

@DavudSafarli
Copy link
Copy Markdown
Contributor

This PR rewrites OPL into a practical how-to guide covering namespaces, relations, subject-set references, and permits, each section with examples

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/keto/reference/ory-permission-language.mdx
Comment thread docs/keto/reference/ory-permission-language.mdx Outdated
Comment on lines +127 to +130
restricted: (ctx: Context) =>
this.related.allowlist.includes(ctx.subject) &&
!this.related.blocklist.includes(ctx.subject),
```
Comment on lines +137 to +138
edit: (ctx: Context) => this.related.owners.includes(ctx.subject),
admin: (ctx: Context) => this.permits.edit(ctx) && this.related.admins.includes(ctx.subject),
Comment thread docs/keto/reference/ory-permission-language.mdx Outdated
Comment on lines +23 to +24
The `related` block defines who can be associated with an object. Each entry names a relation and declares what subject types it
holds.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be confusing, as it is not necessarily clear what objects and subjects are here. I would either add something to namespaces section, or define object and subject here as the direction of relations.

Something along the lines:

Suggested change
The `related` block defines who can be associated with an object. Each entry names a relation and declares what subject types it
holds.
The `related` block defines the relations between namespaces (classes). Each relation has a direction: `subject` is in `relation` of `object`. The relations defined in an OPL class (object) declare the allowed subjects. Generally, relations should represent the "real-world" relations between objects and subjects.

Then add something after the example saying which relations this defines and what the objects/subjects are.

identifier = letter { letter | digit } .
digit = "0" … "9" .
letter = "A" … "Z" | "a" … "z" | "_" .
class File implements Namespace {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class User missing

ClassDecl = "class" identifier "implements" "Namespace" "{" ClassSpec "}" .
ClassSpec = [ RelationDecls ] | [ PermissionDefns] .
```
### Subject-set references
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to completely omit this section, as subject sets are not really useful currently. There is no specific reason to use them, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(.).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

```

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`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear to me what this means.


```ts
class User {}
The `permits` block defines boolean functions the engine evaluates during a check.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The `permits` block defines boolean functions the engine evaluates during a check.
The `permits` block defines permissions. These are evaluated during a permission check request. Permissions are written as functions that return a boolean representing whether a permission is granted or not. While relations follow the real-world, permissions define application-specific rules based on relations.

```

The `ctx` object is a fixed parameter that contains the `subject` for which the permission check should be conducted:
`this.related.x.traverse(g => ...)` iterates over the objects in relation `x` and evaluates the inner expression for each one.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`this.related.x.traverse(g => ...)` iterates over the objects in relation `x` and evaluates the inner expression for each one.
`this.related.x.traverse(g => ...)` iterates over the subjects in relation `x` and evaluates the inner expression for each one.

Comment on lines +139 to +140
edit: (ctx: Context) => this.related.owners.includes(ctx.subject),
admin: (ctx: Context) => this.permits.edit(ctx) && this.related.admins.includes(ctx.subject),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the example is not the most natural one

Suggested change
edit: (ctx: Context) => this.related.owners.includes(ctx.subject),
admin: (ctx: Context) => this.permits.edit(ctx) && this.related.admins.includes(ctx.subject),
isAdmin: (ctx: Context) => this.related.admins.includes(ctx.subject),
edit: (ctx: Context) => this.permits.isAdmin(ctx) ||this.related.owners.includes(ctx.subject),

Comment on lines 149 to 151
related: {
members: (User | Group)[]
members: (User | SubjectSet<Group, "members">)[]
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, ...)

Suggested change
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))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +179 to +183
This schema models:

- Direct access via `viewers` and `owners`
- Group-based access via `SubjectSet<Group, "members">`
- Inherited access from parent folders via `traverse`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants