refactor: enhance permission handling and introduce new Permission and Policy classes

- Updated the `Guard` class to improve permission checking by utilizing the new `Permission` class.
- Refactored tests in `authorize.spec.ts` to use `Permission` instances instead of strings for better type safety.
- Introduced a new `permissions.spec.ts` file to test the functionality of the `Permission` and `Policy` classes.
- Enhanced the `recursivelyReplacePlaceholders` utility function to support various object structures and types.
- Updated middleware and controller files to align with the new permission handling structure.
This commit is contained in:
dswbx
2025-10-03 20:22:42 +02:00
parent db58911df3
commit 90f93caff4
14 changed files with 432 additions and 51 deletions

View File

@@ -60,7 +60,8 @@ export class AuthController extends Controller {
if (create) {
hono.post(
"/create",
permission([AuthPermissions.createUser, DataPermissions.entityCreate]),
permission(AuthPermissions.createUser),
permission(DataPermissions.entityCreate),
describeRoute({
summary: "Create a new user",
tags: ["auth"],

View File

@@ -1,5 +1,5 @@
import { Exception } from "core/errors";
import { $console, objectTransform } from "bknd/utils";
import { $console, objectTransform, type s } from "bknd/utils";
import { Permission } from "core/security/Permission";
import type { Context } from "hono";
import type { ServerEnv } from "modules/Controller";
@@ -12,6 +12,7 @@ export type GuardUserContext = {
export type GuardConfig = {
enabled?: boolean;
context?: string;
};
export type GuardContext = Context<ServerEnv> | GuardUserContext;
@@ -26,6 +27,9 @@ export class Guard {
this.config = config;
}
/**
* @deprecated
*/
static create(
permissionNames: string[],
roles?: Record<
@@ -156,12 +160,25 @@ export class Guard {
return !!rolePermission;
}
granted(permission: Permission | string, c?: GuardContext): boolean {
granted<P extends Permission>(
permission: P,
c?: GuardContext,
context: s.Static<P["context"]> = {} as s.Static<P["context"]>,
): boolean {
const user = c && "get" in c ? c.get("auth")?.user : c;
return this.hasPermission(permission as any, user);
const ctx = {
...context,
user,
context: this.config?.context,
};
return this.hasPermission(permission, user);
}
throwUnlessGranted(permission: Permission | string, c: GuardContext) {
throwUnlessGranted<P extends Permission>(
permission: P,
c: GuardContext,
context: s.Static<P["context"]>,
) {
if (!this.granted(permission, c)) {
throw new Exception(
`Permission "${typeof permission === "string" ? permission : permission.name}" not granted`,

View File

@@ -1,8 +1,9 @@
import type { Permission } from "core/security/Permission";
import { $console, patternMatch } from "bknd/utils";
import { $console, patternMatch, type s } from "bknd/utils";
import type { Context } from "hono";
import { createMiddleware } from "hono/factory";
import type { ServerEnv } from "modules/Controller";
import type { MaybePromise } from "core/types";
function getPath(reqOrCtx: Request | Context) {
const req = reqOrCtx instanceof Request ? reqOrCtx : reqOrCtx.req.raw;
@@ -49,7 +50,7 @@ export const auth = (options?: {
// make sure to only register once
if (authCtx.registered) {
skipped = true;
$console.warn(`auth middleware already registered for ${getPath(c)}`);
$console.debug(`auth middleware already registered for ${getPath(c)}`);
} else {
authCtx.registered = true;
@@ -68,11 +69,12 @@ export const auth = (options?: {
authCtx.user = undefined;
});
export const permission = (
permission: Permission | Permission[],
export const permission = <P extends Permission>(
permission: P,
options?: {
onGranted?: (c: Context<ServerEnv>) => Promise<Response | void | undefined>;
onDenied?: (c: Context<ServerEnv>) => Promise<Response | void | undefined>;
onGranted?: (c: Context<ServerEnv>) => MaybePromise<Response | void | undefined>;
onDenied?: (c: Context<ServerEnv>) => MaybePromise<Response | void | undefined>;
context?: (c: Context<ServerEnv>) => MaybePromise<s.Static<P["context"]>>;
},
) =>
// @ts-ignore
@@ -93,11 +95,11 @@ export const permission = (
}
} else if (!authCtx.skip) {
const guard = app.modules.ctx().guard;
const permissions = Array.isArray(permission) ? permission : [permission];
const context = (await options?.context?.(c)) ?? ({} as any);
if (options?.onGranted || options?.onDenied) {
let returned: undefined | void | Response;
if (permissions.every((p) => guard.granted(p, c))) {
if (guard.granted(permission, c, context)) {
returned = await options?.onGranted?.(c);
} else {
returned = await options?.onDenied?.(c);
@@ -106,7 +108,7 @@ export const permission = (
return returned;
}
} else {
permissions.some((p) => guard.throwUnlessGranted(p, c));
guard.throwUnlessGranted(permission, c, context);
}
}