@rabstack/rab-access
v0.2.4
Published
Access control and authorization utilities for RAB stack
Readme
rab-access
A powerful TypeScript library for Role-Based Access Control (RBAC) with conditional grants, field-level permissions, and custom validation.
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
- Quick Start
- Core Concepts
- Grant Types
- API Reference
- Integration Examples
- Advanced Usage
- Examples
Installation
npm install rab-accessQuick Start
Here's the simplest way to use rab-access in an Express app:
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:
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 definingRab.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 URLRab.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
Rab.grant(AppRoles.admin)Conditional Grant
Rab.grant(AppRoles.user).ifEqual(
Rab.auth('organizationId'),
Rab.params('organizationId')
)Grant with Field Restrictions
Rab.grant(AppRoles.user)
.columns(['name', 'email'])
.ifEqual(Rab.auth('id'), Rab.params('userId'))Grant with Filters
Rab.grant(AppRoles.manager)
.filters({ active: true })
.lookupFilters({
department: {
id: Rab.auth('departmentIds')
}
})Field References
Access different parts of the request context:
// 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.statusAPI 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 checkoptions.role: string - User's roleoptions.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.
Rab.grant('user').ifEqual(
Rab.auth('organizationId'),
Rab.params('organizationId')
)ifContains(fieldOne, fieldTwo)
Adds a contains condition to the grant.
Rab.grant('manager').ifContains(
Rab.auth('departmentIds'),
Rab.params('departmentId')
)columns(columns)
Restricts access to specific fields.
Rab.grant('user').columns(['name', 'email', 'createdAt'])filters(filters)
Adds data filtering conditions.
Rab.grant('user').filters({ active: true, deleted: false })lookupFilters(filters)
Adds lookup-based filtering.
Rab.grant('manager').lookupFilters({
department: {
id: ['departments', 'managedBy']
}
})validator(method)
Adds custom validation logic.
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.
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:
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: '[email protected]',
};
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:
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:
// 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
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
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:
interface PermissionGrant {
isAuthorized: boolean;
columns?: string[];
filters?: Record<string, any>;
lookupFilters?: Record<string, Record<string, string[]>>;
// Additional grant metadata
}Advanced Usage
Multi-level Inheritance
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
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:
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
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
// ✅ 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
// ✅ 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
// ✅ 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
// ✅ 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
// ✅ 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 for details.
License
MIT © Softin Hub
Support
- 📧 Email: [email protected]
Made with ❤️ by Softin
