fix(authz): tighten policy scope enforcement

This commit is contained in:
zouantchaw
2026-03-19 16:48:43 +01:00
parent 2f25d10368
commit a4ac0b2a6b
14 changed files with 743 additions and 30 deletions

View File

@@ -9,6 +9,28 @@ function getBearerToken(header) {
return token;
}
function buildBypassActor() {
let policyContext = {
user: { userId: 'test-user' },
tenant: { tenantId: '*' },
staff: { staffId: '*', workforceId: '*' },
};
if (process.env.AUTH_BYPASS_CONTEXT) {
try {
policyContext = JSON.parse(process.env.AUTH_BYPASS_CONTEXT);
} catch (_error) {
policyContext = {
user: { userId: 'test-user' },
tenant: { tenantId: '*' },
staff: { staffId: '*', workforceId: '*' },
};
}
}
return { uid: 'test-user', email: 'test@krow.local', role: 'TEST', policyContext };
}
export async function requireAuth(req, _res, next) {
try {
const token = getBearerToken(req.get('Authorization'));
@@ -17,7 +39,7 @@ export async function requireAuth(req, _res, next) {
}
if (process.env.AUTH_BYPASS === 'true') {
req.actor = { uid: 'test-user', email: 'test@krow.local', role: 'TEST' };
req.actor = buildBypassActor();
return next();
}
@@ -36,10 +58,14 @@ export async function requireAuth(req, _res, next) {
}
export function requirePolicy(action, resource) {
return (req, _res, next) => {
if (!can(action, resource, req.actor)) {
return next(new AppError('FORBIDDEN', 'Not allowed to perform this action', 403));
return async (req, _res, next) => {
try {
if (!(await can(action, resource, req.actor, req))) {
return next(new AppError('FORBIDDEN', 'Not allowed to perform this action', 403));
}
return next();
} catch (error) {
return next(error);
}
return next();
};
}

View File

@@ -1,5 +1,46 @@
export function can(action, resource, actor) {
void action;
void resource;
return Boolean(actor?.uid);
import { loadActorContext } from './actor-context.js';
function normalize(value) {
return `${value || ''}`.trim();
}
function requestField(req, field) {
return normalize(
req?.params?.[field]
?? req?.body?.[field]
?? req?.query?.[field]
);
}
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;
}
export async function can(action, resource, actor, req) {
void resource;
if (!action.startsWith('core.')) {
return false;
}
const context = await resolveActorContext(actor);
if (!context?.user || !context?.tenant) {
return false;
}
const tenantId = requestField(req, 'tenantId');
if (!tenantId) {
return true;
}
if (context.tenant.tenantId === '*') {
return true;
}
return context.tenant.tenantId === tenantId;
}

View File

@@ -0,0 +1,33 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { can } from '../src/services/policy.js';
test('core actions require tenant scope', async () => {
const allowed = await can(
'core.verification.read',
'verification',
{
uid: 'user-1',
policyContext: {
user: { userId: 'user-1' },
tenant: { tenantId: 'tenant-1' },
},
},
{}
);
const denied = await can(
'core.verification.read',
'verification',
{
uid: 'user-1',
policyContext: {
user: { userId: 'user-1' },
},
},
{}
);
assert.equal(allowed, true);
assert.equal(denied, false);
});