Access Control Core Concepts | Dashforge-UI
DocsStarter Kits
v0.1.0-alpha

Core Concepts

Understanding the fundamental building blocks of Role-Based Access Control in Dashforge.

Subjects

The actors requesting access to resources

A subject represents the user or entity requesting access. Every subject has an ID, a list of assigned roles, and optional attributes for advanced permission logic.

import type { Subject } from '@dashforge/rbac';

const currentUser: Subject = {
  id: 'user-123',
  roles: ['editor', 'finance-reviewer'],
  attributes: {
    department: 'sales',
    region: 'us-west',
    employeeLevel: 3
  }
};

The roles array determines which permissions apply to the subject. The attributes object stores additional context used in conditional permissions (e.g., checking if a user owns a resource or belongs to a specific department).

ℹ️

Best practice: Keep attributes minimal and focused on access control decisions. Avoid storing full user profiles in the subject.


Permissions

Defining what can be done to what resources

A permission is a rule that defines an action that can be performed on a resource. Actions and resources are strings you define based on your domain model.

import type { Permission } from '@dashforge/rbac';

const readBookingPermission: Permission = {
  action: 'read',
  resource: 'booking'
};

const deleteBookingPermission: Permission = {
  action: 'delete',
  resource: 'booking'
};

const editCustomerPermission: Permission = {
  action: 'edit',
  resource: 'customer'
};

Permissions use a simple action-resource pattern. Choose action names that match your business operations: read, edit, delete, approve, export, etc.

Permission Effects

Permissions can have an effect of either allow (grant access) or deny (explicitly block access). If omitted, the effect defaults to allow.

const permissions: Permission[] = [
  // Allow access (default effect)
  {
    action: 'read',
    resource: 'booking'
  },
  
  // Explicitly deny access (blocks even if other roles allow)
  {
    action: 'delete',
    resource: 'booking',
    effect: 'deny'
  }
];

⚠️

Deny takes precedence: If any permission with effect: 'deny' matches a request, access is denied regardless of other allow permissions. See Effect Precedence below.


Roles

Grouping permissions and role inheritance

A role is a named collection of permissions. Roles let you assign multiple permissions to users as a group, rather than assigning permissions individually.

import type { Role } from '@dashforge/rbac';

const editorRole: Role = {
  name: 'editor',
  permissions: [
    { action: 'read', resource: 'booking' },
    { action: 'edit', resource: 'booking' },
    { action: 'read', resource: 'customer' },
    { action: 'edit', resource: 'customer' }
  ]
};

const viewerRole: Role = {
  name: 'viewer',
  permissions: [
    { action: 'read', resource: 'booking' },
    { action: 'read', resource: 'customer' }
  ]
};
Role Inheritance

Roles can inherit permissions from other roles using the inherits property. This creates a hierarchy where higher-level roles automatically include all permissions from lower-level roles.

const roles: Role[] = [
  // Base role with read-only access
  {
    name: 'viewer',
    permissions: [
      { action: 'read', resource: 'booking' },
      { action: 'read', resource: 'customer' }
    ]
  },
  
  // Editor inherits viewer permissions + adds edit permissions
  {
    name: 'editor',
    inherits: ['viewer'],
    permissions: [
      { action: 'edit', resource: 'booking' },
      { action: 'edit', resource: 'customer' }
    ]
  },
  
  // Admin inherits editor permissions + adds delete permissions
  {
    name: 'admin',
    inherits: ['editor'],
    permissions: [
      { action: 'delete', resource: 'booking' },
      { action: 'delete', resource: 'customer' },
      { action: 'manage', resource: 'users' }
    ]
  }
];

// Result: admin can read, edit, and delete (inherited from viewer + editor + own permissions)

When a role inherits from another role, it gains all permissions from the parent role recursively. You can inherit from multiple roles by providing an array of role names.

ℹ️

Inheritance is resolved once when the policy is loaded. Circular inheritance is detected and throws an error at engine creation time.


Policies

The complete RBAC configuration

A policy is the top-level RBAC configuration that contains all roles for your application. It's passed to RbacProvider to configure the access control system.

import type { RbacPolicy } from '@dashforge/rbac';

const policy: RbacPolicy = {
  roles: [
    {
      name: 'viewer',
      permissions: [
        { action: 'read', resource: 'booking' },
        { action: 'read', resource: 'customer' }
      ]
    },
    {
      name: 'editor',
      inherits: ['viewer'],
      permissions: [
        { action: 'edit', resource: 'booking' },
        { action: 'edit', resource: 'customer' }
      ]
    },
    {
      name: 'admin',
      inherits: ['editor'],
      permissions: [
        { action: 'delete', resource: 'booking' },
        { action: 'delete', resource: 'customer' },
        { action: 'manage', resource: 'users' }
      ]
    }
  ]
};

Policies are typically defined once at application startup and remain constant throughout the session. Dynamic policy changes require creating a new RBAC engine instance.

Tip: Store policies in a separate file (e.g., rbac-policy.ts) to keep them maintainable and reviewable by non-developers (security teams, product managers).


Conditions

