feat: permission hooks, isAllowed and permittedOrgs

This commit is contained in:
2026-10-21 14:38:15 +02:00
parent ee15ee0adf
commit bc05d312a6
10 changed files with 259 additions and 71 deletions
+55
View File
@@ -0,0 +1,55 @@
import type { FastifyRequest } from 'fastify';
import type { Pool } from 'pg';
/** One permission decision: may this user perform (activity, view) in this org. */
export interface PermissionCheck {
userId: string;
/** The org acted in, or null for the no-org perimeter. */
orgId: string | null;
activity: string;
view: string;
/** The request behind the decision, when there is one. */
req?: FastifyRequest;
}
/** Runs before morbac is asked. Returning false refuses without asking it. */
export type PrePermissionHook = (check: PermissionCheck, db: Pool) => boolean | Promise<boolean>;
/** Runs after morbac answered, with that answer. Returning false refuses. */
export type PostPermissionHook = (check: PermissionCheck, allowed: boolean, db: Pool) => boolean | Promise<boolean>;
const pre: PrePermissionHook[] = [];
const post: PostPermissionHook[] = [];
/**
* Adds an application rule to every permission decision the helpers make. A
* hook can only narrow: a decision is allowed when morbac allows it and every
* hook agrees, so no hook can grant what morbac refused. A hook that throws
* fails the decision with its error; nothing is swallowed into an allow.
* Returns a function that removes the hook.
*/
export function addPermissionHook(phase: 'pre', hook: PrePermissionHook): () => void;
export function addPermissionHook(phase: 'post', hook: PostPermissionHook): () => void;
export function addPermissionHook(phase: 'pre' | 'post', hook: PrePermissionHook | PostPermissionHook): () => void {
const list = (phase === 'pre' ? pre : post) as unknown[];
list.push(hook);
return () => {
const i = list.indexOf(hook);
if (i !== -1) list.splice(i, 1);
};
}
export async function runPreHooks(check: PermissionCheck, db: Pool): Promise<boolean> {
for (const hook of pre) {
if (await hook(check, db) !== true) return false;
}
return true;
}
export async function runPostHooks(check: PermissionCheck, allowed: boolean, db: Pool): Promise<boolean> {
let result = allowed;
for (const hook of post) {
if (await hook(check, allowed, db) !== true) result = false;
}
return result;
}