Skip to main content

Object Types

Object types are the basic building block of any authorization scheme in Warrant. Represented as JSON, each object type defines a type of resource (e.g. stores, items, etc.) in an application along with the relationships that can exist between that and other resources in the application (e.g. users). Object types are an incredibly flexible way to define authorization models and even allow you to express complex hierarchical and inherited relationships.

Object types can be created directly in the Warrant dashboard or via the Object Types API.

In this overview, we'll explain the key attributes of object types by creating an authorization model for a simple eCommerce application with three object types: user, store, and item.

Type

The first attribute of an object type is its type. Each object type must have a unique string as its type. In our eCommerce app, we'll have the following object types:

{
"type": "user"
},
{
"type": "store"
},
{
"type": "item"
}

Relations

By defining the object types above, we've started building an authorization model for our application that will allow us to create fine-grained access control rules for stores, items, and users, helping us answer questions like:

Does [user:1] have the ability to [edit] [item:x]?
is [user:1] the [owner] of [store:3]?

In order to create access rules using our object types, we first need to add relations to them. The relations of an object type define the relationships available on an object of that type. For example, if we want to specify that [user:A] is an [owner] of [store:S], we must add an owner relation to the store object type.

There are two types of relations that can be defined on object types:

We'll start by adding some direct relations to our object types.

Direct Relations

All relations are direct relations by default. This means they must be explicitly granted via a warrant. In our example application, a store can have owners, editors, and viewers. owners and editors have more privileged access (like being able to modify details about a store) than viewers (who have read-only access).

An item can have the same three relations as a store plus a fourth relation called parent. This is because a store can be the parent of an item, meaning that the item belongs to that store. We'll use this relation later to implement inherited relations on items.

Lastly, our user object type is relatively simple and has one manager relation. This is because a user can be the manager of another user. We'll use this relation later to enable inherited relations based on user hierarchies.

Let's add these relations to our object types:

{
"type": "user",
"relations": {
"manager": {}
}
},
{
"type": "store",
"relations": {
"owner": {},
"editor": {},
"viewer": {}
}
},
{
"type": "item",
"relations": {
"owner": {},
"editor": {},
"viewer": {},
"parent": {}
}
}

With these object types, we can now create authorization rules that specify exactly which users are owners, editors, and viewers of each store and item. We can also assign stores as parents of items, and users as managers of other users.

Inherited Relations

Using only direct relations to build your authorization model can be powerful, but explicitly creating warrants for each and every relationship in an application can become tedious or infeasible in larger, more complex use cases. That's why relations can define conditions under which they will be inherited (e.g. a user is an editor of a store if they're an owner of that store). There are two ways to specify how relations can be inherited:

Inherited Hierarchical Relations

In practice, it's common for relations to have overlap (e.g. an owner has the same privileges as an editor + other privileges). For example, in many applications a user with write privileges often inherits read privileges too. In our example application, an owner is also both an editor and a viewer, and an editor is also a viewer. Instead of having to explicitly assign each of the owner, editor, and viewer relations to a user who is an owner, object types allow you to specify a relation hierarchy (e.g. the editor relation is inherited if the user is an owner) using the inheritIf property. Let's add inheritIf rules to our store and item object types specifying that:

  • owners are also editors
  • editors are also viewers
{
"type": "user",
"relations": {
"manager": {}
}
},
{
"type": "store",
"relations": {
"owner": {},
"editor": {
"inheritIf": "owner"
},
"viewer": {
"inheritIf": "editor"
}
}
},
{
"type": "item",
"relations": {
"owner": {},
"editor": {
"inheritIf": "owner"
},
"viewer": {
"inheritIf": "editor"
},
"parent": {}
}
}

With our inheritIf rules in place, we can simply grant a user the editor relation and they will implicitly inherit the viewer relation. inheritIf rules also work recursively on other inherited relations, so assigning a user the owner relation will implicitly grant that user both the editor and viewer relations. This is because owner will inherit editor and editor will in turn inherit viewer. This will simplify our access checks and cut down on the number of warrants we need to create for each user.

Inherited Object Relations

