api-v1/middleware/fineAuthorize.js

221 lines
6.7 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 {
console.log(`[Fine Auth] Starting fine-grained authorization for ${req.method} ${req.path}`);
console.log(`[Fine Auth] Action: ${action}, Resource: ${resource}`);
console.log(`[Fine Auth] User:`, req.user ? {
userId: req.user.userId,
role: req.user.role,
} : 'NOT SET');
const resourceOwnerId = getResourceOwnerId ? await getResourceOwnerId(req) : null;
const resourceData = getResourceData ? await getResourceData(req) : {};
console.log(`[Fine Auth] Resource details:`, {
resourceOwnerId,
resourceData: Object.keys(resourceData),
});
const result = authorizeAction({
user: req.user,
action,
resource,
resourceOwnerId,
resourceData,
});
console.log(`[Fine Auth] Authorization result:`, {
authorized: result.authorized,
reason: result.reason || 'N/A',
});
if (!result.authorized) {
console.log(`[Fine Auth] ❌ FAILED: ${result.reason || 'Access denied'}`);
// 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',
});
}
console.log(`[Fine Auth] ✅ Authorization passed`);
next();
} catch (err) {
console.error('[Fine Auth] ❌ Fine authorization error:', err);
return res.status(500).json({
error: 'Internal server error',
message: 'Authorization check failed',
});
}
};
}