1.0.16 • Published 8 months ago

permissionless.js v1.0.16

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

🪪 Permissionless.js: Role-Based Access Control for Node.js Projects

NPM Downloads NPM Collaborators Static Badge NPM Type Definitions Codacy Badge

Permissionless is a powerful and extensible TypeScript library designed to manage user roles and permissions in your application. It offers features such as role inheritance, wildcard permissions, contextual checks, and dynamic configuration updates. This was first created in-house for JMServices Pro, and made open source.

Key Features

  • 👑 Role Inheritance: Define hierarchical roles where permissions are inherited from parent roles.
  • 👤 User-Specific Overrides: Grant or deny specific permissions to individual users.
  • 🔍 Contextual Permissions: Check permissions with added context (e.g., read:articles).
  • 🌟 Wildcard Matching: Use wildcards for permissions like read:* or write:docs.*.
  • Dynamic Configuration: Automatically reload changes to .permissionless.json or fetch from APIs.
  • 🚀 Caching for Performance: Built-in caching to optimize repeated permission checks.
  • 🏃 Runtime Compatibility: Works seamlessly with Bun, Deno & Node.js for maximum flexibility across different JavaScript runtimes.
  • 🔥 Firestore Integration (Beta): Seamlessly load and manage permissions through Firebase Firestore, with more cloud functions being actively developed.

Installation

Install the package using npm or yarn:

npm install permissionless.js
yarn add permissionless.js

Getting Started

1. Create a Configuration File

Define your roles, permissions, and user-specific overrides in .permissionless.json:

{
  "roles": {
    "admin": {
      "permissions": ["read:*", "write:*", "delete:*"],
      "inherits": ["editor"]
    },
    "editor": {
      "permissions": ["read:articles", "write:articles"]
    },
    "viewer": {
      "permissions": ["read:articles"]
    }
  },
  "users": {
    "123": {
      "permissions": ["read:restricted-docs"],
      "denies": ["write:articles"]
    }
  }
}

2. Initialize Permissionless

Import and initialize the Permissionless class in your application:

// CommonJS
const { Permissionless } = require('permissionless.js');
const permissions = new Permissionless();

// OR using ES Modules
import { Permissionless } from 'permissionless.js';
const permissions = new Permissionless();

3. Check Permissions

To check if a user has a specific permission, use the hasPermission method:

const user = { id: '123', role: 'editor', ...restOfUser };

if (permissions.hasPermission(user, 'read', 'articles')) {
  console.log('User has permission');
} else {
  console.error('User does NOT have permission');
}

Advanced Features

Role Inheritance

Define roles with inherited permissions. In the example above, admin inherits all permissions from editor. You can specify multiple roles in the inherits array.

User-Specific Overrides

Grant or deny specific permissions to individual users. For example:

"users": {
  "123": {
    "permissions": ["read:restricted-docs"], // Extra permissions for this user
    "denies": ["write:articles"]            // Explicitly deny writing articles
  }
}

Wildcard Permissions 🪪

Define generic permissions using *. For instance:

  • read:* grants read access to all resources.
  • write:articles.* grants write access to all sub-resources of articles.

Contextual Permissions

Add context to permissions checks. Example:

permissions.hasPermission(user, 'read', 'articles');
permissions.hasPermission(user, 'read', 'restricted-docs');

Dynamic Configuration Reload

Permissionless watches for changes in .permissionless.json and reloads the configuration automatically. You can also fetch the configuration from an API:

await permissions.loadConfigFromApi('https://example.com/permissions.json');

Add or Modify Roles Programmatically

You can add or modify roles and permissions dynamically:

permissions.addRole('superadmin', ['manage:users'], ['admin']);
permissions.addPermissionToRole('editor', 'edit:comments');

API Reference

constructor(configFilePath?: string)

  • Description: Initializes Permissionless with the given configuration file.
  • Default: .permissionless.json

hasPermission(user: User, permission: string, context?: string): boolean

  • Description: Checks if a user has the specified permission, optionally within a context.

getPermissionsForRole(role: string): string[]

  • Description: Retrieves all permissions for a specified role, including inherited ones.

addRole(roleName: string, permissions: string[], inherits?: string[]): void

  • Description: Adds a new role with optional inheritance from other roles.