In many applications, resources have their own hierarchy (e.g. a document belongs to a folder) and the access rules for these resources follow that hierarchy (e.g. the owner of a folder is the owner of any document in that folder). By combining the inheritIf, ofType, and withRelation properties, you can specify that a relation will be inherited when a user has a specified relation (inheritIf) on another type of object (ofType) with a hierarchical relation (withRelation) on the current object. For example, a user is an editor of a document if they are an editor of a folder that is the document's parent. In our example app, let's define three inherited object relations:

  1. A user is an owner of an item if that user is an owner of a store that is the item's parent.
  2. A user is an editor of an item if that user is an editor of a store that is the item's parent.
  3. A user is an editor of an item if that user is the manager of the user that is the item's owner.

NOTE: Some of the relations below will be composing multiple inheritance rules together using logical operators. We'll cover this in detail later.

{
"type": "user",
"relations": {
"manager": {}
}
},
{
"type": "store",
"relations": {
"owner": {},
"editor": {
"inheritIf": "owner"
},
"viewer": {
"inheritIf": "editor"
}
}
},
{
"type": "item",
"relations": {
"owner": {
"inheritIf": "owner",
"ofType": "store",
"withRelation": "parent"
},
"editor": {
"inheritIf": "anyOf",
"rules": [
{
"inheritIf": "owner"
},
{
"inheritIf": "editor",
"ofType": "store",
"withRelation": "parent"
},
{
"inheritIf": "manager",
"ofType": "user",
"withRelation": "owner"
}
]
},
"viewer": {
"inheritIf": "editor"
},
"parent": {}
}
}

The inheritIf, ofType, and withRelation properties make it easy to define inheritance rules for complex relationships between objects so we don't have to create a large number of explicit warrants. Without them, we'd need to create a warrant for every item ↔ store ↔ user relationship in our application. This could easily be thousands, if not hundreds of thousands of rules.

Composing Inherited Relations Using Logical Operators

With both direct and inherited relations in our toolkit, we can create authorization models for a majority of use cases, but there are still some scenarios in practice that require a combination of inheritance rules (e.g. a user is an editor of an item if they are an owner of that item OR they are the manager of another user who is an editor of that item). To support designing authorization models that cover such scenarios, relations can compose multiple inheritance rules using logical operations to form more complex conditions.

The three supported logical operations are anyOf, allOf, and noneOf.

anyOf

The anyOf operation allows you to specify a set of rules that are considered fulfilled if at least one of the rules in the set is satisfied. In other words, it works like the logical OR operation. The following object type specifies an editor-or-viewer relation that is inherited if the user is an editor OR if the user is a viewer:

{
"type": "item",
"relations": {
"editor": {},
"viewer": {},
"editor-or-viewer": {
"inheritIf": "anyOf",
"rules": [
{
"inheritIf": "editor"
},
{
"inheritIf": "viewer"
}
]
}
}
}

allOf

The allOf rule type allows you to specify a set of rules that are considered fulfilled if all of the rules in the set are satisfied. In other words, it works like the logical AND operation. The following object type specifies an editor-and-viewer relation that is implicitly granted if the user is an editor AND the user is a viewer:

{
"type": "item",
"relations": {
"editor": {},
"viewer": {},
"editor-and-viewer": {
"inheritIf": "allOf",
"rules": [
{
"inheritIf": "editor"
},
{
"inheritIf": "viewer"
}
]
}
}
}

noneOf

The noneOf rule type allows you to specify a set of rules that are considered fulfilled if none of the rules in the set are satisfied. In other words, it works like the logical NOR operation. The following object type specifies a not-editor-and-not-viewer relation that is implicitly granted if the user is not an editor AND the user is not a viewer:

{
"type": "item",
"relations": {
"editor": {},
"viewer": {},
"not-editor-and-not-viewer": {
"inheritIf": "noneOf",
"rules": [
{
"inheritIf": "editor"
},
{
"inheritIf": "viewer"
}
]
}
}
}

Built-in Object Types

Warrant provides a default set of built-in object types to make it easier to implement common use-cases like role based access control, organization & team-based permissions, and pricing-tiers & feature entitlements without much configuration of object types. These built-in types also serve as a great starting point for more complex authorization use cases and can easily be modified to better suit the needs of an application.

