mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 04:46:05 +00:00
refactor: restructure permission handling and enhance Guard functionality
- Introduced a new `createGuard` function to streamline the creation of Guard instances with permissions and roles. - Updated tests in `authorize.spec.ts` to reflect changes in permission checks, ensuring they now return undefined for denied permissions. - Added new `Permission` and `Policy` classes to improve type safety and flexibility in permission management. - Refactored middleware and controller files to utilize the updated permission structure, including context handling for permissions. - Created a new `SystemController.spec.ts` file to test the integration of the new permission system within the SystemController. - Removed legacy permission handling from core security files, consolidating permission logic within the new structure.
This commit is contained in:
@@ -60,8 +60,8 @@ export class AuthController extends Controller {
|
||||
if (create) {
|
||||
hono.post(
|
||||
"/create",
|
||||
permission(AuthPermissions.createUser),
|
||||
permission(DataPermissions.entityCreate),
|
||||
permission(AuthPermissions.createUser, {}),
|
||||
permission(DataPermissions.entityCreate, {}),
|
||||
describeRoute({
|
||||
summary: "Create a new user",
|
||||
tags: ["auth"],
|
||||
@@ -239,7 +239,7 @@ export class AuthController extends Controller {
|
||||
}),
|
||||
},
|
||||
async (params, c) => {
|
||||
await c.context.ctx().helper.throwUnlessGranted(AuthPermissions.createUser, c);
|
||||
await c.context.ctx().helper.granted(c, AuthPermissions.createUser);
|
||||
|
||||
return c.json(await this.auth.createUser(params));
|
||||
},
|
||||
@@ -256,7 +256,7 @@ export class AuthController extends Controller {
|
||||
}),
|
||||
},
|
||||
async (params, c) => {
|
||||
await c.context.ctx().helper.throwUnlessGranted(AuthPermissions.createToken, c);
|
||||
await c.context.ctx().helper.granted(c, AuthPermissions.createToken);
|
||||
|
||||
const user = await getUser(params);
|
||||
return c.json({ user, token: await this.auth.authenticator.jwt(user) });
|
||||
@@ -275,7 +275,7 @@ export class AuthController extends Controller {
|
||||
}),
|
||||
},
|
||||
async (params, c) => {
|
||||
await c.context.ctx().helper.throwUnlessGranted(AuthPermissions.changePassword, c);
|
||||
await c.context.ctx().helper.granted(c, AuthPermissions.changePassword);
|
||||
|
||||
const user = await getUser(params);
|
||||
if (!(await this.auth.changePassword(user.id, params.password))) {
|
||||
@@ -296,7 +296,7 @@ export class AuthController extends Controller {
|
||||
}),
|
||||
},
|
||||
async (params, c) => {
|
||||
await c.context.ctx().helper.throwUnlessGranted(AuthPermissions.testPassword, c);
|
||||
await c.context.ctx().helper.granted(c, AuthPermissions.testPassword);
|
||||
|
||||
const pw = this.auth.authenticator.strategy("password") as PasswordStrategy;
|
||||
const controller = pw.getController(this.auth.authenticator);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Permission } from "core/security/Permission";
|
||||
import { Permission } from "auth/authorize/Permission";
|
||||
|
||||
export const createUser = new Permission("auth.user.create");
|
||||
//export const updateUser = new Permission("auth.user.update");
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Exception } from "core/errors";
|
||||
import { $console, objectTransform, type s } from "bknd/utils";
|
||||
import { Permission } from "core/security/Permission";
|
||||
import { $console, type s } from "bknd/utils";
|
||||
import type { Permission, PermissionContext } from "auth/authorize/Permission";
|
||||
import type { Context } from "hono";
|
||||
import type { ServerEnv } from "modules/Controller";
|
||||
import { Role } from "./Role";
|
||||
import type { Role } from "./Role";
|
||||
import { HttpStatus } from "bknd/utils";
|
||||
import type { Policy, PolicySchema } from "./Policy";
|
||||
|
||||
export type GuardUserContext = {
|
||||
role?: string | null;
|
||||
@@ -12,45 +14,43 @@ export type GuardUserContext = {
|
||||
|
||||
export type GuardConfig = {
|
||||
enabled?: boolean;
|
||||
context?: string;
|
||||
context?: object;
|
||||
};
|
||||
export type GuardContext = Context<ServerEnv> | GuardUserContext;
|
||||
|
||||
export class Guard {
|
||||
permissions: Permission[];
|
||||
roles?: Role[];
|
||||
config?: GuardConfig;
|
||||
export class GuardPermissionsException extends Exception {
|
||||
override name = "PermissionsException";
|
||||
override code = HttpStatus.FORBIDDEN;
|
||||
|
||||
constructor(permissions: Permission[] = [], roles: Role[] = [], config?: GuardConfig) {
|
||||
constructor(
|
||||
public permission: Permission,
|
||||
public policy?: Policy,
|
||||
public description?: string,
|
||||
) {
|
||||
super(`Permission "${permission.name}" not granted`);
|
||||
}
|
||||
|
||||
override toJSON(): any {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
description: this.description,
|
||||
permission: this.permission.name,
|
||||
policy: this.policy?.toJSON(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class Guard {
|
||||
constructor(
|
||||
public permissions: Permission<any, any, any, any>[] = [],
|
||||
public roles: Role[] = [],
|
||||
public config?: GuardConfig,
|
||||
) {
|
||||
this.permissions = permissions;
|
||||
this.roles = roles;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
static create(
|
||||
permissionNames: string[],
|
||||
roles?: Record<
|
||||
string,
|
||||
{
|
||||
permissions?: string[];
|
||||
is_default?: boolean;
|
||||
implicit_allow?: boolean;
|
||||
}
|
||||
>,
|
||||
config?: GuardConfig,
|
||||
) {
|
||||
const _roles = roles
|
||||
? objectTransform(roles, ({ permissions = [], is_default, implicit_allow }, name) => {
|
||||
return Role.createWithPermissionNames(name, permissions, is_default, implicit_allow);
|
||||
})
|
||||
: {};
|
||||
const _permissions = permissionNames.map((name) => new Permission(name));
|
||||
return new Guard(_permissions, Object.values(_roles), config);
|
||||
}
|
||||
|
||||
getPermissionNames(): string[] {
|
||||
return this.permissions.map((permission) => permission.name);
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export class Guard {
|
||||
return this;
|
||||
}
|
||||
|
||||
registerPermission(permission: Permission) {
|
||||
registerPermission(permission: Permission<any, any, any, any>) {
|
||||
if (this.permissions.find((p) => p.name === permission.name)) {
|
||||
throw new Error(`Permission ${permission.name} already exists`);
|
||||
}
|
||||
@@ -86,9 +86,13 @@ export class Guard {
|
||||
return this;
|
||||
}
|
||||
|
||||
registerPermissions(permissions: Record<string, Permission>);
|
||||
registerPermissions(permissions: Permission[]);
|
||||
registerPermissions(permissions: Permission[] | Record<string, Permission>) {
|
||||
registerPermissions(permissions: Record<string, Permission<any, any, any, any>>);
|
||||
registerPermissions(permissions: Permission<any, any, any, any>[]);
|
||||
registerPermissions(
|
||||
permissions:
|
||||
| Permission<any, any, any, any>[]
|
||||
| Record<string, Permission<any, any, any, any>>,
|
||||
) {
|
||||
const p = Array.isArray(permissions) ? permissions : Object.values(permissions);
|
||||
|
||||
for (const permission of p) {
|
||||
@@ -121,69 +125,133 @@ export class Guard {
|
||||
return this.config?.enabled === true;
|
||||
}
|
||||
|
||||
hasPermission(permission: Permission, user?: GuardUserContext): boolean;
|
||||
hasPermission(name: string, user?: GuardUserContext): boolean;
|
||||
hasPermission(permissionOrName: Permission | string, user?: GuardUserContext): boolean {
|
||||
if (!this.isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const name = typeof permissionOrName === "string" ? permissionOrName : permissionOrName.name;
|
||||
$console.debug("guard: checking permission", {
|
||||
name,
|
||||
user: { id: user?.id, role: user?.role },
|
||||
});
|
||||
const exists = this.permissionExists(name);
|
||||
if (!exists) {
|
||||
throw new Error(`Permission ${name} does not exist`);
|
||||
}
|
||||
|
||||
private collect(permission: Permission, c: GuardContext, context: any) {
|
||||
const user = c && "get" in c ? c.get("auth")?.user : c;
|
||||
const ctx = {
|
||||
...((context ?? {}) as any),
|
||||
...this.config?.context,
|
||||
user,
|
||||
};
|
||||
const exists = this.permissionExists(permission.name);
|
||||
const role = this.getUserRole(user);
|
||||
const rolePermission = role?.permissions.find(
|
||||
(rolePermission) => rolePermission.permission.name === permission.name,
|
||||
);
|
||||
return {
|
||||
ctx,
|
||||
user,
|
||||
exists,
|
||||
role,
|
||||
rolePermission,
|
||||
};
|
||||
}
|
||||
|
||||
granted<P extends Permission<any, any, any, any>>(
|
||||
permission: P,
|
||||
c: GuardContext,
|
||||
context: PermissionContext<P>,
|
||||
): void;
|
||||
granted<P extends Permission<any, any, undefined, any>>(permission: P, c: GuardContext): void;
|
||||
granted<P extends Permission<any, any, any, any>>(
|
||||
permission: P,
|
||||
c: GuardContext,
|
||||
context?: PermissionContext<P>,
|
||||
): void {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
const { ctx, user, exists, role, rolePermission } = this.collect(permission, c, context);
|
||||
|
||||
$console.debug("guard: checking permission", {
|
||||
name: permission.name,
|
||||
context: ctx,
|
||||
});
|
||||
if (!exists) {
|
||||
throw new GuardPermissionsException(
|
||||
permission,
|
||||
undefined,
|
||||
`Permission ${permission.name} does not exist`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!role) {
|
||||
$console.debug("guard: user has no role, denying");
|
||||
return false;
|
||||
throw new GuardPermissionsException(permission, undefined, "User has no role");
|
||||
} else if (role.implicit_allow === true) {
|
||||
$console.debug(`guard: role "${role.name}" has implicit allow, allowing`);
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
const rolePermission = role.permissions.find(
|
||||
(rolePermission) => rolePermission.permission.name === name,
|
||||
);
|
||||
|
||||
$console.debug("guard: rolePermission, allowing?", {
|
||||
permission: name,
|
||||
role: role.name,
|
||||
allowing: !!rolePermission,
|
||||
});
|
||||
return !!rolePermission;
|
||||
}
|
||||
|
||||
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;
|
||||
const ctx = {
|
||||
...context,
|
||||
user,
|
||||
context: this.config?.context,
|
||||
};
|
||||
return this.hasPermission(permission, user);
|
||||
}
|
||||
|
||||
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`,
|
||||
403,
|
||||
if (!rolePermission) {
|
||||
$console.debug("guard: rolePermission not found, denying");
|
||||
throw new GuardPermissionsException(
|
||||
permission,
|
||||
undefined,
|
||||
"Role does not have required permission",
|
||||
);
|
||||
}
|
||||
|
||||
// validate context
|
||||
let ctx2 = Object.assign({}, ctx);
|
||||
if (permission.context) {
|
||||
ctx2 = permission.parseContext(ctx2);
|
||||
}
|
||||
|
||||
if (rolePermission?.policies.length > 0) {
|
||||
$console.debug("guard: rolePermission has policies, checking");
|
||||
for (const policy of rolePermission.policies) {
|
||||
// skip filter policies
|
||||
if (policy.content.effect === "filter") continue;
|
||||
|
||||
// if condition unmet or effect is deny, throw
|
||||
const meets = policy.meetsCondition(ctx2);
|
||||
if (!meets || (meets && policy.content.effect === "deny")) {
|
||||
throw new GuardPermissionsException(
|
||||
permission,
|
||||
policy,
|
||||
"Policy does not meet condition",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$console.debug("guard allowing", {
|
||||
permission: permission.name,
|
||||
role: role.name,
|
||||
});
|
||||
}
|
||||
|
||||
getPolicyFilter<P extends Permission<any, any, any, any>>(
|
||||
permission: P,
|
||||
c: GuardContext,
|
||||
context: PermissionContext<P>,
|
||||
): PolicySchema["filter"] | undefined;
|
||||
getPolicyFilter<P extends Permission<any, any, undefined, any>>(
|
||||
permission: P,
|
||||
c: GuardContext,
|
||||
): PolicySchema["filter"] | undefined;
|
||||
getPolicyFilter<P extends Permission<any, any, any, any>>(
|
||||
permission: P,
|
||||
c: GuardContext,
|
||||
context?: PermissionContext<P>,
|
||||
): PolicySchema["filter"] | undefined {
|
||||
if (!permission.isFilterable()) return;
|
||||
|
||||
const { ctx, exists, role, rolePermission } = this.collect(permission, c, context);
|
||||
|
||||
// validate context
|
||||
let ctx2 = Object.assign({}, ctx);
|
||||
if (permission.context) {
|
||||
ctx2 = permission.parseContext(ctx2);
|
||||
}
|
||||
|
||||
if (exists && role && rolePermission && rolePermission.policies.length > 0) {
|
||||
for (const policy of rolePermission.policies) {
|
||||
if (policy.content.effect === "filter") {
|
||||
return policy.meetsFilter(ctx2) ? policy.content.filter : undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
68
app/src/auth/authorize/Permission.ts
Normal file
68
app/src/auth/authorize/Permission.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { s, type ParseOptions, parse, InvalidSchemaError, HttpStatus } from "bknd/utils";
|
||||
|
||||
export const permissionOptionsSchema = s
|
||||
.strictObject({
|
||||
description: s.string(),
|
||||
filterable: s.boolean(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
export type PermissionOptions = s.Static<typeof permissionOptionsSchema>;
|
||||
export type PermissionContext<P extends Permission<any, any, any, any>> = P extends Permission<
|
||||
any,
|
||||
any,
|
||||
infer Context,
|
||||
any
|
||||
>
|
||||
? Context extends s.ObjectSchema
|
||||
? s.Static<Context>
|
||||
: never
|
||||
: never;
|
||||
|
||||
export class InvalidPermissionContextError extends InvalidSchemaError {
|
||||
override name = "InvalidPermissionContextError";
|
||||
|
||||
// changing to internal server error because it's an unexpected behavior
|
||||
override code = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
static from(e: InvalidSchemaError) {
|
||||
return new InvalidPermissionContextError(e.schema, e.value, e.errors);
|
||||
}
|
||||
}
|
||||
|
||||
export class Permission<
|
||||
Name extends string = string,
|
||||
Options extends PermissionOptions = {},
|
||||
Context extends s.ObjectSchema | undefined = undefined,
|
||||
ContextValue = Context extends s.ObjectSchema ? s.Static<Context> : undefined,
|
||||
> {
|
||||
constructor(
|
||||
public name: Name,
|
||||
public options: Options = {} as Options,
|
||||
public context: Context = undefined as Context,
|
||||
) {}
|
||||
|
||||
isFilterable() {
|
||||
return this.options.filterable === true;
|
||||
}
|
||||
|
||||
parseContext(ctx: ContextValue, opts?: ParseOptions) {
|
||||
try {
|
||||
return this.context ? parse(this.context!, ctx, opts) : undefined;
|
||||
} catch (e) {
|
||||
if (e instanceof InvalidSchemaError) {
|
||||
throw InvalidPermissionContextError.from(e);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
name: this.name,
|
||||
...this.options,
|
||||
context: this.context,
|
||||
};
|
||||
}
|
||||
}
|
||||
42
app/src/auth/authorize/Policy.ts
Normal file
42
app/src/auth/authorize/Policy.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { s, parse, recursivelyReplacePlaceholders } from "bknd/utils";
|
||||
import * as query from "core/object/query/object-query";
|
||||
|
||||
export const policySchema = s
|
||||
.strictObject({
|
||||
description: s.string(),
|
||||
condition: s.object({}).optional() as s.Schema<{}, query.ObjectQuery | undefined>,
|
||||
effect: s.string({ enum: ["allow", "deny", "filter"], default: "allow" }),
|
||||
filter: s.object({}).optional() as s.Schema<{}, query.ObjectQuery | undefined>,
|
||||
})
|
||||
.partial();
|
||||
export type PolicySchema = s.Static<typeof policySchema>;
|
||||
|
||||
export class Policy<Schema extends PolicySchema = PolicySchema> {
|
||||
public content: Schema;
|
||||
|
||||
constructor(content?: Schema) {
|
||||
this.content = parse(policySchema, content ?? {}, {
|
||||
withDefaults: true,
|
||||
}) as Schema;
|
||||
}
|
||||
|
||||
replace(context: object, vars?: Record<string, any>) {
|
||||
return vars ? recursivelyReplacePlaceholders(context, /^@([a-zA-Z_\.]+)$/, vars) : context;
|
||||
}
|
||||
|
||||
meetsCondition(context: object, vars?: Record<string, any>) {
|
||||
return query.validate(this.replace(this.content.condition!, vars), context);
|
||||
}
|
||||
|
||||
meetsFilter(subject: object, vars?: Record<string, any>) {
|
||||
return query.validate(this.replace(this.content.filter!, vars), subject);
|
||||
}
|
||||
|
||||
getFiltered<Given extends any[]>(given: Given): Given {
|
||||
return given.filter((item) => this.meetsFilter(item)) as Given;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.content;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,33 @@
|
||||
import { Permission } from "core/security/Permission";
|
||||
import { parse, s } from "bknd/utils";
|
||||
import { Permission } from "./Permission";
|
||||
import { Policy, policySchema } from "./Policy";
|
||||
|
||||
export const rolePermissionSchema = s.strictObject({
|
||||
permission: s.string(),
|
||||
policies: s.array(policySchema).optional(),
|
||||
});
|
||||
export type RolePermissionSchema = s.Static<typeof rolePermissionSchema>;
|
||||
|
||||
export const roleSchema = s.strictObject({
|
||||
name: s.string(),
|
||||
permissions: s.anyOf([s.array(s.string()), s.array(rolePermissionSchema)]).optional(),
|
||||
is_default: s.boolean().optional(),
|
||||
implicit_allow: s.boolean().optional(),
|
||||
});
|
||||
export type RoleSchema = s.Static<typeof roleSchema>;
|
||||
|
||||
export class RolePermission {
|
||||
constructor(
|
||||
public permission: Permission,
|
||||
public config?: any,
|
||||
public permission: Permission<any, any, any, any>,
|
||||
public policies: Policy[] = [],
|
||||
) {}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
permission: this.permission.name,
|
||||
policies: this.policies.map((p) => p.toJSON()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class Role {
|
||||
@@ -15,31 +38,24 @@ export class Role {
|
||||
public implicit_allow: boolean = false,
|
||||
) {}
|
||||
|
||||
static createWithPermissionNames(
|
||||
name: string,
|
||||
permissionNames: string[],
|
||||
is_default: boolean = false,
|
||||
implicit_allow: boolean = false,
|
||||
) {
|
||||
return new Role(
|
||||
name,
|
||||
permissionNames.map((name) => new RolePermission(new Permission(name))),
|
||||
is_default,
|
||||
implicit_allow,
|
||||
);
|
||||
static create(config: RoleSchema) {
|
||||
const permissions =
|
||||
config.permissions?.map((p: string | RolePermissionSchema) => {
|
||||
if (typeof p === "string") {
|
||||
return new RolePermission(new Permission(p), []);
|
||||
}
|
||||
const policies = p.policies?.map((policy) => new Policy(policy));
|
||||
return new RolePermission(new Permission(p.permission), policies);
|
||||
}) ?? [];
|
||||
return new Role(config.name, permissions, config.is_default, config.implicit_allow);
|
||||
}
|
||||
|
||||
static create(config: {
|
||||
name: string;
|
||||
permissions?: string[];
|
||||
is_default?: boolean;
|
||||
implicit_allow?: boolean;
|
||||
}) {
|
||||
return new Role(
|
||||
config.name,
|
||||
config.permissions?.map((name) => new RolePermission(new Permission(name))) ?? [],
|
||||
config.is_default,
|
||||
config.implicit_allow,
|
||||
);
|
||||
toJSON() {
|
||||
return {
|
||||
name: this.name,
|
||||
permissions: this.permissions.map((p) => p.toJSON()),
|
||||
is_default: this.is_default,
|
||||
implicit_allow: this.implicit_allow,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { Permission } from "core/security/Permission";
|
||||
import { $console, patternMatch, type s } from "bknd/utils";
|
||||
import { $console, patternMatch } 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;
|
||||
@@ -68,49 +66,3 @@ export const auth = (options?: {
|
||||
authCtx.resolved = false;
|
||||
authCtx.user = undefined;
|
||||
});
|
||||
|
||||
export const permission = <P extends Permission>(
|
||||
permission: P,
|
||||
options?: {
|
||||
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
|
||||
createMiddleware<ServerEnv>(async (c, next) => {
|
||||
const app = c.get("app");
|
||||
const authCtx = c.get("auth");
|
||||
if (!authCtx) {
|
||||
throw new Error("auth ctx not found");
|
||||
}
|
||||
|
||||
// in tests, app is not defined
|
||||
if (!authCtx.registered || !app) {
|
||||
const msg = `auth middleware not registered, cannot check permissions for ${getPath(c)}`;
|
||||
if (app?.module.auth.enabled) {
|
||||
throw new Error(msg);
|
||||
} else {
|
||||
$console.warn(msg);
|
||||
}
|
||||
} else if (!authCtx.skip) {
|
||||
const guard = app.modules.ctx().guard;
|
||||
const context = (await options?.context?.(c)) ?? ({} as any);
|
||||
|
||||
if (options?.onGranted || options?.onDenied) {
|
||||
let returned: undefined | void | Response;
|
||||
if (guard.granted(permission, c, context)) {
|
||||
returned = await options?.onGranted?.(c);
|
||||
} else {
|
||||
returned = await options?.onDenied?.(c);
|
||||
}
|
||||
if (returned instanceof Response) {
|
||||
return returned;
|
||||
}
|
||||
} else {
|
||||
guard.throwUnlessGranted(permission, c, context);
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
93
app/src/auth/middlewares/permission.middleware.ts
Normal file
93
app/src/auth/middlewares/permission.middleware.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { Permission, PermissionContext } from "auth/authorize/Permission";
|
||||
import { $console, threw } from "bknd/utils";
|
||||
import type { Context, Hono } from "hono";
|
||||
import type { RouterRoute } from "hono/types";
|
||||
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;
|
||||
return new URL(req.url).pathname;
|
||||
}
|
||||
|
||||
const permissionSymbol = Symbol.for("permission");
|
||||
|
||||
type PermissionMiddlewareOptions<P extends Permission<any, any, any, any>> = {
|
||||
onGranted?: (c: Context<ServerEnv>) => MaybePromise<Response | void | undefined>;
|
||||
onDenied?: (c: Context<ServerEnv>) => MaybePromise<Response | void | undefined>;
|
||||
} & (P extends Permission<any, any, infer PC, any>
|
||||
? PC extends undefined
|
||||
? {
|
||||
context?: never;
|
||||
}
|
||||
: {
|
||||
context: (c: Context<ServerEnv>) => MaybePromise<PermissionContext<P>>;
|
||||
}
|
||||
: {
|
||||
context?: never;
|
||||
});
|
||||
|
||||
export function permission<P extends Permission<any, any, any, any>>(
|
||||
permission: P,
|
||||
options: PermissionMiddlewareOptions<P>,
|
||||
) {
|
||||
// @ts-ignore (middlewares do not always return)
|
||||
const handler = createMiddleware<ServerEnv>(async (c, next) => {
|
||||
const app = c.get("app");
|
||||
const authCtx = c.get("auth");
|
||||
if (!authCtx) {
|
||||
throw new Error("auth ctx not found");
|
||||
}
|
||||
|
||||
// in tests, app is not defined
|
||||
if (!authCtx.registered || !app) {
|
||||
const msg = `auth middleware not registered, cannot check permissions for ${getPath(c)}`;
|
||||
if (app?.module.auth.enabled) {
|
||||
throw new Error(msg);
|
||||
} else {
|
||||
$console.warn(msg);
|
||||
}
|
||||
} else if (!authCtx.skip) {
|
||||
const guard = app.modules.ctx().guard;
|
||||
const context = (await options?.context?.(c)) ?? ({} as any);
|
||||
|
||||
if (options?.onGranted || options?.onDenied) {
|
||||
let returned: undefined | void | Response;
|
||||
if (threw(() => guard.granted(permission, c, context))) {
|
||||
returned = await options?.onDenied?.(c);
|
||||
} else {
|
||||
returned = await options?.onGranted?.(c);
|
||||
}
|
||||
if (returned instanceof Response) {
|
||||
return returned;
|
||||
}
|
||||
} else {
|
||||
guard.granted(permission, c, context);
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
return Object.assign(handler, {
|
||||
[permissionSymbol]: { permission, options },
|
||||
});
|
||||
}
|
||||
|
||||
export function getPermissionRoutes(hono: Hono<any>) {
|
||||
const routes: {
|
||||
route: RouterRoute;
|
||||
permission: Permission;
|
||||
options: PermissionMiddlewareOptions<Permission>;
|
||||
}[] = [];
|
||||
for (const route of hono.routes) {
|
||||
if (permissionSymbol in route.handler) {
|
||||
routes.push({
|
||||
route,
|
||||
...(route.handler[permissionSymbol] as any),
|
||||
});
|
||||
}
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
Reference in New Issue
Block a user