npm package: @rabstack/rab-access
Description: Access control and authorization utilities for RAB stack
Author: apps
Latest version: 0.2.4

# rab-access

> A powerful TypeScript library for Role-Based Access Control (RBAC) with conditional grants, field-level permissions, and custom validation.

[![npm version](https://img.shields.io/npm/v/rab-access.svg)](https://www.npmjs.com/package/rab-access)
[![License](https://img.shields.io/npm/l/rab-access.svg)](https://github.com/softinhub/softin-sdk/blob/main/LICENSE)

## Features

✨ **Schema-Based Permissions** - Define permissions using declarative schemas
🎯 **Role-Based Access Control** - Grant permissions based on user roles
🔀 **Conditional Grants** - Support for conditional permissions based on field comparisons
📊 **Field-Level Permissions** - Control access to specific fields/columns
🔄 **Permission Inheritance** - Extend permissions from other grants
✅ **Custom Validation** - Support for custom validation functions
🔍 **Advanced Filtering** - Built-in lookup filters and data filtering
🛡️ **Type Safety** - Full TypeScript support with type safety
🚀 **Production Ready** - Battle-tested in production applications

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [Grant Types](#grant-types)
- [API Reference](#api-reference)
- [Integration Examples](#integration-examples)
- [Advanced Usage](#advanced-usage)
- [Examples](#examples)

---

## Installation

```bash
npm install rab-access
```

## Quick Start

Here's the simplest way to use `rab-access` in an Express app:

```typescript
import express from 'express';
import { Rab } from 'rab-access';

const app = express();

// 1. Define permissions
const permissions = Rab.schema({
  canReadProfile: [
    Rab.grant('user').ifEqual(Rab.auth('id'), Rab.params('userId')),
    Rab.grant('admin'),
  ],
  canDeleteUser: [
    Rab.grant('admin'),
  ],
});

// 2. Create middleware
const authorize = (permission: string) => async (req, res, next) => {
  const grant = await permissions.getGrant({
    permission,
    role: req.user.role, // from your auth middleware
    request: { auth: req.user, params: req.params },
  });

  if (!grant.isAuthorized) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  next();
};

// 3. Use it
app.get('/users/:userId', authorize('canReadProfile'), (req, res) => {
  res.json({ message: 'User profile' });
});

app.delete('/users/:userId', authorize('canDeleteUser'), (req, res) => {
  res.json({ message: 'User deleted' });
});

app.listen(3000);
```

**That's it!** This example:
- ✅ Users can only read their own profile
- ✅ Admins can read any profile and delete users
- ✅ Everyone else gets 403 Forbidden

## Core Concepts

### Permission Schema

A permission schema defines the relationship between permissions and roles:

```typescript
const permissions = Rab.schema({
  [AppAccess.canReadUser]: [
    Rab.grant(AppRoles.user).ifEqual(
      Rab.auth('id'),
      Rab.params('userId')
    ),
    Rab.grant(AppRoles.admin),
  ],
});
```

**What this means:**
- `[AppAccess.canReadUser]` - The permission we're defining
- `Rab.grant(AppRoles.user)` - Grant permission to users with role "user"
- `.ifEqual(Rab.auth('id'), Rab.params('userId'))` - BUT only if the user's ID (from token) equals the userId in the URL
- `Rab.grant(AppRoles.admin)` - Also grant to admins (no conditions, they can read any user)

**Example:**
- User with `id: "123"` requests `/users/123` ✅ **Allowed** (their own profile)
- User with `id: "123"` requests `/users/456` ❌ **Forbidden** (different user)
- Admin requests `/users/456` ✅ **Allowed** (admins can read anyone)

### Grant Types

#### Simple Role Grant
```typescript
Rab.grant(AppRoles.admin)
```

#### Conditional Grant
```typescript
Rab.grant(AppRoles.user).ifEqual(
  Rab.auth('organizationId'),
  Rab.params('organizationId')
)
```

#### Grant with Field Restrictions
```typescript
Rab.grant(AppRoles.user)
  .columns(['name', 'email'])
  .ifEqual(Rab.auth('id'), Rab.params('userId'))
```

#### Grant with Filters
```typescript
Rab.grant(AppRoles.manager)
  .filters({ active: true })
  .lookupFilters({
    department: {
      id: Rab.auth('departmentIds')
    }
  })
```

### Field References

Access different parts of the request context:

```typescript
// Reference authenticated user fields (from token/session)
Rab.auth('id')           // user.id
Rab.auth(['profile', 'organizationId'])  // user.profile.organizationId

// Reference request parameters
Rab.params('shopId')     // params.shopId
Rab.params(['nested', 'field'])  // params.nested.field

// Reference query parameters
Rab.query('status')      // query.status
```

## API Reference

### Rab Class

#### `Rab.schema(config)`
Creates a new permission schema.

**Parameters:**
- `config`: Record<string, RabGrant[]> - Permission configuration

**Returns:** `Rab` instance

#### `Rab.grant(role)`
Creates a new grant for a specific role.

**Parameters:**
- `role`: string - The role name

**Returns:** `RabGrant` instance

#### `getGrant(options)`
Evaluates permissions for a specific request.

**Parameters:**
- `options.permission`: string - Permission to check
- `options.role`: string - User's role
- `options.request`: object - Request context (user, params, query)
- `options.validations`: object - Custom validation functions

**Returns:** `Promise<PermissionGrant>`

### RabGrant Class

#### `ifEqual(fieldOne, fieldTwo)`
Adds an equality condition to the grant.

```typescript
Rab.grant('user').ifEqual(
  Rab.auth('organizationId'),
  Rab.params('organizationId')
)
```

#### `ifContains(fieldOne, fieldTwo)`
Adds a contains condition to the grant.

```typescript
Rab.grant('manager').ifContains(
  Rab.auth('departmentIds'),
  Rab.params('departmentId')
)
```

#### `columns(columns)`
Restricts access to specific fields.

```typescript
Rab.grant('user').columns(['name', 'email', 'createdAt'])
```

#### `filters(filters)`
Adds data filtering conditions.

```typescript
Rab.grant('user').filters({ active: true, deleted: false })
```

#### `lookupFilters(filters)`
Adds lookup-based filtering.

```typescript
Rab.grant('manager').lookupFilters({
  department: {
    id: ['departments', 'managedBy']
  }
})
```

#### `validator(method)`
Adds custom validation logic.

```typescript
Rab.grant('user').validator('customValidation')
```

The validation function receives the request object and must return `true` (authorized) or `false` (denied).

#### `extend(role, permission)`
Inherits permissions from another grant.

```typescript
Rab.grant('admin').extend('user', 'canReadProfile')
```

## Integration Examples

### With Express.js

#### Complete Real-World Example

Here's a complete example showing how to integrate `rab-access` with an Express.js application:

```typescript
import express, { Request, Response, NextFunction } from 'express';
import { Rab } from 'rab-access';

// 1. Define your roles and permissions
enum AppRoles {
  user = 'user',
  manager = 'manager',
  admin = 'admin',
  system_admin = 'system_admin',
}

enum AppPermissions {
  canReadUsers = 'canReadUsers',
  canUpdateUser = 'canUpdateUser',
  canDeleteUser = 'canDeleteUser',
  canReadOrders = 'canReadOrders',
  canManageDepartment = 'canManageDepartment',
}

// 2. Create your permission schema
const permissionSchema = Rab.schema({
  // Users can read only their own data
  [AppPermissions.canReadUsers]: [
    Rab.grant(AppRoles.user)
      .columns(['id', 'name', 'email'])
      .ifEqual(Rab.auth('id'), Rab.params('userId')),

    // Managers can read users in their department
    Rab.grant(AppRoles.manager)
      .columns(['id', 'name', 'email', 'department'])
      .ifEqual(Rab.auth('departmentId'), Rab.query('departmentId'))
      .filters({ active: true }),

    // Admins can read all users with all fields
    Rab.grant(AppRoles.admin).columns(['*']),
  ],

  [AppPermissions.canUpdateUser]: [
    // Users can only update their own profile
    Rab.grant(AppRoles.user).ifEqual(
      Rab.auth('id'),
      Rab.params('userId')
    ),

    // Admins can update any user
    Rab.grant(AppRoles.admin),
  ],

  [AppPermissions.canDeleteUser]: [
    Rab.grant(AppRoles.admin).validator('canDeleteUser'),
    Rab.grant(AppRoles.system_admin),
  ],

  [AppPermissions.canReadOrders]: [
    // Users see only their own orders
    Rab.grant(AppRoles.user)
      .filters({ userId: Rab.auth('id') })
      .columns(['id', 'total', 'status', 'createdAt']),

    // Managers see orders from their department
    Rab.grant(AppRoles.manager)
      .lookupFilters({
        user: {
          departmentId: Rab.auth('departmentId'),
        },
      })
      .columns(['*']),

    Rab.grant(AppRoles.admin).columns(['*']),
  ],
});

// 3. Define custom validation functions
const validations = {
  canDeleteUser: async (context: any) => {
    const { user, params } = context.request;

    // Prevent admins from deleting system admins
    const targetUser = await getUserById(params.userId);
    if (targetUser.role === 'system_admin') {
      return false;
    }

    // Only allow deletion of accounts older than 30 days
    const accountAge = Date.now() - targetUser.createdAt;
    const minAge = 30 * 24 * 60 * 60 * 1000;

    return accountAge >= minAge;
  },
};

// 4. Create the middleware factory
function createAuthMiddleware(
  accessControl: Rab,
  validations: Record<string, Function>
) {
  return (permission: string) => {
    return async (req: Request, res: Response, next: NextFunction) => {
      try {
        // Assume user is already authenticated and req.user is set
        if (!req.user) {
          return res.status(401).json({ error: 'Not authenticated' });
        }

        // Get the permission grant
        const grant = await accessControl.getGrant({
          permission,
          role: req.user.role,
          request: {
            auth: req.user, // User object from authentication
            params: req.params, // Route parameters
            query: req.query, // Query parameters
            body: req.body, // Request body
          },
          validations,
        });

        if (!grant.isAuthorized) {
          return res.status(403).json({
            error: 'Forbidden',
            message: 'You do not have permission to perform this action'
          });
        }

        // Attach grant information to request for use in route handlers
        req.grant = grant;
        req.allowedColumns = grant.columns;
        req.filters = grant.filters;
        req.lookupFilters = grant.lookupFilters;

        next();
      } catch (error) {
        console.error('Authorization error:', error);
        return res.status(500).json({
          error: 'Internal server error',
          message: 'Failed to check permissions'
        });
      }
    };
  };
}

// 5. Create the middleware instance
const authorize = createAuthMiddleware(permissionSchema, validations);

// 6. Set up Express app
const app = express();
app.use(express.json());

// Mock authentication middleware (replace with your actual auth)
app.use((req: Request, res: Response, next: NextFunction) => {
  // In production, decode JWT token or check session
  req.user = {
    id: 'user123',
    role: AppRoles.user,
    departmentId: 'dept-1',
    email: 'user@example.com',
  };
  next();
});

// 7. Use the middleware in your routes
app.get(
  '/users/:userId',
  authorize(AppPermissions.canReadUsers),
  async (req: Request, res: Response) => {
    try {
      const user = await getUserById(req.params.userId);

      // Apply column filtering based on permission grant
      if (req.allowedColumns && req.allowedColumns.length > 0) {
        const filteredUser = filterColumns(user, req.allowedColumns);
        return res.json(filteredUser);
      }

      return res.json(user);
    } catch (error) {
      res.status(500).json({ error: 'Failed to fetch user' });
    }
  }
);

app.put(
  '/users/:userId',
  authorize(AppPermissions.canUpdateUser),
  async (req: Request, res: Response) => {
    try {
      const updatedUser = await updateUser(req.params.userId, req.body);
      return res.json(updatedUser);
    } catch (error) {
      res.status(500).json({ error: 'Failed to update user' });
    }
  }
);

app.delete(
  '/users/:userId',
  authorize(AppPermissions.canDeleteUser),
  async (req: Request, res: Response) => {
    try {
      await deleteUser(req.params.userId);
      return res.json({ message: 'User deleted successfully' });
    } catch (error) {
      res.status(500).json({ error: 'Failed to delete user' });
    }
  }
);

app.get(
  '/orders',
  authorize(AppPermissions.canReadOrders),
  async (req: Request, res: Response) => {
    try {
      // Apply filters from permission grant
      const filters = req.filters || {};
      const lookupFilters = req.lookupFilters || {};

      const orders = await getOrders(filters, lookupFilters);

      // Apply column filtering
      if (req.allowedColumns && req.allowedColumns.length > 0) {
        const filteredOrders = orders.map(order =>
          filterColumns(order, req.allowedColumns)
        );
        return res.json(filteredOrders);
      }

      return res.json(orders);
    } catch (error) {
      res.status(500).json({ error: 'Failed to fetch orders' });
    }
  }
);

// Helper functions
function filterColumns(obj: any, allowedColumns: string[]): any {
  if (allowedColumns.includes('*')) return obj;

  const filtered: any = {};
  allowedColumns.forEach(col => {
    if (obj.hasOwnProperty(col)) {
      filtered[col] = obj[col];
    }
  });
  return filtered;
}

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
```

#### Simplified Middleware Example

For a simpler use case:

```typescript
import { Rab } from 'rab-access';
import { Request, Response, NextFunction } from 'express';

const permissions = Rab.schema({
  canReadProfile: [
    Rab.grant('user').ifEqual(Rab.auth('id'), Rab.params('userId')),
    Rab.grant('admin'),
  ],
});

const authorize = (permission: string) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    const grant = await permissions.getGrant({
      permission,
      role: req.user.role,
      request: {
        user: req.user,
        params: req.params,
        query: req.query,
      },
    });

    if (!grant.isAuthorized) {
      return res.status(403).json({ error: 'Forbidden' });
    }

    req.grant = grant;
    next();
  };
};

// Usage
app.get('/users/:userId', authorize('canReadProfile'), async (req, res) => {
  // Your route handler
});
```

#### TypeScript Type Extensions

To get proper TypeScript support, extend the Express Request type:

```typescript
// types/express.d.ts
import { PermissionGrant } from 'rab-access';

declare global {
  namespace Express {
    interface Request {
      user?: {
        id: string;
        role: string;
        departmentId?: string;
        [key: string]: any;
      };
      grant?: PermissionGrant;
      allowedColumns?: string[];
      filters?: Record<string, any>;
      lookupFilters?: Record<string, Record<string, any>>;
    }
  }
}
```

### With AtomAPI Framework

```typescript
import { Rab } from 'rab-access';

// Define permission bloc
@Injectable()
export class UserPermissionBloc implements PermissionAbstractBloc {
  getMetaData() {
    return {
      schema: Rab.schema({
        [AppAccess.canUpdateUser]: [
          Rab.grant(AppRoles.user).ifEqual(
            Rab.auth('id'),
            Rab.params('userId')
          ),
          Rab.grant(AppRoles.admin),
        ],
      }),
      validations: {
        customValidation: async (context) => {
          // Custom validation logic
          return context.user.verified === true;
        },
      },
    };
  }
}

// Use in controller
@Put('/users/:userId', {
  permission: AppAccess.canUpdateUser,
  bodySchema: updateUserSchema,
})
export class UpdateUserController implements AtomApiPut<T> {
  handler: T['request'] = async (request) => {
    // Access the resolved permission grant
    const grant = request.accessGrant;

    // Use grant information for business logic
    if (grant.columns) {
      // Filter response based on allowed columns
    }

    return this.userService.update(request.params.userId, request.body);
  };
}
```

### Custom Validation Functions

```typescript
const userPermissions = Rab.schema({
  [AppAccess.canDeleteUser]: [
    Rab.grant(AppRoles.admin).validator('canDeleteUser'),
  ],
});

// Validation function receives the request object
const validations = {
  canDeleteUser: async (request) => {
    const targetUser = await getUserById(request.params.userId);
    const accountAge = Date.now() - targetUser.createdAt;
    const minAge = 30 * 24 * 60 * 60 * 1000; // 30 days

    // Return true to allow, false to deny
    return accountAge >= minAge;
  },
};
```

## Permission Grant Response

The `getGrant` method returns a `PermissionGrant` object:

```typescript
interface PermissionGrant {
  isAuthorized: boolean;
  columns?: string[];
  filters?: Record<string, any>;
  lookupFilters?: Record<string, Record<string, string[]>>;
  // Additional grant metadata
}
```

## Advanced Usage

### Multi-level Inheritance

```typescript
const permissions = Rab.schema({
  [AppAccess.canReadBasicProfile]: [
    Rab.grant(AppRoles.user).columns(['name', 'email']),
  ],
  [AppAccess.canReadFullProfile]: [
    Rab.grant(AppRoles.admin)
      .extend(AppRoles.user, AppAccess.canReadBasicProfile)
      .columns(['*']),
  ],
});
```

### Complex Conditional Logic

```typescript
const permissions = Rab.schema({
  [AppAccess.canManageProject]: [
    Rab.grant(AppRoles.project_manager)
      .ifEqual(Rab.auth('departmentId'), Rab.params('departmentId'))
      .ifContains(Rab.auth('projectIds'), Rab.params('projectId'))
      .validator('hasActiveSubscription'),
  ],
});
```

## Error Handling

The library provides built-in error handling:

```typescript
try {
  const grant = await permissions.getGrant({
    permission: 'nonexistent',
    role: 'user',
    request: { user: {}, params: {} },
  });
} catch (error) {
  console.error('Permission evaluation failed:', error.message);
  // Grant will return { isAuthorized: false }
}
```

---

## Examples

### Complete Permission System

```typescript
import { Rab } from 'rab-access';

// Define roles and permissions
enum AppRoles {
  user = 'user',
  manager = 'manager',
  admin = 'admin',
  system_admin = 'system_admin',
}

enum AppAccess {
  canReadProfile = 'canReadProfile',
  canUpdateProfile = 'canUpdateProfile',
  canDeleteUser = 'canDeleteUser',
  canManageDepartment = 'canManageDepartment',
}

// Create comprehensive permission schema
const permissions = Rab.schema({
  // Users can read their own profile
  [AppAccess.canReadProfile]: [
    Rab.grant(AppRoles.user)
      .columns(['name', 'email', 'avatar'])
      .ifEqual(Rab.auth('id'), Rab.params('userId')),
    Rab.grant(AppRoles.admin).columns(['*']),
  ],

  // Users can update their own profile
  [AppAccess.canUpdateProfile]: [
    Rab.grant(AppRoles.user).ifEqual(
      Rab.auth('id'),
      Rab.params('userId')
    ),
    Rab.grant(AppRoles.admin),
  ],

  // Only admins can delete users
  [AppAccess.canDeleteUser]: [
    Rab.grant(AppRoles.admin).validator('canDeleteUser'),
    Rab.grant(AppRoles.system_admin),
  ],

  // Managers can manage their own department
  [AppAccess.canManageDepartment]: [
    Rab.grant(AppRoles.manager)
      .ifEqual(Rab.auth('departmentId'), Rab.params('departmentId'))
      .filters({ active: true }),
    Rab.grant(AppRoles.system_admin),
  ],
});

// Validation functions
const validations = {
  canDeleteUser: async (context) => {
    // Admins can only delete users older than 30 days
    const accountAge = Date.now() - context.request.params.createdAt;
    return accountAge > 30 * 24 * 60 * 60 * 1000;
  },
};

// Check permission
const grant = await permissions.getGrant({
  permission: AppAccess.canReadProfile,
  role: AppRoles.user,
  request: {
    user: { id: '123', departmentId: 'dept-1' },
    params: { userId: '123' },
  },
  validations,
});

console.log(grant.isAuthorized); // true
console.log(grant.columns); // ['name', 'email', 'avatar']
```

---

## Best Practices

### 1. Define Clear Role Hierarchies

```typescript
// ✅ Good - Clear role hierarchy
enum AppRoles {
  user = 'user',
  manager = 'manager',
  admin = 'admin',
  system_admin = 'system_admin',
}

// Use inheritance to build on lower roles
Rab.grant(AppRoles.admin).extend(AppRoles.user, AppAccess.canReadProfile)
```

### 2. Use Descriptive Permission Names

```typescript
// ✅ Good - Clear and descriptive
enum AppAccess {
  canCreateProduct = 'canCreateProduct',
  canUpdateOwnProfile = 'canUpdateOwnProfile',
  canApproveShopVerification = 'canApproveShopVerification',
}

// ❌ Avoid - Too generic
enum AppAccess {
  create = 'create',
  update = 'update',
  approve = 'approve',
}
```

### 3. Leverage Field-Level Permissions

```typescript
// ✅ Good - Different columns for different roles
[AppAccess.canReadUser]: [
  Rab.grant(AppRoles.user).columns(['name', 'email']),
  Rab.grant(AppRoles.admin).columns(['*']),
]
```

### 4. Use Validators for Complex Logic

```typescript
// ✅ Good - Complex logic in validator
Rab.grant(AppRoles.user).validator('canPerformAction')

// Validation function handles complexity
const validations = {
  canPerformAction: async (request) => {
    // Hardcode limits or retrieve from config
    const MAX_ATTEMPTS = 3;
    const attempts = await getRecentAttempts(request.user.id);
    return attempts < MAX_ATTEMPTS;
  },
};
```

### 5. Combine Conditions Wisely

```typescript
// ✅ Good - Multiple conditions for fine-grained control
Rab.grant(AppRoles.manager)
  .ifEqual(Rab.auth('departmentId'), Rab.params('departmentId'))
  .ifContains(Rab.auth('teamIds'), Rab.params('teamId'))
  .filters({ active: true, deleted: false })
```

---

## Contributing

Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.

## License

MIT © [Softin Hub](https://github.com/softin)

## Support

- 📧 Email: softin.developer@gmail.com

---

**Made with ❤️ by Softin**