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 alsoeditors
editors
are alsoviewers
{
"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:
- A user is an
owner
of an item if that user is anowner
of astore
that is the item'sparent
. - A user is an
editor
of an item if that user is aneditor
of astore
that is the item'sparent
. - A user is an
editor
of an item if that user is themanager
of theuser
that is the item'sowner
.
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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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"
}
]
}
}
}