Skip to content

ReBAC

Relationship-based access control

ReBAC or Relationship-based access control is a model where authorizations to resources are based on relationships between different resources, such as data ownership, parent-child relationships, groups, and hierarchies.

Let’s explore different ReBAC scenarios and how they can be implemented using Kilpi.

Data ownership

“The user should be able to delete their own comment”.

This is trivial to implement with Kilpi as follows.

export const policies = {
comments: {
delete(user, comment: Comment) {
return user && user.id === comment.userId ? grant(user) : deny()
}
}
}

Parent-child relationships

“The user should be able to delete a folder if they are the owner of the folder or any parent folder”.

Implementing parent-child relationships in your authorization model is less a problem of how to implement it in Kilpi, and more a problem of data-modeling and designing efficient queries.

Let’s imagine the following data model:

CREATE TABLE folders
(
id SERIAL PRIMARY KEY,
name TEXT,
parent_id INT,
owner_id INT
);

One way of approaching this problem is with a recursively fetching authorization policy.

async function isOwnerOfFolderOrParent(folderId: string): Promise<boolean> {
// Fetch folder
const folder = await db.getFolderById(folderId);
// Is owner: Stop recursion
if (folder?.ownerId === user.id) return true;
// Recursively check parent folders until root
if (folder?.parentId) return isOwnerOfFolderOrParent(folder.parentId);
// Base case
return false
}
export const policies = {
folders: {
async delete(user, folderId: string) {
if (!user) return deny("Unauthenticated");
return await isOwnerOfFolderOrParent(folderId) ? grant(user) : deny()
},
}
}

Groups (e.g. Organizations)

“The user should be able to create a document in an organization if they are a member of the organization”.

Instead of using an asynchronous data-fetching policy as we did for parent-child relationships, let’s explore integrating memberships into the subject.

This is a great alternative if most of your authorization policies are based on memberships, which is common in many SaaS applications.

type Subject = User & {
memberships: Array<{
organizationId: string;
role: string;
}>
};
export async function getSubject() {
const user = await getCurrentUser();
const memberships = await listMembershipsForUser(user.id);
return Object.assign(user, { memberships })
}
// Tip: Use utility functions (You can extend this function with e.g. role checks in the future)
export function isMember(user: Subject, orgId: string) {
return user.memberships.some(m => m.organizationId === orgId);
}
export const policies = {
documents: {
create(user: Subject, organizationId: string) {
if (!user) return deny("Unauthenticated");
return isMember(user, organizationId) ? grant(user) : deny()
},
}
}

Permissions

See the article on implementing permissions using Kilpi for a more detailed guide on implementing a dynamic permissions-based system.

Generalized ReBAC solution

In general, any ReBAC problem can be solved by designing a data model and queries for fetching the necessary relationships. Here are some common strategies for querying your data model.

  1. Pass as resource

    When the relationship is between the resource and the subject, it is best to pass the resource as an argument to the policy function directly. See the Data ownership example.

  2. Fetch in policy

    For data that can not be considered the resource of the policy, and is not global enough to be included in the subject, you can fetch in an asynchronous policy. See the Parent-child relationships example.

  3. Pass in subject

    For data that can be considered global enough to be always fetched, you can include it in the subject. See the Groups (e.g. Organizations) example.