Dynamic permission evaluation based on runtime context

Permissions can include a condition function that evaluates whether the permission applies based on runtime context. Conditions enable dynamic access control like "users can only edit their own bookings" or "managers can approve within their department."

import type { Permission, ConditionContext } from '@dashforge/rbac';

const permissions: Permission[] = [
  // Allow all editors to read any booking
  {
    action: 'read',
    resource: 'booking'
  },
  
  // Allow editing only if user owns the booking
  {
    action: 'edit',
    resource: 'booking',
    condition: (context: ConditionContext) => {
      const booking = context.resourceData as { ownerId: string };
      return booking?.ownerId === context.subject.id;
    }
  },
  
  // Allow deletion only for same department
  {
    action: 'delete',
    resource: 'booking',
    condition: (context: ConditionContext) => {
      const booking = context.resourceData as { department: string };
      const userDept = context.subject.attributes?.department as string;
      return booking?.department === userDept;
    }
  }
];
Condition Context

Condition functions receive a ConditionContext object containing:

  • subject - The current user (with id, roles, attributes)
  • resourceData - Optional data about the specific resource being accessed (e.g., booking object)
  • environment - Optional environment context (e.g., current time, request IP)
// Example: Allow access only during business hours
{
  action: 'approve',
  resource: 'expense',
  condition: (context: ConditionContext) => {
    const now = context.environment?.currentTime as Date || new Date();
    const hour = now.getHours();
    return hour >= 9 && hour < 17; // 9 AM to 5 PM
  }
}

⚠️

Condition requirements: Condition functions must be synchronous and return a boolean. If a condition throws an error or returns a non-boolean value, it is treated as false (access denied).


Allow vs Deny Precedence

Understanding how permission conflicts are resolved

When a subject has multiple roles with conflicting permissions, the RBAC engine follows a clear precedence rule: deny always wins.

Precedence Rule:

If any permission with effect: 'deny' matches the access request, access is denied — even if other permissions would allow it.

const policy: RbacPolicy = {
  roles: [
    {
      name: 'editor',
      permissions: [
        { action: 'edit', resource: 'booking' } // allow (default)
      ]
    },
    {
      name: 'restricted',
      permissions: [
        { action: 'edit', resource: 'booking', effect: 'deny' } // explicit deny
      ]
    }
  ]
};

// Subject with both roles
const subject: Subject = {
  id: 'user-123',
  roles: ['editor', 'restricted']
};

// Request: edit booking
const request: AccessRequest = {
  action: 'edit',
  resource: 'booking'
};

// Result: DENIED (deny from 'restricted' role takes precedence over allow from 'editor' role)

This precedence rule ensures that deny permissions act as absolute blocks, useful for temporarily revoking access or implementing blacklists.

ℹ️

Default behavior: If no permissions match the request at all, access is denied by default. You must explicitly grant permissions using effect: 'allow' (or omit the effect).


Wildcard Support

Using * for flexible action and resource matching

The RBAC system supports * (asterisk) wildcards in both actions and resources to grant broad permissions without listing every individual action or resource.

const adminRole: Role = {
  name: 'admin',
  permissions: [
    // Allow all actions on all resources
    { action: '*', resource: '*' },
    
    // Allow all actions on bookings specifically
    { action: '*', resource: 'booking' },
    
    // Allow reading all resources
    { action: 'read', resource: '*' },
    
    // Wildcard works with deny too
    { action: 'delete', resource: '*', effect: 'deny' } // deny all deletes
  ]
};
Wildcard Matching Rules
  • action: '*' matches any action (read, edit, delete, approve, etc.)
  • resource: '*' matches any resource (booking, customer, user, etc.)
  • { action: '*', resource: '*' } grants unlimited access (use with caution)
  • Wildcards are exact: * only, no partial matches like read-*

⚠️

Security note: Wildcard permissions like { action: '*', resource: '*' } grant unrestricted access. Use them sparingly and only for superadmin roles. Prefer explicit permissions when possible.


Best Practices

Recommendations for effective RBAC design

Follow these guidelines to build maintainable and secure RBAC policies:

Use Domain-Specific Names

Name actions and resources based on your business domain, not technical implementation. Use approve-expense not post-api-expense-approve.

Start with Roles, Not Permissions

Design roles around job functions (editor, approver, viewer) rather than individual permissions. Assign users to roles, not individual permissions.

Leverage Role Inheritance

Build role hierarchies with inherits. Start with minimal base roles (viewer) and extend upward (editor, admin). Reduces duplication.

Use Conditions Sparingly

Conditions add complexity. Use them only when permissions depend on resource ownership or runtime context. Prefer static permissions when possible.

Default Deny, Explicit Allow

The RBAC engine denies access by default. Only explicitly granted permissions are allowed. This "fail-secure" approach prevents accidental privilege escalation.

Test Your Policy

Write unit tests for critical permissions using createRbacEngine. Test that admins can delete, viewers cannot edit, etc. Catch policy bugs early.

Next steps: Learn how to use the RBAC engine in React applications with React Integration, or jump to Interactive Playground to experiment with roles and permissions.

On This Page