Introduction to handling permissions

Introduction to handling permissions

Simplest Permission System

At first, many of us begin with a basic role system:

type User = {
  id: string;
  role: "admin" | "editor" | "user";
};

function canDeleteArticle(user: User) {
  return user.role === "admin";
}

This setup is fine for basic needs. Admins can do everything, editors can edit, and users can view. It's simple and clear.

But as needs change, you might ask, "What if editors should delete their own articles?" Now our simple function changes to:

function canDeleteArticle(user: User, article: Article) {
  return (
    user.role === "admin" ||
    (user.role === "editor" && article.authorId === user.id)
  );
}

These checks start popping up all over your code. With each new requirement, more conditions are added, making the code harder to maintain and understand.

RBAC (Role-Based Access Control)

Instead of spreading permission checks throughout the code, we can centralize them using RBAC (Role-Based Access Control):

type Permission = "create:articles" | "edit:articles" | "delete:articles";

const ROLES = {
  admin: ["create:articles", "edit:articles", "delete:articles"],
  editor: ["create:articles", "edit:articles"],
  user: ["create:articles"],
} as const;

function hasPermission(user: User, permission: Permission) {
  return ROLES[user.role].includes(permission);
}

Better! Our permissions are now centralized and clear. Want to know what an editor can do? Just check the ROLES object.

However, RBAC has its limits. For example, the rule "editors can delete their own articles" can't be captured with simple permission strings.

ABAC (Attribute-Based Access Control)

This is where ABAC shines. Instead of just checking roles and permissions, we can check any attribute of the user, resource, or environment:

type User = {
  id: string;
  role: "admin" | "editor" | "user";
  department: string;
};

type Article = {
  id: string;
  authorId: string;
  department: string;
  status: "draft" | "published";
};

const permissions = {
  admin: {
    articles: {
      delete: true, // Admins can delete any article
      edit: true,
    },
  },
  editor: {
    articles: {
      delete: (user: User, article: Article) =>
        article.authorId === user.id || // Their own articles
        article.department === user.department, // Their department's articles
      edit: (user: User, article: Article) =>
        article.status === "draft" && // Only drafts
        (article.authorId === user.id ||
          article.department === user.department),
    },
  },
};

function can(user: User, action: string, resource: string, data?: any) {
  const permission = permissions[user.role]?.[resource]?.[action];
  if (typeof permission === "boolean") return permission;
  if (typeof permission === "function") return permission(user, data);
  return false;
}

Now we can set up complex rules like:

  • Editors can delete their own articles.

  • Editors can edit draft articles in their department.

  • Admins can do anything.

  • Access can depend on the time of day, user location, article status, and more.

The great thing about ABAC is its flexibility. You can use any attribute of the user, resource, or environment for permission checks. Plus, all your logic is centralized in one place.