Developing a namespace

note

This guide makes heavy use of the Authzed Playground as an easy way to develop and validate the namespace being constructed. We recommend using the Playground whenever possible to develop your namespaces.

What is a namespace?#

A namespace in Authzed is the definition of a class of object.

Examples include resources, users, groups, documents, or any kind of object either being protected or for whom something is being protected.

Relations#

Namespaces can define zero or more relations, which indicate (as the name implies) how one object relates to another.

Examples include roles (this user has this role on this object), membership (this group is a member of this other group), ownership (this resource is owned by this organization), etc.

Defining a namespace#

Namespaces in Authzed are defined in the Authzed namespace configuration format, which is a textual representation of a series of Protocol Buffer messages:

name: "mytenant/mynamespace"
relation { name: "reader" }
...

Namespaces are written to a tenant by use of the Write Namespace API call.

A very basic example#

info

This guide will use the tenant name example on namespaces. The Authzed Playground will accept any tenant name, but make sure to replace with your own tenant before trying against a real Authzed API.

For this example, let's imagine a basic system consisting of a resource to be protected, such as a document, and users that can potentially access that resource via one or more roles such as reader or writer.

Define our namespaces#

To begin, we first need to define the namespaces to represent the two classes of objects in our system: document and user:

document#
name: "example/document"
user#
name: "example/user"

So far, our namespaces don't do much: They define the two classes of objects for our system, but as they don't have any relations defined, they cannot be related to one another in any form.

Defining our first relation#

Our next step, therefore, is to decide how our namespaces can relate to one another, thus defining both the kind of data we can store in Authzed, as well as the questions we can ask of it via Check.

For this example, we've chosen a simple RBAC-style permissions model, where users can be granted a role, such as reader, on our resource (document).

The choice of RBAC therefore means that the relationship between our resource (document) and our users will be defined by the roles we want, and thus, we can start by defining a relation on our document to represent one of these roles (here, reader):

name: "example/document"
relation {
name: "reader"
}
info

Note that the definition of reader above is a direct definition: By not specifying anything but the name of the relation, we indicate to Authzed that the relation will only consist of its own data, unrelated to other relations.

Namespaces Playground#

Validating our namespace#

To validate that our namespace definition is correct, the Authzed Playground supports the defintion of Validation Tuples to define the test data for our system. In addition, the Playground has an Expected Relations section, which defines a set of permissions to lookup, and the full set of users found for each.

What can we ask?#

With the definition of our two object classes as namespaces, and the reader relation on our document resource, we now have enough to answer a basic question:

Can a specific user/group/object view a specific document?

In Authzed, such a question is answered via use of a Check call, which takes in two objects (the object and the subject), as well as a possible relation between them (the action), and returns whether the userset is reachable from the target, via the relation specified.

Can a specific user/group/object view a specific document?
^ subject ^ object
^ action

To add this question for validation in the Authzed Playground, we must translate the question into the Validation Tuples and the Expected Relations.

Creating validation tuples#

To translate the question, we first must write a tuple to represent the test data for the request.

First, let's reframe our question a bit to make the example easier:

Can [specificuser] <view> [specificdocument]?

To test the above, we need to add at least one relation from a document to a user, indicating that the user can view the document.

To do so, let's rephrase the question into a Check:

Check: [specificdocument] <reader> [specificuser]

Note that we've reversed the order here: Tuples always go from the target (here specificdocument) to the userset. Since we've reversed the order, we've also changed the name of the attribute being checked from view to reader, to match the relation we've defined.

Our next step is to translate the objects into their object reference forms:

example/document:specificdocument <reader> example/user:specificuser

Here, we've indicated to Authzed that the document is of kind example/document, while the userset is example/user, since those are the namespaces we defined above. The portion of the reference after the : is the ID of the object.

info

Note that here we are using IDs specificdocument and specificuser in the object references.

In a real system, these IDs would most likely be the primary key of the rows for these objects in the database tables representing documents and users, respectively. Users are also sometimes represented by an external ID, such as an e-mail address or the sub field of an OAuth token.

The choice of object IDs is up to you, but must be unique and stable within a namespace.

Next, we need to indicate how the user is translated into a userset: In Authzed, a userset is itself an object, and is therefore represented as an object reference (here example/user:specificuser) as well as a relation.