User

In most cases, applications require access control rules to be defined per user. To make this easier, Warrant comes with a built-in user object type. This object type has one parent relation which makes it easy to associate users to a parent object such as a tenant (in a B2B context), a parent user, or a team. The full representation of the user object type is:

User
{
"type": "user",
"relations": {
"parent": {
"inheritIf": "parent",
"ofType": "user",
"withRelation": "parent"
}
}
}

Using this object type, we can create warrants for individual users like:

[user:7] is a [parent] of [user:84]
[tenant:A] is a [parent] of [user:1]

Tenant

Most multitenant B2B applications have a concept of tenants: a way to partition data and users between customers. Some applications might refer to a tenant as an organization, a customer, a company, or one of many other alternatives. Warrant helps enforce data isolation and access control across tenants in multitenant B2B applications by allowing you to specify authorization rules per tenant in your application. The full representation of the tenant object type is:

Tenant
{
"type": "tenant",
"relations": {
"admin": {},
"manager": {
"inheritIf": "admin"
},
"member": {
"inheritIf": "manager"
}
}
}

Role

Roles are one of the core building blocks of role based access control. They can be thought of as 'containers' or 'groups' of (typically) users. In most RBAC implementations, the set of roles is finite and usually based on some organizational structure and/or role (ex. admin, owner, member etc.). The role object type in Warrant has a member relation for designating that a user (or in some cases, another role) is a member of a particular role. The full representation of the object type is:

Role
{
"type": "role",
"relations": {
"owner": {},
"editor": {
"inheritIf": "owner"
},
"viewer": {
"inheritIf": "editor"
},
"member": {
"inheritIf": "member",
"ofType": "role",
"withRelation": "member"
}
}
}

Permission

Permissions are the second building block for implementing role based access control. Permissions typically represent specific abilities or actions (e.g. creating a report, editing a report, etc.) that can be taken within an application. The permission object in Warrant has a member relation for designating that a role (or user) has that particular permission. Permissions are typically assigned to roles and then the roles are assigned to users. However, sometimes permissions can be assigned directly to users. The full representation of the permission object type is:

Permission
{
"type": "permission",
"relations": {
"owner": {},
"editor": {
"inheritIf": "owner"
},
"viewer": {
"inheritIf": "editor"
},
"member": {
"inheritIf": "anyOf",
"rules": [
{
"inheritIf": "member",
"ofType": "permission",
"withRelation": "member"
},
{
"inheritIf": "member",
"ofType": "role",
"withRelation": "member"
}
]
}
}
}

Pricing Tier

Pricing tiers represent specific 'packages' (or 'tiers') of features within an application. They can be considered similar to roles in RBAC and can be assigned to specific users and/or tenants to grant them access to varying levels of features based on their subscription/payment plan. Pricing tiers are typically used in SaaS applications to implement and manage different pricing plans (ex. 'free', 'growth', 'enterprise'). The full representation of the pricing-tier object type is:

Pricing Tier
{
"type": "pricing-tier",
"relations": {
"owner": {},
"editor": {
"inheritIf": "owner"
},
"viewer": {
"inheritIf": "editor"
},
"member": {
"inheritIf": "member",
"ofType": "pricing-tier",
"withRelation": "member"
}
}
}

Feature

Features represent specific features in an application (e.g. 'analytics_dashboard', 'report_builder', etc.) and can be used to implement paid feature entitlements in conjuction with pricing tiers. Features are typically assigned to pricing tiers and those pricing tiers are assigned to tenants or users to grant them access to specific features in an application based on their subscription/payment plan. Features can be considered similar to permissions in RBAC. The full representation of the feature object type is:

Feature
{
"type": "feature",
"relations": {
"owner": {},
"editor": {
"inheritIf": "owner"
},
"viewer": {
"inheritIf": "editor"
},
"member": {
"inheritIf": "anyOf",
"rules": [
{
"inheritIf": "member",
"ofType": "feature",
"withRelation": "member"
},
{
"inheritIf": "member",
"ofType": "pricing-tier",
"withRelation": "member"
}
]
}
}
}