Skip to content

stroncium/arbac-ts

Repository files navigation

npm install arbac

Zero-dependency type-safe Attribute enhanced Role-Based Access Control for typescript.

Features

  • Simple centralized access control declaration, role lists can be supplied from any source
  • Attributes can be supplied in (almost) any form and from any source, type-safe and extendable
  • Synchronous and performant
  • Can be used with any runtime

Example usage

import { ARBAC, ARBACError } from 'arbac';

enum BlogRole {
	Administrator = 'admin',
	Reader = 'reader',
	Writer = 'writer',
}

enum BlogAction {
	CreatePost,
	ReadPost,
	UpdatePost,
	DeletePost,
}

type UserID = string;

const USER1: UserID = 'user1';
const USER2: UserID = 'user2';

interface BlogActionAttributes {
	[BlogAction.DeletePost]: {
		authorId: UserID;
		actorId: UserID;
	};
	[BlogAction.UpdatePost]: {
		authorId: UserID;
		actorId: UserID;
	};
}

const actorIsAuthor = (
	attrs: BlogActionAttributes[BlogAction.UpdatePost | BlogAction.DeletePost],
) => attrs.authorId === attrs.actorId;

const rbac = ARBAC.fromObjects<BlogRole, BlogAction, BlogActionAttributes>(
	{
		[BlogRole.Reader]: {
			[BlogAction.ReadPost]: true,
		},
		[BlogRole.Writer]: {
			// special property to extend other roles, local properties override inherited ones
			[ARBAC.EXTENDS_ROLE]: BlogRole.Reader,
			[BlogAction.CreatePost]: true,
			[BlogAction.UpdatePost]: actorIsAuthor,
			[BlogAction.DeletePost]: actorIsAuthor,
		},
		[BlogRole.Administrator]: {
			[BlogAction.ReadPost]: true,
			[BlogAction.DeletePost]: true,
		},
	},
	{
		// list of actions should be passed if they have number values, can be detected otherwise
		actions: [
			BlogAction.CreatePost,
			BlogAction.ReadPost,
			BlogAction.UpdatePost,
			BlogAction.DeletePost,
		],
		// hideAttributesInErrors: false, // attributes aren't passed to errors on asserts by default to avoid leaks
		// makeError: (action, roles, attrs) => new Error('custom'), // generate custom errors on asserts
		// roles: [...], // list of roles should be passed if they have number values, can be detected otherwise
	},
);

expect(rbac.permits(BlogAction.ReadPost, [BlogRole.Reader])).toBe(true);

// first level arrays are flattened, roles are deduplicated, undefined values ignored
expect(
	rbac.permits(BlogAction.ReadPost, [
		BlogRole.Reader,
		[BlogRole.Reader, undefined],
		undefined,
	]),
).toBe(true);

expect(rbac.actionRelevantRoles(BlogAction.DeletePost).all).toEqual([
	BlogRole.Administrator,
	BlogRole.Writer,
]); // finding all roles that can interact with particular action

rbac.assertPermits(BlogAction.CreatePost, [
	BlogRole.Reader,
	BlogRole.Writer,
	BlogRole.Administrator,
]);

rbac.assertPermits(BlogAction.DeletePost, [BlogRole.Administrator], {
	authorId: USER1,
	actorId: USER2,
}); // types won't allow you to skip attributes here

expect(() =>
	rbac.assertPermits(BlogAction.CreatePost, [BlogRole.Reader]),
).toThrow(ARBACError); // role check won't permit this

expect(() =>
	rbac.assertPermits(
		BlogAction.UpdatePost,
		[BlogRole.Reader, BlogRole.Writer],
		{
			authorId: USER1,
			actorId: USER2,
		},
	),
).toThrow(ARBACError); // attribute check won't permit this

More examples at src/examples.test.ts

About

Zero-dependency type-safe Attribute enhanced Role-Based Access Control for typescript.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published