In this example, we make of the implicitly defined special relation ..., which indicates that we want to treat the user as an opaque object:

example/document:specificdocument <reader> example/user:specificuser#...
info

Note that we could have just as easily defined a relation on the user and used it instead. A defined non-... relation is typically used if you want to have multiple ways to represent an object as a userset.

For example, if the userset was a group, you might want to reference example/group:specificgroup#members, to indicate it is the group's members that have reader, rather than the group itself.

The final step in translating is to link the object references by the relation we've created:

example/document:specificdocument#reader@example/user:specificuser#...

To connect our two object references via our relation reader, we use the syntax # and @ to create the final tuple.

We now have a valid tuple representing that specificuser can view specificdocument.

Validation Data Playground#

Writing expected relations#

Now that we have test data, we can write an expected relation definition for the playground to validate that our namespace configuration and test data are valid.

Validation rules within the Playground are defined in the Expected Relations section.

Expected Relations is a dictionary, in YAML form, going from our verification objects and relations, to the expected related objects for that object and relation.

Recall the question from above that we wished to ask:

Can a specific user/group/object view a specific document?
^ subject ^ object
^ action

Translating this into an expected relation, we place the object and action as a key of the YAML block:

example/document:specificdocument#reader:

We then list the expected subject(s) for that key. Since our validation tuples only contains a single user, we only specify a single expected user, in the square brackets [...]:

example/document:specificdocument#reader:
- "[example/user:specificuser#...]"

Next, we must specify the expected relation to be found, in angled brackets: <...>

example/document:specificdocument#reader:
- "[example/user:specificuser#...] is <example/document:specificdocument#reader>"
info

Why we need to repeat the expected relation will become important later in the document.

Testing the namespace configuration#

Now that we've written a complete set of namespace configurations, validation tuple data and expected relations, we can hit the Validate button and see that our configuration is indeed valid:

We've just successfully written our very first namespace configurations, and now have a working permissions system for documents.

Basic Configurations Playground#

Expanding our configuration#

While being able to ask whether a user is a reader of a document is super useful, it is expected that the majority of permissions systems will consist of more than a single role.

As we discussed at the beginning of the guide, for our example we'd like to have a second role, that of writer, which will allow us to check if a user is a writer on the document.

Adding the writer relation#

To begin, we once again start by adding another relation, in this case writer:

name: "example/document"
relation {
name: "reader"
}
relation {
name: "writer"
}

Adding a writer validation tuple#

Next, we'd like to be able to test our new relation, so we add another validation tuple for a different user:

example/document:specificdocument#reader@example/user:specificuser#...
example/document:specificdocument#writer@example/user:differentuser#...

Adding an expected relation#

Finally, we add an expected relation for the new relation, to validate it:

example/document:specificdocument#reader:
- "[example/user:specificuser#...] is <example/document:specificdocument#reader>"
example/document:specificdocument#writer:
- "[example/user:differentuser#...] is <example/document:specificdocument#writer>"

Writer user playground#

We verify this via the playground:

Implicit from writer#

The above configuration and validation exposes one issue, however: All users that are assigned to the relation writer should also be assigned to the relation reader.

As a naive solution, we could write a reader tuple for every user when we write writer, but that would get difficult to maintain very quickly.

Instead, ideally we'd like a user being assigned to writer to be implicitly assigned to reader, such that we only ever need to write one tuple representing the user's actual relation/role.

Implicit relations via userset_rewrite#

Fortunately, Authzed supports definining implicit relationships via the user of userset_rewrite rules.

A userset_rewrite rule can be thought of an override: It indicates to Authzed how to compute the set of userset tuples reachable from a relation.

To begin, let's take our existing reader relation and add a userset_rewrite:

name: "example/document"
relation {
name: "reader"
userset_rewrite {
union {
child { _this {} } # Indicates that `reader` can have its own tuples.
}
}
}
relation {
name: "writer"
}

In the above, we add a userset_rewrite with a union rule, which tells Authzed to union together all the users found for each child rule.

The first rule we add is _this, which indicates to Authzed that reader should always include its own usersets (since we are defining validation tuples for it).

Next, we want to include all the usersets found in writer as well. We do so by adding another child to the union block with a computed_userset pointing to writer:

name: "example/document"
relation {
name: "reader"
userset_rewrite {
union {
child { _this {} } # Indicates that `reader` can have its own tuples.
child { computed_userset { relation: "writer" } } # + those from `writer`.
}
}
}
relation {
name: "writer"
}

This tells Authzed to include all usersets found in the writer relation on the same namespace.

Updating our expected relations#

Now that we've updated our namespace configuration to have reader implicitly include all users found in writer, we must update our expected relations to include the user(s) as well:

example/document:specificdocument#reader:
- "[example/user:specificuser#...] is <example/document:specificdocument#reader>"
- "[example/user:differentuser#...] is <example/document:specificdocument#writer>"
example/document:specificdocument#writer:
- "[example/user:differentuser#...] is <example/document:specificdocument#writer>"

In the above example, we've added example/user:differentuser#... to the reader list, as that user will now be included in reader.

Note, however, that the contents of the expected relation (in <...>) for differentuser is different: It says writer instead of reader, as expect that the permission of reader will have be answered by differentuser being assigned the relation writer.

info

The inclusion of the expected relation in the angle brackets ensures that validation verifies not only the users found, but also how they were found.

Implicit reader example playground#

We can now verify via the playground that our permission was implied properly:

Preparing to inherit permissions#

As we've seen above, we can use userset_rewrite to define implicit permissions, such as writers automatically having reader permission. Implicit permissions within a namespace, however, are often insufficient: Sometimes permissions need to be inherited between namespaces.

As an example: Imagine that we add the concept of an organization to our permissions system, where any user that is an administrator of an organization automatically gains both reader and writer permissions on any document within that organization... how would we define such a permissions model?

Defining organization#

To begin, we must first define the namespace that represents our organization. Here, we include the admin relation, to represent the administrator role for users:

name: "example/organization"
relation {
name: "admin"
}

Connecting organizations and documents#

In order for our inheritance to function, we must define a way to indicate that a document "lives" under an organization. Fortunately, this is just another relationship (between a document and its parent organization), so we can use another relation within the document namespace:

name: "example/document"
relation {
name: "docorg"
}
... reader and writer ...

Here we've chosen to call this relation docorg, but it could be called anything; it is generally recommended to use either a contraction of the two namespaces being connected or, alternatively, a term representing the actual relationship between the objects in those namespaces (such as parent).

Adding the relationship#

Now that we've defined the relation to hold our new relationship, we need to define the relationship itself in our test data:

example/document:specificdocument#docorg@example/organization:someorg#...
info

Note the use of the organization as the "user" in this relation tuple: Here the subject of the tuple is someorg, the action is docorg and the object is specificdocument.

Verifying the relationship#

Once this tuple is added, we can add the document to the Expected Relations list, to verify that the relationship does exist:

example/document:specificdocument#docorg:
- "[example/organization:someorg#...] is <example/document:specificdocument#docorg>"
info

This also means you can issue a Check that someorg is the organization for the document: Since all relationships in Authzed are stored using the same tuple system, you can Check not just for users, but any form of userset.

Document owned by organization playground#

Definining inheritance#

Now that we have a means of stating that a document is owned by an organization, and a relation to define administrators on the organization itself, our next step is to connect the permissions defined on the document to the the adminstrator.

To do so, we once again make use of userset_rewrite. Recall our existing namespace configuration for example/document:

name: "example/document"
relation {
name: "docorg"
}
relation {
name: "reader"
userset_rewrite {
union {
child { _this {} } # Indicates that `reader` can have its own tuples.
child { computed_userset { relation: "writer" } } # + those from `writer`.
}
}
}
relation {
name: "writer"
}

In the above existing configuration, we've defined that reader consists of the users found in itself (_this), as well as in the relation writer, to ensure that all writers are implicitly also readers.

Our task now is to add further implicit rules: All users which are in admin on the document's organization should implicitly be both writer and reader.

To define this implicit relationship between namespaces, we first change writer so it itself supports a union:

name: "example/document"
relation {
name: "docorg"
}
relation {
name: "reader"
userset_rewrite {
union {
child { _this {} } # Indicates that `reader` can have its own tuples.
child { computed_userset { relation: "writer" } } # + those from `writer`.
}
}
}
relation {
name: "writer"
userset_rewrite {
union {
child { _this {} } # Indicates that `writer` can have its own tuples.
}
}
}

