feat: centralize application framework primitives
This commit is contained in:
@@ -10,6 +10,27 @@ import type { AuthzScope } from "./types.ts";
|
||||
*/
|
||||
export const AUTHZ_LOCALS_KEY = "_authz";
|
||||
|
||||
export interface AuthorizationResponses {
|
||||
forbidden(decision?: AuthorizationDecision, options?: { exposeReason?: boolean }): Response;
|
||||
notFoundOrForbidden(options?: {
|
||||
mayDiscover?: boolean;
|
||||
decision?: AuthorizationDecision;
|
||||
exposeReason?: boolean;
|
||||
}): Response;
|
||||
}
|
||||
|
||||
export interface RequestAuthorization extends AuthorizationResponses {
|
||||
can(permission: string, resource?: unknown): Promise<boolean>;
|
||||
decide(permission: string, resource?: unknown): Promise<AuthorizationDecision>;
|
||||
}
|
||||
|
||||
declare module "@wrnexus/core" {
|
||||
interface Context {
|
||||
/** Installed by authzMiddleware for handlers that prefer context-local authorization. */
|
||||
authz?: RequestAuthorization;
|
||||
}
|
||||
}
|
||||
|
||||
interface RequestAuthz {
|
||||
resolver: AuthzResolver;
|
||||
/**
|
||||
@@ -49,10 +70,39 @@ export function authzMiddleware(options: AuthzResolverOptions): Middleware {
|
||||
byValue: new Map(),
|
||||
};
|
||||
ctx.locals[AUTHZ_LOCALS_KEY] = request;
|
||||
ctx.authz = {
|
||||
can: (permission, resource) => can(ctx, permission, resource),
|
||||
decide: (permission, resource) => decideFor(ctx, permission, resource),
|
||||
forbidden: (decision, responseOptions) =>
|
||||
forbiddenResponse(decision, responseOptions?.exposeReason),
|
||||
notFoundOrForbidden: (responseOptions = {}) =>
|
||||
responseOptions.mayDiscover
|
||||
? Response.json(
|
||||
{ ok: false, error: "Not Found" },
|
||||
{ status: 404, headers: NO_STORE_HEADERS },
|
||||
)
|
||||
: forbiddenResponse(responseOptions.decision, responseOptions.exposeReason),
|
||||
};
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
function forbiddenResponse(decision?: AuthorizationDecision, exposeReason = false): Response {
|
||||
return Response.json(
|
||||
exposeReason
|
||||
? { ok: false, error: "Forbidden", reason: decision?.reason, policy: decision?.policy }
|
||||
: { ok: false, error: "Forbidden" },
|
||||
{ status: 403, headers: NO_STORE_HEADERS },
|
||||
);
|
||||
}
|
||||
|
||||
export function getRequestAuthorization(ctx: Context): RequestAuthorization {
|
||||
if (!ctx.authz) {
|
||||
throw new Error("WRN-AUTHZ-SETUP: authzMiddleware() is not registered for this request.");
|
||||
}
|
||||
return ctx.authz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the tenant from the context at decision time, not at middleware time:
|
||||
* a request that switches tenant mid-flight must not keep the old scope.
|
||||
@@ -236,10 +286,7 @@ export function guardPermission(permission: string, options: GuardOptions = {}):
|
||||
reason: "Resource unavailable",
|
||||
at: Date.now(),
|
||||
});
|
||||
return Response.json(
|
||||
{ ok: false, error: "Forbidden" },
|
||||
{ status: 403, headers: NO_STORE_HEADERS },
|
||||
);
|
||||
return forbiddenResponse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,12 +311,7 @@ export function guardPermission(permission: string, options: GuardOptions = {}):
|
||||
);
|
||||
}
|
||||
|
||||
return Response.json(
|
||||
options.exposeReason
|
||||
? { ok: false, error: "Forbidden", reason: result.reason, policy: result.policy }
|
||||
: { ok: false, error: "Forbidden" },
|
||||
{ status: 403, headers: NO_STORE_HEADERS },
|
||||
);
|
||||
return forbiddenResponse(result, options.exposeReason);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user