201 lines
6.0 KiB
JavaScript
201 lines
6.0 KiB
JavaScript
// middleware/fineAuthorize.js
|
|
/**
|
|
* Fine-Grained Authorization Helpers
|
|
*
|
|
* Provides reusable authorization functions for business logic
|
|
* Checks if user can perform specific actions on resources
|
|
*/
|
|
|
|
/**
|
|
* Authorize action on resource
|
|
*
|
|
* @param {Object} params
|
|
* @param {Object} params.user - User object from req.user
|
|
* @param {string} params.action - Action being performed (e.g., 'read', 'write', 'delete', 'create')
|
|
* @param {string} params.resource - Resource type (e.g., 'order', 'user', 'listing')
|
|
* @param {string} params.resourceOwnerId - ID of the resource owner (optional)
|
|
* @param {Object} params.resourceData - Additional resource data for context (optional)
|
|
* @returns {Object} { authorized: boolean, reason?: string }
|
|
*/
|
|
export function authorizeAction({ user, action, resource, resourceOwnerId = null, resourceData = {} }) {
|
|
if (!user || !user.userId) {
|
|
return { authorized: false, reason: 'User not authenticated' };
|
|
}
|
|
|
|
const userRole = (user.role || '').toUpperCase();
|
|
const isAdmin = userRole === 'ADMIN';
|
|
|
|
// Admin can do everything
|
|
if (isAdmin) {
|
|
return { authorized: true };
|
|
}
|
|
|
|
// Resource-specific authorization rules
|
|
switch (resource.toLowerCase()) {
|
|
case 'user':
|
|
return authorizeUserAction({ user, action, resourceOwnerId });
|
|
|
|
case 'order':
|
|
return authorizeOrderAction({ user, action, resourceOwnerId, resourceData });
|
|
|
|
case 'listing':
|
|
return authorizeListingAction({ user, action, resourceOwnerId, resourceData });
|
|
|
|
case 'location':
|
|
return authorizeLocationAction({ user, action, resourceOwnerId });
|
|
|
|
default:
|
|
// Default: Users can access their own resources, admins can access all
|
|
if (resourceOwnerId && resourceOwnerId === user.userId) {
|
|
return { authorized: true };
|
|
}
|
|
return { authorized: false, reason: `Access denied for ${resource}` };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Authorize user-related actions
|
|
*/
|
|
function authorizeUserAction({ user, action, resourceOwnerId }) {
|
|
// Users can read their own profile
|
|
if (action === 'read' && resourceOwnerId === user.userId) {
|
|
return { authorized: true };
|
|
}
|
|
|
|
// Users can update their own profile
|
|
if (action === 'write' && resourceOwnerId === user.userId) {
|
|
return { authorized: true };
|
|
}
|
|
|
|
// Users cannot delete accounts (should go through special endpoint)
|
|
if (action === 'delete') {
|
|
return { authorized: false, reason: 'Account deletion requires special process' };
|
|
}
|
|
|
|
// Users cannot access other users' data
|
|
if (resourceOwnerId && resourceOwnerId !== user.userId) {
|
|
return { authorized: false, reason: 'Cannot access other users\' data' };
|
|
}
|
|
|
|
return { authorized: true };
|
|
}
|
|
|
|
/**
|
|
* Authorize order-related actions
|
|
*/
|
|
function authorizeOrderAction({ user, action, resourceOwnerId, resourceData }) {
|
|
// Users can create orders
|
|
if (action === 'create') {
|
|
// Verify user can create order for their tenantId if multi-tenant
|
|
if (resourceData.tenantId && resourceData.tenantId !== user.tenantId) {
|
|
return { authorized: false, reason: 'Cannot create order for different tenant' };
|
|
}
|
|
return { authorized: true };
|
|
}
|
|
|
|
// Users can read/update/delete their own orders
|
|
if (resourceOwnerId === user.userId) {
|
|
return { authorized: true };
|
|
}
|
|
|
|
// Users cannot access other users' orders
|
|
return { authorized: false, reason: 'Cannot access other users\' orders' };
|
|
}
|
|
|
|
/**
|
|
* Authorize listing-related actions
|
|
*/
|
|
function authorizeListingAction({ user, action, resourceOwnerId, resourceData }) {
|
|
// Users can create listings
|
|
if (action === 'create') {
|
|
return { authorized: true };
|
|
}
|
|
|
|
// Users can read all active listings (public)
|
|
if (action === 'read' && resourceData.isActive !== false) {
|
|
return { authorized: true };
|
|
}
|
|
|
|
// Users can update/delete their own listings
|
|
if ((action === 'write' || action === 'delete') && resourceOwnerId === user.userId) {
|
|
return { authorized: true };
|
|
}
|
|
|
|
// Users cannot modify other users' listings
|
|
if (resourceOwnerId && resourceOwnerId !== user.userId) {
|
|
return { authorized: false, reason: 'Cannot modify other users\' listings' };
|
|
}
|
|
|
|
return { authorized: true };
|
|
}
|
|
|
|
/**
|
|
* Authorize location-related actions
|
|
*/
|
|
function authorizeLocationAction({ user, action, resourceOwnerId }) {
|
|
// Users can manage their own locations
|
|
if (resourceOwnerId === user.userId) {
|
|
return { authorized: true };
|
|
}
|
|
|
|
// Users cannot access other users' locations
|
|
return { authorized: false, reason: 'Cannot access other users\' locations' };
|
|
}
|
|
|
|
/**
|
|
* Express middleware wrapper for fine-grained authorization
|
|
*
|
|
* Usage:
|
|
* app.get('/orders/:orderId',
|
|
* fineAuthorize({ action: 'read', resource: 'order', getResourceOwnerId: (req) => req.params.orderId }),
|
|
* handler
|
|
* )
|
|
*/
|
|
export function fineAuthorize({ action, resource, getResourceOwnerId, getResourceData }) {
|
|
return async function(req, res, next) {
|
|
try {
|
|
const resourceOwnerId = getResourceOwnerId ? await getResourceOwnerId(req) : null;
|
|
const resourceData = getResourceData ? await getResourceData(req) : {};
|
|
|
|
const result = authorizeAction({
|
|
user: req.user,
|
|
action,
|
|
resource,
|
|
resourceOwnerId,
|
|
resourceData,
|
|
});
|
|
|
|
if (!result.authorized) {
|
|
// Log authorization failure
|
|
if (req.auditLogger) {
|
|
req.auditLogger.log({
|
|
userId: req.user.userId,
|
|
action: 'fine_authorization_failed',
|
|
route: req.path,
|
|
status: 'forbidden',
|
|
meta: {
|
|
action,
|
|
resource,
|
|
resourceOwnerId,
|
|
reason: result.reason,
|
|
},
|
|
});
|
|
}
|
|
|
|
return res.status(403).json({
|
|
error: 'Forbidden',
|
|
message: result.reason || 'Access denied',
|
|
});
|
|
}
|
|
|
|
next();
|
|
} catch (err) {
|
|
console.error('Fine authorization error:', err);
|
|
return res.status(500).json({
|
|
error: 'Internal server error',
|
|
message: 'Authorization check failed',
|
|
});
|
|
}
|
|
};
|
|
}
|