UNPKG

pundit-ts

Version:

Authorization library in pure typescript.

280 lines (212 loc) 7.02 kB
# Pundit-TS - Plain Typescript Authorization Library Pundit-TS is an authorization library highly-inspired by the [pundit](https://github.com/varvet/pundit) gem. <small> Pundit-TS is a fully type-safe `pundit` alternative.<br/> Here how it auto-completes the actions based on the object you pass: </small> https://github.com/user-attachments/assets/c7815622-c1c9-4fbe-986e-6c9f88c8b31d <br/> ### 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 ```sh 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.. <details> <summary>Role-Based Access Control (RBAC)</summary> ```ts 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 } } } ``` </details> <details> <summary>Attribute-Based Access Control (ABAC)</summary> ```ts 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; } } } ``` </details> <details> <summary>Discretionary Access Control (DAC)</summary> Great choice for multi-tenant applications (ie. SaaS applications). Here we use `organization` as our tenant. ```ts 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; } } } ``` </details> ### 🔑 Authorize users Encapsulate your authorization logic behind your `PunditPolicy` implementations. Reuse those policies when you need. Manage your authorization logic from one place. ```diff 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. ```diff -prisma.post.findMany({ /* your arguments */ }) +prisma.post.findMany(pundit.filter(context, Post)) ``` ### ⌨️ Usage Declare your models. ```typescript // models.ts class User {} class Post {} ``` Declare your actions for each model. ```typescript // actions.ts export type UserActions = "create" | "delete" | "update"; export type PostActions = "create" | "delete" | "update"; ``` Declare your policies ```typescript // 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: ```typescript // 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. ```typescript // 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"); ```