AccessControl

repository·master·Indexed 25 days ago

https://github.com/onury/accesscontrol

A Node.js library implementing Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC). It allows developers to define complex permission policies using roles, resource ownership, attribute filtering with glob notation, and conditional logic via .where() and .require() gates. Version 3.1.0 supports async conditions, policy serialization, and a chainable API for managing grants and inheritance.

Tokens
38.9K
Snippets
130
Records
201
Agent score
80%

What's inside accesscontrol

  1. Overview of AccessControl features

    master

    AccessControl is a Role and Attribute Based Access Control (RBAC + ABAC) engine for Node.js. It provides a chainable API to manage complex permissions through several key mechanisms:

    • RBAC + ABAC: Supports hierarchical role inheritance with a deny-overrides model, combined with attribute-based rules.
    • Ownership & Groups: Use the own permission to verify record ownership (configured via ownerField or owner). Supports granting access to groups and categories for bounded bulk access.
    • Custom Actions & Gates: Extend beyond standard CRUD using .action() or .do(). Implement mandatory require() gates to restrict access.
    • Conditionals & Async Checks: Attach logic via .where() using comparisons, in, cidr, or time windows. Use grantedAsync for custom or asynchronous logic resolution.
    • Security & Reliability: Uses tryCan() to prevent throwing errors, protects against prototype pollution, and provides redacted errors via err.code.
    • Auditing: Emits an access event for every decision (both granted and denied) including a reason, along with change and error hooks.
  2. Scope access using Attribute Glob Notation

    master

    When granting access, you can scope permissions to specific attributes using glob notation. This allows you to control exactly which fields a role can access.

    Supported patterns:

    • *: All attributes.
    • !: Negation (e.g., !password means everything except password).
    • Nested paths: Use dotted notation (e.g., profile.*) to target nested attributes.

    Important Behaviors:

    • Implicit All: If you provide a list containing only negations (e.g., ['!password']), it is treated as ['*', '!password'] (everything except password).
    • Defaulting: Omitting the attribute array defaults to ['*'].
    • Empty Array: Providing an explicit empty array [] allows no attributes, resulting in granted: false.
    ac.grant('user').readOwn('account', [
      '*',          // all attributes…
      '!password',  // …except password
      'profile.*'   // (nested paths are supported)
    ]);
  3. Determine when to use policy vs context

    master

    When writing conditions, follow this rule of thumb to decide where data belongs:

    • context: Use this for data that the condition reads using the $. prefix (e.g., data belonging to the specific record being checked).
    • policy: Use this for data that the AccessControl engine reads to decide its internal behavior or logic.
  4. Use Deny-overrides for Inheritance Control

    master

    In v3, grants are purely additive. A child grant cannot 'shrink' an inherited grant by simply defining fewer attributes. To remove access that was inherited, you must use an explicit deny rule.

    Note: deny does not cascade across possession. For example, deny create:any will not affect create:own permissions.

    ac.grant('user').readAny('post', ['*']);
    ac.grant('moderator').extend('user');
    ac.deny('moderator').readAny('post', ['secret']);   // carve a field back
    
    ac.can('moderator').readAny('post').attributes;     // ['*', '!secret']
  5. Use Conditions (ABAC) to restrict grants

    master

    A condition determines whether a grant applies at check time by evaluating a context object. You attach conditions to grants using the .where() method. This implements Attribute-Based Access Control (ABAC) by comparing data provided during the access check against the policy requirements.

    ac.grant('manager')
      .where('$.order.value <= 100000')
      .updateAny('order', ['*']);
    
    ac.can('manager')
      .with({ order: { value: 5000 } })
      .updateAny('order').granted; // true
    
    ac.can('manager')
      .with({ order: { value: 250000 } })
      .updateAny('order').granted; // false
  6. Protect against Prototype Pollution

    master

    AccessControl includes built-in protections against prototype pollution and inherited key attacks:

    1. Reserved Names: The names __proto__, prototype, and constructor are rejected during validation with err.code === 'RESERVED_NAME'. Attempting to grant these will throw an error.
    2. Inherited Keys: Names that collide with built-in object properties (like toString or hasOwnProperty) are treated as plain data. The library uses Object.hasOwn for all internal lookups, so these names will simply return granted: false rather than accessing or mutating the prototype.
    3. JSON/DB Imports: If you import a grants object from an external source, any __proto__ keys are rejected rather than merged.
    ac.can('user').readAny('toString').granted; // false (never throws)
    ac.grant('__proto__');                       // throws RESERVED_NAME
  7. Manage Roles and Inheritance

    master

    Create roles using .grant(role) or .deny(role). Roles can inherit permissions from other roles using .extend(). Grants are additive, but an explicit deny always takes precedence over inherited grants. Note that deny does not cascade across possession (e.g., a deny create:any does not automatically deny create:own).

    ac.grant('user').readAny('post', ['*']);
    ac.grant('moderator').extend('user');
    ac.deny('moderator').readAny('post', ['secret']);   // carve a field back
    
    ac.can('moderator').readAny('post').attributes;     // ['*', '!secret']
  8. Register custom functions for serializable models

    master

    Because AccessControl stores only the name + args of a custom function in a grant, your permission model remains JSON/DB-serializable.

    When loading a model from a database or JSON file, you must re-register the corresponding functions using defineCondition() on the new instance. If you attempt to use a function name that has not been registered, the check will fail closed with err.code === 'UNKNOWN_CONDITION_FN'.

  9. Organize resources with Categories

    master
    You can group related resources using a category separator (/). For example, media/photo and media/video belong to the media category. This allows you to apply grants or gates to an entire category of resources. Categories also prevent name collisions between different domains, such as media/photo and legal/photo being treated as distinct.
  10. Understand model immutability and locking

    master

    AccessControl protects the integrity of the authorization model through several mechanisms:

    • Detached Copies: Methods like getGrants(), getGrantsList(), and getRequirements() return deep copies. Mutating these results will not affect the live AccessControl instance.
    • Model Locking: You can call lock() to deep-freeze the model. Once locked, any attempt to use mutator methods will throw an error with err.code === 'LOCKED'.
    • Frozen Attributes: Permission.attributes and .roles are also returned as frozen copies.