https://github.com/fatihky/pundit-ts
Organize your authorization logic with full type-safety.
https://github.com/fatihky/pundit-ts
authorization
Last synced: 2 months ago
JSON representation
Organize your authorization logic with full type-safety.
- Host: GitHub
- URL: https://github.com/fatihky/pundit-ts
- Owner: fatihky
- License: mit
- Created: 2024-12-11T11:45:23.000Z (4 months ago)
- Default Branch: master
- Last Pushed: 2025-02-08T11:19:46.000Z (2 months ago)
- Last Synced: 2025-02-08T12:23:08.604Z (2 months ago)
- Topics: authorization
- Language: TypeScript
- Homepage:
- Size: 102 KB
- Stars: 5
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
- awesome-auth - pundit-ts - Fully type-safe authorization library inspired by awesome [pundit](https://github.com/varvet/pundit) gem. Can be used for RBAC, ABAC access control models or any other model you wish. (Authorization / <a name="authZ-node"></a>Node.js)
README
# Pundit-TS - Plain Typescript Authorization Library
Pundit-TS is an authorization library highly-inspired by the [pundit](https://github.com/varvet/pundit) gem.
Pundit-TS is a fully type-safe `pundit` alternative.
Here how it auto-completes the actions based on the object you pass: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
```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..
Role-Based Access Control (RBAC)
```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
}
}
}
```Attribute-Based Access Control (ABAC)
```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 contentdefault:
return false;
}
}
}
```Discretionary Access Control (DAC)
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;
}
}
}
```### 🔑 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 {
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 {
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()
.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");
```