Usage Guide A comprehensive guide to using ExpressRoleManager for role-based access control in Express.js applications.
The ExpressRoleManager
is a powerful tool for implementing role-based access control (RBAC) in Express.js applications. It integrates seamlessly with the @ iamjs / core
package to provide robust authorization capabilities. This guide will walk you through setting up, configuring, and using the ExpressRoleManager
in various scenarios.
To get started with ExpressRoleManager
, you need to set up your roles, create a schema, and initialize the manager.
import express from ' express ' ;
import { Role, Schema } from ' @iamjs/core ' ;
import { ExpressRoleManager } from ' @iamjs/express ' ;
// Define roles
const userRole = new Role ( {
name : ' user ' ,
config : {
posts : {
base : ' cr--l ' ,
custom : {
like : true ,
comment : true
}
},
profile : {
base : ' crud- ' ,
custom : {
changePassword : true
}
}
}
} );
const adminRole = new Role ( {
name : ' admin ' ,
config : {
posts : {
base : ' crudl ' ,
custom : {
pin : true ,
delete : true
}
},
users : {
base : ' crudl ' ,
custom : {
ban : true
}
}
}
} );
// Create schema
const schema = new Schema ( {
roles : { user : userRole , admin : adminRole }
} );
// Initialize ExpressRoleManager
const roleManager = new ExpressRoleManager ( {
schema ,
onError : ( err , req , res , next ) => {
res . status ( 403 ) . json ( { error : ' Access denied ' } ) ;
},
onSuccess : ( req , res , next ) => {
next () ;
}
} );
const app = express ();
// Use the role manager in a route
app . get ( ' /posts ' ,
roleManager . check ({
resources: ' posts ' ,
actions: [ ' read ' , ' list ' ],
role: ' user '
}),
( req , res ) => {
res . json ({ posts: [ ' Post 1 ' , ' Post 2 ' ] });
}
);
app . listen ( 3000 , () => console . log ( ' Server running on port 3000 ' ));
This basic setup demonstrates how to define roles, create a schema, initialize the ExpressRoleManager
, and use it in a route.
For more complex scenarios, you might need to dynamically determine the role or perform additional checks.
import express from ' express ' ;
import { Role, Schema } from ' @iamjs/core ' ;
import { ExpressRoleManager } from ' @iamjs/express ' ;
// ... (previous role and schema setup)
const app = express ();
// Middleware to simulate user authentication
const auth = async ( req , res , next ) => {
// In a real app, you'd verify a token or session here
req . user = { id : 1 , role : ' user ' };
next () ;
} ;
// Middleware to fetch user permissions
const fetchUserPermissions = async ( req , res , next ) => {
// In a real app, you might fetch this from a database
req . permissions = userRole . toObject () ;
next () ;
} ;
const roleManager = new ExpressRoleManager ( {
schema ,
onError : ( err , req , res , next ) => {
res . status ( 403 ) . json ( { error : ' Access denied ' } ) ;
},
onSuccess : ( req , res , next ) => {
next () ;
}
} );
app . post ( ' /posts ' ,
auth,
fetchUserPermissions,
roleManager . check ({
resources: ' posts ' ,
actions: [ ' create ' ],
strict: true ,
construct: true ,
data : async ( req ) => req . permissions
}),
( req , res ) => {
res . json ({ message: ' Post created successfully ' });
}
);
app . delete ( ' /posts/:id ' ,
auth,
fetchUserPermissions,
roleManager . check ({
resources: ' posts ' ,
actions: [ ' delete ' ],
strict: true ,
construct: true ,
data : async ( req ) => req . permissions
}),
( req , res ) => {
res . json ({ message: ' Post deleted successfully ' });
}
);
app . listen ( 3000 , () => console . log ( ' Server running on port 3000 ' ));
This advanced example shows how to use middleware for authentication and fetching user permissions, and how to use the construct
and data
options for dynamic role checking.
Customizing success and error handling allows you to tailor the behavior of your application based on authorization results.
const roleManager = new ExpressRoleManager ( {
schema ,
onError : ( err , req , res , next ) => {
console . error ( ' Authorization error: ' , err ) ;
res . status ( 403 ) . json ( {
error : ' Access denied ' ,
details : err . message
} ) ;
},
onSuccess : ( req , res , next ) => {
console . log ( ' Authorization successful ' ) ;
// You can add custom headers or modify the request object here
req . authorized = true ;
next () ;
}
} );
The onActivity
handler allows you to log or process user activities, which can be useful for auditing or analytics.
const roleManager = new ExpressRoleManager ( {
schema ,
onError : ( err , req , res , next ) => { /* ... */ },
onSuccess : ( req , res , next ) => { /* ... */ },
async onActivity ( data ) {
console . log ( ' User activity: ' , {
timestamp : new Date () ,
userId : data . req . user . id ,
role : data . role ,
resources : data . resources ,
actions : data . actions ,
success : data . success
} ) ;
// In a real app, you might save this to a database or send to a logging service
}
} );
ExpressRoleManager
provides full TypeScript support, allowing you to specify types for request and response objects. You can also extend the Express Request type globally for a more streamlined development experience.
You can create custom interfaces that extend the Express Request type for local use:
import { Request, Response, NextFunction } from ' express ' ;
import { Role, Schema } from ' @iamjs/core ' ;
import { ExpressRoleManager } from ' @iamjs/express ' ;
interface CustomRequest extends Request {
user ?: { id : number ; role : string };
permissions ?: any ;
}
const roleManager = new ExpressRoleManager ( {
schema ,
onSuccess : < CustomRequest , Response > ( req , res , next ) => {
console . log ( ' Authorized user: ' , req . user ?. id ) ;
next () ;
},
onError : < CustomRequest , Response > ( err , req , res , next ) => {
console . error ( ' Authorization failed for user: ' , req . user ?. id ) ;
res . status ( 403 ) . json ( { error : ' Access denied ' } ) ;
},
} );
app . get ( ' /protected ' ,
roleManager . check < CustomRequest , Response >({
resources: ' protectedResource ' ,
actions: [ ' read ' ],
construct: true ,
data : async ( req ) => req . permissions
}),
( req : CustomRequest , res : Response ) => {
res . json ({ message: ' Access granted ' , userId: req . user ?. id });
}
);
For a more global approach, you can extend the Express Request type for your entire project. This is particularly useful when you want to add custom properties to the Request object across multiple files.
Create a file named types / express / index . d . ts
in your project and add the following code:
import express from ' express ' ;
declare global {
namespace Express {
interface Request {
user ?: { id : number ; role : string };
permissions ?: any ;
}
}
}
After adding this global declaration, you can use the extended Request type throughout your project without explicitly importing or extending it in each file:
import { Request, Response } from ' express ' ;
import { ExpressRoleManager } from ' @iamjs/express ' ;
const roleManager = new ExpressRoleManager ( {
schema ,
onSuccess : ( req : Request , res : Response , next ) => {
console . log ( ' Authorized user: ' , req . user ?. id ) ; // TypeScript now recognizes req.user
next () ;
},
// ... rest of the configuration
} );
app . get ( ' /protected ' ,
roleManager . check ({
resources: ' protectedResource ' ,
actions: [ ' read ' ],
construct: true ,
data : async ( req ) => req . permissions // TypeScript recognizes req.permissions
}),
( req : Request , res : Response ) => {
res . json ({ message: ' Access granted ' , userId: req . user ?. id });
}
);
For more information on declaration merging and global augmentation in TypeScript, refer to the official TypeScript documentation on Declaration Merging .
By using these TypeScript features, you can ensure type safety throughout your Express application while using ExpressRoleManager
, leading to more robust and maintainable code.
The checkFn
method allows you to perform authorization checks without the Express middleware context. This is particularly useful for:
Background jobs
API-based applications
Serverless functions
Any scenario where you need programmatic access control
import { ExpressRoleManager, Schema, Role } from ' @iamjs/express ' ;
// Define roles and schema
const userRole = new Role ( {
name : ' user ' ,
config : {
documents : {
base : ' r--l ' ,
custom : {
download : true
}
}
}
} );
const adminRole = new Role ( {
name : ' admin ' ,
config : {
documents : {
base : ' crudl ' ,
custom : {
delete : true
}
}
}
} );
const schema = new Schema ( {
roles : { user : userRole , admin : adminRole }
} );
// Initialize ExpressRoleManager
const roleManager = new ExpressRoleManager ( { schema } );
// Headless check function
async function canAccess ( userId : string , resource : string , action : string ) : Promise < boolean > {
const userRole = await getUserRole ( userId ); // Implement this function to fetch user's role
return await roleManager . checkFn ({
role: userRole ,
resources: resource ,
actions: action
});
}
// Usage in a background job
async function processDocuments () {
const documents = await fetchDocumentsToProcess ();
for ( const doc of documents) {
if ( await canAccess (doc . ownerId , ' documents ' , ' read ' )) {
await processDocument (doc);
} else {
console . log ( ` Access denied for document ${ doc . id } ` );
}
}
}
import { ExpressRoleManager, Schema } from ' @iamjs/express ' ;
const schema = new Schema ( { /* ... */ } );
const roleManager = new ExpressRoleManager ( { schema } );
async function checkDynamicPermission ( userId : string , resource : string , action : string ) : Promise < boolean > {
const userPermissions = await fetchUserPermissions ( userId );
return await roleManager . checkFn ({
resources: resource ,
actions: action ,
construct: true ,
data: userPermissions
});
}
// Usage in an API endpoint
app . get ( ' /api/documents/:id ' , async ( req , res ) => {
const canAccess = await checkDynamicPermission ( req . user . id , ' documents ' , ' read ' );
if (canAccess) {
const document = await fetchDocument ( req . params . id );
res . json (document);
} else {
res . status ( 403 ) . json ({ error: ' Access denied ' });
}
});
import { ExpressRoleManager, Schema, Role } from ' @iamjs/express ' ;
const schema = new Schema ( { /* ... */ } );
const roleManager = new ExpressRoleManager ( { schema } );
export async function handleDocumentAccess ( event , context ) {
const { userId , action } = JSON . parse ( event . body );
const userRole = await getUserRoleFromDatabase ( userId );
const canAccess = await roleManager . checkFn ( {
role : userRole ,
resources : ' documents ' ,
actions : action
} );
if (canAccess) {
// Perform the action
return {
statusCode: 200 ,
body: JSON . stringify ({ message: ' Access granted ' })
};
} else {
return {
statusCode: 403 ,
body: JSON . stringify ({ error: ' Access denied ' })
};
}
}
async function batchProcessDocuments ( userIds : string [], action : string ) {
const results = await Promise . all (
userIds . map ( async ( userId ) => {
const userRole = await getUserRole ( userId ) ;
const canAccess = await roleManager . checkFn ( {
role : userRole ,
resources : ' documents ' ,
actions : action
} ) ;
return { userId , canAccess };
} )
);
return results . filter ( result => result . canAccess ) . map ( result => result . userId );
}
// Usage
const userIdsWithAccess = await batchProcessDocuments ([ ' user1 ' , ' user2 ' , ' user3 ' ] , ' read ' );
console . log ( ' Users with read access: ' , userIdsWithAccess);
These examples demonstrate how checkFn
can be used in various "headless" scenarios, providing flexible and powerful authorization checks outside of the typical Express middleware flow. This approach allows you to implement consistent access control across different parts of your application, from web routes to background jobs and serverless functions.
Granular Permissions : Define roles with specific, granular permissions to allow for flexible access control.
Dynamic Role Assignment : Use the construct
and data
options to dynamically assign roles based on user data or other factors.
Error Handling : Provide clear, informative error messages in your onError
handler to aid in debugging and improve user experience.
Logging and Auditing : Utilize the onActivity
handler to maintain a comprehensive audit trail of authorization attempts and successes.
Performance Optimization : If you're dealing with a large number of roles or complex permission structures, consider caching role data or authorization results.
Security Considerations : Always validate and sanitize user input, especially when constructing roles dynamically.
Testing : Implement thorough unit and integration tests for your authorization logic, covering various role and permission scenarios.