0.6.0 • Published 7 months ago

pundit-ts v0.6.0

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

Pundit-TS - Plain Typescript Authorization Library

Pundit-TS is an authorization library highly-inspired by the pundit gem.

https://github.com/user-attachments/assets/c7815622-c1c9-4fbe-986e-6c9f88c8b31d

Use Cases

  • Apply RBAC, ABAC, DAC models (see the example snippets below for how to implement)
  • Check if a user is authorized to perform an action on an entity (ie. Post, Product, Category etc..)
  • Declare actions can be performed per entity class basis
    • UserActions: create, update
    • PostActions: create, publish, unpublish, update, delete
    • CategoryActions: create, update, delete
  • Filter entities on database to avoid unnecessary database queries
    • Apply joins, specific where clauses or similar things to filter database rows.
  • Apply user tiers: only premium users can add more than 3 seats to their organization.
  • Avoid code duplication: Keep your authorization logic in one place. Use whenever you need them. Change one place to affect rest of the code.

Examples

Blog: Plain typescript blog example. Has no relation with a database. Great starting point if you are just starting to use pundit-ts

Prisma Blog: Prisma ORM based blog example. Advanced version of the plain blog example. Uses Prisma ORM for querying database, utilizes PostPolicy#filter for building argument for prisma.post.findMany method.

Installation

npm i -S pundit-ts

🔒 Access Control Models

Here some examples to utilize pundit-ts for applying common access control models like RBAC, ABAC etc..

class Policy {
  authorize(ctx, object, action) {
    const isAuthenticated = ctx.actor !== null;
    const role = ctx.actor?.role;
    const isAdmin = role === "admin";
    const isEditor = role === "editor";

    switch (action) {
      case "create":
        return isAdmin || isEditor;
      case "delete":
        return isAdmin;
      case "view":
        return true; // everyone, including anonymous users can view everything
      default:
        return false; // disallow every other action
    }
  }
}
class Policy {
  authorize(ctx, object, action) {
    // non logged-in users cannot perform any action...
    if (ctx.actor === null) {
      throw new UnauthorizedError();
    }

    const role = ctx.actor.role;
    const isAdmin = role === "admin";
    const isEditor = role === "editor";

    switch (action) {
      case "delete": // only admins can delete the record
        return isAdmin;

      // update permissions
      case "update:content":
        return isAdmin || isEditor;
      case "update:title":
        return isAdmin;

      case "view:content":
      case "view:title":
        return true; // everyone can view title and content

      default:
        return false;
    }
  }
}

Great choice for multi-tenant applications (ie. SaaS applications). Here we use organization as our tenant.

class DocumentPolicy {
  authorize(ctx, object, action) {
    if (ctx.actor === null) {
      return false; // 1. anonymous users cannot perform anything
    }

    const isOrganizationOwner = ctx.actor.id === object.organization.owner_id;

    if (isOrganizationOwner) {
      return true; // 2. organization owner can perform any action
    }

    // 3. check if the actor is member of the related organization
    const member = object.organization.members.findById(ctx.actor.id);

    if (!member) {
      return false;
    }

    switch (action) {
      case "create":
        return member.permissions.canCreateDocument;
      case "update":
        return member.permissions.canUpdateDocument;

      // 4. Even combine with the Attribute-Based Access Control (ABAC) model
      case "update:title":
        return member.permissions.canUpdateDocumentTitle;

      default:
        return false;
    }
  }
}

🔑 Authorize users

Encapsulate your authorization logic behind your PunditPolicy implementations. Reuse those policies when you need. Manage your authorization logic from one place.

const currentUser = {}; // get user from cookies, headers, jwt etc...

// update post

// fetch post from db
const post = await prisma.post.findFirst({ where: { id: 123 } });

- if (post.authorId === currentUser.id) {
-   // update logic...
- }
+ if (await pundit.authorize(currentUser, post, 'update')) {
+   // update logic...
+ }

🔎 Filter entitites

Pundit-TS is a ORM-agnostic library. You may use your choice of ORM, query builder or anything.

-prisma.post.findMany({ /* your arguments  */ })
+prisma.post.findMany(pundit.filter(context, Post))

⌨️ Usage

Declare your models.

// models.ts
class User {}

class Post {}

Declare your actions for each model.

// actions.ts
export type UserActions = "create" | "delete" | "update";
export type PostActions = "create" | "delete" | "update";

Declare your policies

// policies.ts
import { PunditPolicy } from "pundit-ts";
import { Post, User } from "./models";
import { PostActions, UserActions } from "./actions";

export class PolicyContext {
  // your orm related properties might go here
}

export class PostPolicy extends PunditPolicy<PolicyContext, Post, PostActions> {
  constructor() {
    super(Post);
  }

  authorize(context, post, action) {
    switch (action) {
      case "create":
        return true;
      case "delete":
        return false; // to be implemented...
      case "update":
        return false; // to be implemented...
    }
  }

  filter(ctx) {
    // modify context
  }
}

export class UserPolicy extends PunditPolicy<PolicyContext, User, UserActions> {
  constructor() {
    super(User);
  }

  authorize(context, post, action) {
    switch (action) {
      case "create":
        return true;
      case "delete":
        return false; // to be implemented...
      case "update":
        return false; // to be implemented...
    }
  }

  filter(ctx) {
    // modify context or return some filter object...
  }
}

Create your Pundit instance:

// pundit.ts
import { Pundit } from "pundit-ts";
import { PostPolicy, UserPolicy } from "./policies";

export const pundit = new Pundit<PolicyContext>()
  .register(new UserPolicy())
  .register(new PostPolicy());

Authorize your actions in a fully type-safe way.

// index.ts
import { PolicyContext } from "./policies";
import { Post } from "./models";
import { pundit } from "./pundit";

const ctx = new PolicyContext();
const post = new Post();

await pundit.authorize(ctx, post, "create");
0.6.0

7 months ago

0.5.0

7 months ago

0.4.2

7 months ago

0.4.1

8 months ago

0.4.0

8 months ago

0.3.0

8 months ago

0.2.8

10 months ago

0.2.7

10 months ago

0.2.5

10 months ago

0.2.4

10 months ago

0.2.3

10 months ago

0.2.2

10 months ago

0.2.1

10 months ago

0.2.0

10 months ago

0.1.0

10 months ago