Skip to content

Organize your authorization logic with full type-safety.

License

Notifications You must be signed in to change notification settings

fatihky/pundit-ts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

33 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Pundit-TS - Plain Typescript Authorization Library

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

Pundit-TS is a fully type-safe `pundit` alternative.
Here how it auto-completes the actions based on the object you pass:
pundit-ts_demo.mov

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..

Role-Based Access Control (RBAC)
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
    }
  }
}
Attribute-Based Access Control (ABAC)
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;
    }
  }
}
Discretionary Access Control (DAC)

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");

About

Organize your authorization logic with full type-safety.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published