Once again we start with a child that includes _this, as we have already defined users that are directly applied to the writer relation.

Our next step is to add another branch to the union that will include all users in the admin relation:

name: "example/document"
relation {
name: "docorg"
}
relation {
name: "reader"
userset_rewrite {
union {
child { _this {} } # Indicates that `reader` can have its own tuples.
child { computed_userset { relation: "writer" } } # + those from `writer`.
}
}
}
relation {
name: "writer"
userset_rewrite {
union {
child { _this {} } # Indicates that `writer` can have its own tuples.
child { computed_userset { relation: "admin" } } # + those from `admin`? But `document` doesn't have an admin...
}
}
}

Uh oh... The document namespace doesn't have an admin relation... how do we get to the organization namespace?

tuple_to_userset#

The answer is a new operator in namespace configurations: tuple_to_userset. tuple_to_userset provides a means of "walking" across one relation to another namespace, and the other relation within.

As defined above, we want to "walk" from the document across the docorg relation, to the organization, and from there we can look in the admin relation. Therefore, we update our namespace confing like so:

name: "example/document"
relation {
name: "reader"
userset_rewrite {
union {
child { _this {} } # Indicates that `reader` can have its own tuples.
child { computed_userset { relation: "writer" } } # + those from `writer`.
}
}
}
relation {
name: "writer"
userset_rewrite {
union {
child { _this {} } # Indicates that `writer` can have its own tuples.
child {
tuple_to_userset {
tupleset { relation: "docorg" } # Walk from document->organization
computed_userset {
object: TUPLE_USERSET_OBJECT
relation: "admin"
}
}
} # + those from `admin` on the organization
}
}
}

tuple_to_userset explaination#

tuple_to_userset is a bit verbose, so it is important to explain.

The first child of tuple_to_userset is a tupleset, which indicates the relation to "walk" from the current object (in this case: document). Here the tupleset and its relation indicates we want Authzed to "walk" from the document to any subjects found via the docorg relation.

info

Note that the walk does not specify the target namespace: This means if objects under two namespaces are referenced via the docorg relation, the "walk" will succeed across both.

Using a unique name for relations referenced by tuple_to_userset is therefore a really good practice.

The second child of tuple_to_userset is a computed_userset; it is very similar to the one used above for implicitly defining that all readers are writers, with one extra piece: we specify an explicit object as TUPLE_USERSET_OBJECT. This is necessary to indicate to Authzed that the relation within the computed_userset should be checked on the result of the "walk", rather than the original object.

note

No other values are supported here, so just always specify TUPLE_USERSET_OBJECT when using tuple_to_userset

Adding an administrator user#

Now that we've declared that all users in admin on the organization are also implicitly within writer, let's define at least one user in our test data to be an adminstrator:

example/organization:someorg#admin@example/user:someadminuser#...

Testing inherited permissions#

Finally, we can add the user to the declarations in Expected Relations and verify that the inheritance works:

example/document:specificdocument#reader:
- "[example/user:specificuser#...] is <example/document:specificdocument#reader>"
- "[example/user:differentuser#...] is <example/document:specificdocument#writer>"
- "[example/user:someadminuser#...] is <example/organization:someorg#admin>"
example/document:specificdocument#writer:
- "[example/user:differentuser#...] is <example/document:specificdocument#writer>"
- "[example/user:someadminuser#...] is <example/organization:someorg#admin>"
example/document:specificdocument#orgdoc:
- "[example/organization:someorg#...] is <example/document:specificdocument#orgdoc>"
note

Note that we have to add someadminuser to both reader and writer, as reader still implicitly receives all members of writer.

This starts to demonstrate the true power of Authzed: We've defined a single additional implicit rule yet received the closure of all our rules.

info

Note the expectation of <example/organization:someorg#admin> for someadminuser, instead of reader or writer on the document: the permission is being granted by virtue of the user being an admin on the organization.

Full inherited permissions playground#

And that's it! We've successfully created a working set of namespace configurations, with both implicit and inherited permissions, added test data, and can now validate the entire bundle by making use of the Authzed Playground:

Get started for real#

Want to get started for real? We’ll model your permissions with you. Speak with one of our engineers