The Ory Permission Language is a language that allows configuring Ory Keto.

What is it good for

Before September 2022 Ory Keto - which powers Ory Permissions - only supported storing and checking direct relationships between subjects and objects. As an example, consider a basic file-sharing service. To track ownership of a file, it stores a relationship patrik is owner of file secrets.txt. In this domain, it is common that owners have read and write permissions, but Keto does not know that. The Ory Permission Language allows one to define such global rules.

Permission checks (e.g., can patrik read secrets.txt) are then answered based on the stored relationships (e.g., patrik is owner of file secrets.txt) and the rules defined through the OPL (e.g.,any owner of a document can also read it).

Requirements

First, we defined the requirements and scope for the new Ory Permission Language.

  • Powerful, yet easy to understand and maintain.

    The cost of long-term maintenance was the decisive factor because a custom language is the most expressive but requires specialized domain knowledge.

  • A language that you configure as you grow, and that grows with you.

    This includes that new people don't need workshops to understand what is going on, and no refreshment course is needed to change permissions months after you last touched the system.

  • Excellent editor support as well as an automated way to test changes.

    As always the development experience is front & center when new features are designed. If you have any feedback that could help with your work as developer, feel free to share it with the Ory community.

  • Not Turing complete.

    This minimizes room for errors and makes it impossible to generate a code path that exhausts time or space constraints, in line with the Ory software architecture guidelines.

Ideation

The next step was to come up with proposals and analyze other solutions. Ory Keto is the first open-source implementation of Google Zanzibar but is not the only one. Because the Zanzibar paper is vague in parts, and the system was driven by Google's internal requirements, everyone has a different idea of how to implement the concept of graph-based access control. The concept of global rules was described in the paper, and adopted by all implementations in one way or the other. Therefore we came up with several proposals, including competitors' permission languages, and compared all of them during user interviews.

Interview setup

Being the first open-source Zanzibar implementation, Ory Keto has a vibrant user community. To make sure that we arrive at a user-friendly permission language we had many interviews with community members. During the interviews, we proposed four different permission languages and tasked the interviewees with understanding and extending the rules in each language.

The four languages all described access to documents such that users can be an owner, editor, or viewer of a document. An owner is also an editor of a document, and an editor is also a viewer of a document. Furthermore, documents can be arranged in a hierarchy: if a user is a viewer of a parent document, they can also view that document.

1: Original Zanzibar AST

The original syntax from the Zanzibar paper, which is just the internal abstract syntax tree (AST) presented as Protobuf text format. We included this language as a baseline, and all interviewees found the syntax verbose and confusing.

name: "document"
relation { name: "owner" }
relation {
    name: "editor"
    userset_rewrite {
        union {
            child { _this {} }
            child { computed_userset { relation: "owner" } }
        }
    }
}
relation {
    name: "viewer"
    userset_rewrite {
        union {
            child { _this {} }
            child { computed_userset { relation: "editor" } }
            child { tuple_to_userset {
                tupleset { relation: "parent" }
                computed_userset {
                    object: $TUPLE_USERSET_OBJECT # parent folder
                    relation: "viewer"
                } }
            }
        }
    }
}

2: Pythonesque

As a second alternative, we used the DSL version of the Auth0 FGA configuration language. The language somewhat resembles Python with its concise and whitespace-significant syntax. In our interviews, we found that people were confused by missing type annotations and as self syntax (similar to child { _this {} } from the Zanzibar AST).

type document
    relations
        define parent as self
        define owner as self
        define editor as self or owner
        define viewer as self or editor or viewer from parent

3: Typed & logical

Next, we introduced a language that uses first-order logic to express permission rules and types to restrict relationships. Our takeaways were that while the types are useful, the logical component was polarizing. Some people struggled with the logical implications, while others loved them.

type user {}
type document {
	relation owner, editor, viewer: user

	relation parent: document
}

for all document:d, user:u {
	u is owner of d => u is editor of d
    u is editor of d => u is viewer of d
}
for all document:d, document:p, user:u {
	p is parent of d & u is viewer of p => u is viewer of d
}

4: Typed & declarative

For the final language, we used the AuthZed schema language. It features a type system as well as a declarative syntax. While the interviewees liked the types, they had trouble grasping the concepts and failed at writing transitive rules. An example of a transitive rule is parent->viewer, meaning that you can view a document if you are a viewer of the parent.

definition user {}
definition document {
	relation viewer: user
	relation editor: user
	relation owner: user

	relation parent: document

	permission view = owner or editor or viewer or parent->viewer
	permission edit = owner or editor
	permission own = owner
}

Interview takeaways

  • Types help guide the user toward understanding the permission model. We want types in our language.
  • Transitive rules were hard to grasp. This was true for all languages.
  • No language felt familiar, since all DSLs were custom-made just for expressing permission rules.
  • It was hard to predict the implications of changes to the permission language. For us, this meant that the language tooling should support testing.

The Ory Permission Language

From the user interviews and our original requirements, we gathered that our permission language should be self-explanatory and familiar, with good editor support. Linters and unit test frameworks would help the users modify the permission rules with high confidence.

Therefore, we based the Ory Permission Language on TypeScript (more precisely, a non-Turing-complete subset of TypeScript, see the spec of the Ory Permission Language). TypeScript and JavaScript are widely used programming languages that most programmers probably have come in contact with. Further, there is out-of-the-box syntax highlighting, and auto-completion through type declarations in a wide variety of editors and IDEs. This also opens the possibility to unit-test the permission rules with a test runner such as Jest.

Let's look at the example from above written in the Ory Permission Language:

import { Namespace, SubjectSet, Context } from "@ory/permission-namespace-types"

// Namespaces are declared as classes that implement the `Namespace` interface.
class User implements Namespace {
  related: { manager: User[] }
}

class Group implements Namespace {
  related: {
    // The relation types are declared in the TypeScript syntax:
    members: (User | Group)[]
  }
}

class Folder implements Namespace {
  related: {
    parents: Folder[]
    // SubjectSet<T, "r"> refers to the relation r of namespace T.
    viewers: SubjectSet<Group, "members">[]
  }

  // Permissions are declared separately from relations as functions that take a
  // context (containing the subject of a permission check) and that defer the
  // decision to either another permission or relation.
  permits = {
    view: (ctx: Context): boolean =>
      this.related.viewers.includes(ctx.subject) ||
      this.related.parents.traverse((p) => p.permits.view(ctx)),
  }
}

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

  permits = {
    view: (ctx: Context): boolean =>
      this.related.viewers.includes(ctx.subject) ||
      this.related.parents.traverse((p) => p.permits.view(ctx)) ||
      this.related.owners.includes(ctx.subject),

    edit: (ctx: Context) => this.related.owners.includes(ctx.subject),
  }
}

Implementation

We carefully defined the Ory Permission Language as a subset of TypeScript that is not Turing-complete and maps well to the original Zanzibar AST. This allows us to re-use all caching and optimization improvements from the Zanzibar paper.

For our implementation, we wrote a custom parser in Go that only accepts the subset that we can handle. You can check out the lexer and parser on GitHub. We also have an extensive suite of tests and a fuzzer to check the correctness and robustness of the parser.

Use it!

The Ory Permission Language is now part of Ory Keto as well as built into the Ory Network. The best way to get started is to use our guide to defining global permission rules using the Ory Permission Language in combination with the Ory Console, all in your browser.

If you want to chat, be sure to say hi in our community Slack channel and share your experiences with us.

Never miss an article - Subscribe to our newsletter!