fix(authz): tighten policy scope enforcement
This commit is contained in:
@@ -1,5 +1,118 @@
|
||||
export function can(action, resource, actor) {
|
||||
void action;
|
||||
void resource;
|
||||
return Boolean(actor?.uid);
|
||||
import { loadActorContext } from './actor-context.js';
|
||||
|
||||
const TENANT_ADMIN_ROLES = new Set(['OWNER', 'ADMIN']);
|
||||
|
||||
function normalize(value) {
|
||||
return `${value || ''}`.trim();
|
||||
}
|
||||
|
||||
function requestField(req, field) {
|
||||
return normalize(
|
||||
req?.params?.[field]
|
||||
?? req?.body?.[field]
|
||||
?? req?.query?.[field]
|
||||
);
|
||||
}
|
||||
|
||||
function isTenantAdmin(context) {
|
||||
return TENANT_ADMIN_ROLES.has(normalize(context?.tenant?.role).toUpperCase());
|
||||
}
|
||||
|
||||
function hasTenantScope(context) {
|
||||
return Boolean(context?.user && context?.tenant);
|
||||
}
|
||||
|
||||
function hasClientScope(context) {
|
||||
return hasTenantScope(context) && Boolean(context?.business || isTenantAdmin(context));
|
||||
}
|
||||
|
||||
function hasStaffScope(context) {
|
||||
return hasTenantScope(context) && Boolean(context?.staff);
|
||||
}
|
||||
|
||||
function requiredScopeFor(action) {
|
||||
if (action === 'attendance.read') {
|
||||
return 'tenant';
|
||||
}
|
||||
|
||||
if (
|
||||
action === 'orders.read'
|
||||
|| action === 'orders.reorder.read'
|
||||
|| action === 'business.favorite-staff.read'
|
||||
|| action === 'staff.reviews.read'
|
||||
|| action.startsWith('client.')
|
||||
|| action.startsWith('billing.')
|
||||
|| action.startsWith('coverage.')
|
||||
|| action.startsWith('hubs.')
|
||||
|| action.startsWith('vendors.')
|
||||
|| action.startsWith('reports.')
|
||||
) {
|
||||
return 'client';
|
||||
}
|
||||
|
||||
if (
|
||||
action === 'shifts.read'
|
||||
|| action.startsWith('staff.')
|
||||
|| action.startsWith('payments.')
|
||||
) {
|
||||
return 'staff';
|
||||
}
|
||||
|
||||
return 'deny';
|
||||
}
|
||||
|
||||
async function resolveActorContext(actor) {
|
||||
if (!actor?.uid) {
|
||||
return null;
|
||||
}
|
||||
if (actor.policyContext) {
|
||||
return actor.policyContext;
|
||||
}
|
||||
const context = await loadActorContext(actor.uid);
|
||||
actor.policyContext = context;
|
||||
return context;
|
||||
}
|
||||
|
||||
function requestScopeMatches(req, context, requiredScope) {
|
||||
const tenantId = requestField(req, 'tenantId');
|
||||
if (tenantId && context?.tenant?.tenantId !== '*' && context?.tenant?.tenantId !== tenantId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const businessId = requestField(req, 'businessId');
|
||||
if (
|
||||
requiredScope === 'client'
|
||||
&& businessId
|
||||
&& context?.business?.businessId
|
||||
&& context.business.businessId !== '*'
|
||||
&& context.business.businessId !== businessId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function can(action, resource, actor, req) {
|
||||
void resource;
|
||||
const context = await resolveActorContext(actor);
|
||||
const requiredScope = requiredScopeFor(action);
|
||||
|
||||
if (requiredScope === 'deny' || !context?.user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requiredScope === 'tenant') {
|
||||
return hasTenantScope(context) && requestScopeMatches(req, context, requiredScope);
|
||||
}
|
||||
|
||||
if (requiredScope === 'client') {
|
||||
return hasClientScope(context) && requestScopeMatches(req, context, requiredScope);
|
||||
}
|
||||
|
||||
if (requiredScope === 'staff') {
|
||||
return hasStaffScope(context) && requestScopeMatches(req, context, requiredScope);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -233,6 +233,7 @@ export async function getAssignmentAttendance({ tenantId, assignmentId }) {
|
||||
SELECT
|
||||
a.id AS "assignmentId",
|
||||
a.status,
|
||||
a.business_id AS "businessId",
|
||||
a.shift_id AS "shiftId",
|
||||
a.staff_id AS "staffId",
|
||||
s.title AS "shiftTitle",
|
||||
|
||||
Reference in New Issue
Block a user