addPermissionToRole(roleName: string, permission: string): void

  • Description: Adds a new permission to an existing role.

clearCache(): void

  • Description: Clears the internal cache of permissions.

loadConfigFromApi(apiUrl: string): Promise<void>

  • Description: Loads configuration from an external API URL.

Utility Methods

listRoles(): string[]

  • Description: Returns a list of all role names defined in the configuration.

listUsers(): string[]

  • Description: Returns a list of all user IDs defined in the configuration.

hasRole(roleName: string): boolean

  • Description: Checks if a role exists in the configuration.

checkMultiplePermissions(user: User, permissions: string[], context?: string): boolean

  • Description: Checks if a user has ALL of the specified permissions. Optionally checks within a context.

checkAnyPermission(user: User, permissions: string[], context?: string): boolean

  • Description: Checks if a user has ANY of the specified permissions. Optionally checks within a context.

Example Usage

Basic Permission Check

const user = { id: '456', role: 'viewer', ...restOfUserConfig };

if (permissions.hasPermission(user, 'read', 'articles')) {
  console.log('Viewer can read articles');
} else {
  console.error('Viewer cannot read articles');
}

Adding a New Role

permissions.addRole('moderator', ['moderate:comments'], ['viewer']);

Dynamic Permissions Update

permissions.addPermissionToRole('viewer', 'read:docs');

Advanced Wildcard and Contextual Matching

const user = { id: '789', role: 'editor' };

if (permissions.hasPermission(user, 'read', 'articles.section1')) {
  console.log('Editor can read section 1 of articles');
}

Configuration Reload

File Watcher

Permissionless automatically reloads .permissionless.json when changes are detected. No additional setup is needed for this feature.

Load Config from API

You can dynamically load configuration from an external API:

await permissions.loadConfigFromApi('https://example.com/permissions');

Performance Optimizations

  • Caching: Permissionless uses in-memory caching for permission calculations. Clear the cache when roles or permissions are updated:

    permissions.clearCache();
  • Lazy Loading: Role permissions are calculated and cached only when accessed.


Error Handling

  • Role Not Found: If a role is missing in the configuration:

    Error: Role [roleName] not found
  • Circular Inheritance: If roles inherit in a loop:

    Error: Circular inheritance detected in role: [roleName]
  • Invalid User Role: If a user has an undefined role:

    Error: Role [user.role] not found

🔥 Firebase Documentation (Beta)

Load config from Firestore Database

loadConfigFromFirestore(collection?: string, documentId?: string): Promise<void>

  • Description: Loads permission configuration from Firebase Firestore. This allows dynamic loading of permissions from a Firestore database.
  • Parameters:
    • collection (optional): The Firestore collection name where the config is stored
      • Default: 'permissionlessConfig'
    • documentId (optional): The document ID containing the config
      • Default: 'config'
  • Returns: Promise that resolves when configuration is loaded
  • Throws: PermissionlessError if:
    • Firestore instance is not initialized
    • Document doesn't exist in collection
    • Configuration validation fails
    • Firestore request fails

Example Usage:

const firestore = new Firestore();
const permissions = new Permissionless();

// Using default collection and document
await permissions.loadConfigFromFirestore();

// Using custom collection and document
await permissions.loadConfigFromFirestore('customCollection', 'customConfig');

Expected Firestore Document Structure:

{
  "roles": {
    "admin": {
      "permissions": ["*"],
      "inherits": []
    },
    "editor": {
      "permissions": ["read:*", "write:articles"],
      "inherits": ["viewer"]
    }
  },
  "users": {
    "user123": {
      "permissions": ["special:access"],
      "denies": ["write:comments"]
    }
  }
}

License

Permissionless is distributed under the MIT License.


Contribution

Contributions are welcome! Feel free to open issues or submit pull requests to improve Permissionless.

1.0.16

8 months ago

1.0.15

8 months ago

1.0.14

8 months ago

1.0.13

8 months ago

1.0.12

8 months ago

1.0.11

8 months ago

1.0.10

8 months ago

1.0.9

8 months ago

1.0.8

8 months ago

1.0.7

8 months ago

1.0.6

8 months ago

1.0.5

8 months ago

1.0.4

8 months ago

1.0.3

8 months ago

1.0.1

8 months ago

1.0.0

8 months ago