/** * @wrnexus/authz — authorization: role-based (RBAC), policy-based (PBAC), and * attribute-based (ABAC). Compose freely; all three reduce to a boolean check * plus an `authorize()` guard middleware. * * const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"] }); * rbac.can(user, "post:write"); * * // PBAC/ABAC: a policy is a predicate over subject + resource + attributes * const ownsPost: Policy = (u, post) => u.id === post.authorId; * authorize((ctx) => ownsPost(ctx.user, resource)) // middleware */ import type { Context, Middleware } from "@wrnexus/core"; export interface Subject { id?: string; roles?: string[]; [attribute: string]: unknown; } // --- RBAC ------------------------------------------------------------------ export interface Rbac { /** True if any of the subject's roles grants `permission` (supports "*" and "ns:*"). */ can(subject: Subject | undefined, permission: string): boolean; /** All permissions granted to a set of roles. */ permissionsFor(roles: string[]): Set; } /** Build an RBAC checker from a role → permissions map. */ export function defineRbac(roles: Record): Rbac { const grants = (role: string, seen = new Set()): string[] => { if (seen.has(role)) return []; seen.add(role); const out: string[] = []; for (const p of roles[role] ?? []) { // A permission that names another role (prefixed "role:") inherits it. if (p.startsWith("role:")) out.push(...grants(p.slice(5), seen)); else out.push(p); } return out; }; const permissionsFor = (subjectRoles: string[]): Set => { const set = new Set(); for (const r of subjectRoles) for (const p of grants(r)) set.add(p); return set; }; return { permissionsFor, can(subject, permission) { if (!subject?.roles?.length) return false; const perms = permissionsFor(subject.roles); if (perms.has("*") || perms.has(permission)) return true; // Namespace wildcards: "post:*" grants "post:write". const ns = permission.includes(":") ? permission.slice(0, permission.indexOf(":")) + ":*" : null; return ns ? perms.has(ns) : false; }, }; } /** True if the subject has ALL of the given roles. */ export function hasRole(subject: Subject | undefined, ...required: string[]): boolean { const roles = new Set(subject?.roles ?? []); return required.every((r) => roles.has(r)); } // --- PBAC / ABAC ----------------------------------------------------------- /** A policy predicate: subject (+ optional resource/attributes) → allowed. */ export type Policy = ( subject: S, resource?: R, ) => boolean | Promise; /** Combine policies: allow if ANY passes (OR). */ export function any(...policies: Policy[]): Policy { return async (s, r) => { for (const p of policies) if (await p(s, r)) return true; return false; }; } /** Combine policies: allow only if ALL pass (AND). */ export function all(...policies: Policy[]): Policy { return async (s, r) => { for (const p of policies) if (!(await p(s, r))) return false; return true; }; } /** ABAC helper: allow when an attribute matches (equality or predicate). */ export function attr( name: string, match: unknown | ((value: unknown) => boolean), ): Policy { return (subject) => { const value = subject?.[name]; return typeof match === "function" ? (match as (v: unknown) => boolean)(value) : value === match; }; } // --- Guards (middleware) --------------------------------------------------- function forbidden(): Response { return Response.json({ ok: false, error: "Forbidden" }, { status: 403 }); } /** Guard a route with a policy over `ctx` (reads `ctx.user` as the subject). */ export function authorize(policy: (ctx: Context) => boolean | Promise): Middleware { return async (ctx, next) => ((await policy(ctx)) ? next() : forbidden()); } /** Guard requiring one of the given roles. */ export function requireRole(...roles: string[]): Middleware { return authorize((ctx) => { const subject = ctx.user as Subject | undefined; const have = new Set(subject?.roles ?? []); return roles.some((r) => have.has(r)); }); } /** Guard requiring an RBAC permission. */ export function requirePermission(rbac: Rbac, permission: string): Middleware { return authorize((ctx) => rbac.can(ctx.user as Subject | undefined, permission)); } export { allow, deny, decision, owner, anyDecision, allDecisions, authorizeDecision, filterAuthorized, } from "./advanced.ts"; export type { AuthorizationDecision, DecisionPolicy } from "./advanced.ts";