mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 20:37:21 +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:
@@ -5,7 +5,7 @@ import { entityTypes } from "data/entities/Entity";
|
||||
import { isEqual } from "lodash-es";
|
||||
import type { ModuleBuildContext, ModuleBuildContextMcpContext } from "./Module";
|
||||
import type { EntityRelation } from "data/relations";
|
||||
import type { Permission } from "core/security/Permission";
|
||||
import type { Permission, PermissionContext } from "auth/authorize/Permission";
|
||||
import { Exception } from "core/errors";
|
||||
import { invariant, isPlainObject } from "bknd/utils";
|
||||
|
||||
@@ -114,10 +114,20 @@ export class ModuleHelper {
|
||||
entity.__replaceField(name, newField);
|
||||
}
|
||||
|
||||
async throwUnlessGranted(
|
||||
permission: Permission,
|
||||
async granted<P extends Permission<any, any, any, any>>(
|
||||
c: { context: ModuleBuildContextMcpContext; raw?: unknown },
|
||||
) {
|
||||
permission: P,
|
||||
context: PermissionContext<P>,
|
||||
): Promise<void>;
|
||||
async granted<P extends Permission<any, any, undefined, any>>(
|
||||
c: { context: ModuleBuildContextMcpContext; raw?: unknown },
|
||||
permission: P,
|
||||
): Promise<void>;
|
||||
async granted<P extends Permission<any, any, any, any>>(
|
||||
c: { context: ModuleBuildContextMcpContext; raw?: unknown },
|
||||
permission: P,
|
||||
context?: PermissionContext<P>,
|
||||
): Promise<void> {
|
||||
invariant(c.context.app, "app is not available in mcp context");
|
||||
const auth = c.context.app.module.auth;
|
||||
if (!auth.enabled) return;
|
||||
@@ -127,12 +137,6 @@ export class ModuleHelper {
|
||||
}
|
||||
|
||||
const user = await auth.authenticator?.resolveAuthFromRequest(c.raw as any);
|
||||
|
||||
if (!this.ctx.guard.granted(permission, user)) {
|
||||
throw new Exception(
|
||||
`Permission "${typeof permission === "string" ? permission : permission.name}" not granted`,
|
||||
403,
|
||||
);
|
||||
}
|
||||
this.ctx.guard.granted(permission, { user }, context as any);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { auth, permission } from "auth/middlewares";
|
||||
export { auth } from "auth/middlewares/auth.middleware";
|
||||
export { permission } from "auth/middlewares/permission.middleware";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Permission } from "core/security/Permission";
|
||||
import { Permission } from "auth/authorize/Permission";
|
||||
import { s } from "bknd/utils";
|
||||
|
||||
export const accessAdmin = new Permission("system.access.admin");
|
||||
@@ -24,6 +24,12 @@ export const configWrite = new Permission(
|
||||
module: s.string().optional(),
|
||||
}),
|
||||
);
|
||||
export const schemaRead = new Permission("system.schema.read");
|
||||
export const schemaRead = new Permission(
|
||||
"system.schema.read",
|
||||
{},
|
||||
s.object({
|
||||
module: s.string().optional(),
|
||||
}),
|
||||
);
|
||||
export const build = new Permission("system.build");
|
||||
export const mcp = new Permission("system.mcp");
|
||||
|
||||
@@ -116,6 +116,7 @@ export class AdminController extends Controller {
|
||||
onDenied: async (c) => {
|
||||
addFlashMessage(c, "You not allowed to read the schema", "warning");
|
||||
},
|
||||
context: (c) => ({}),
|
||||
}),
|
||||
async (c) => {
|
||||
const obj: AdminBkndWindowContext = {
|
||||
@@ -147,9 +148,10 @@ export class AdminController extends Controller {
|
||||
return c.redirect(authRoutes.success);
|
||||
}
|
||||
},
|
||||
context: (c) => ({}),
|
||||
};
|
||||
const redirectRouteParams = [
|
||||
permission(SystemPermissions.accessAdmin, options),
|
||||
permission(SystemPermissions.accessAdmin, options as any),
|
||||
permission(SystemPermissions.schemaRead, options),
|
||||
async (c) => {
|
||||
return c.html(c.get("html")!);
|
||||
|
||||
@@ -87,6 +87,10 @@ export class AppServer extends Module<AppServerConfig> {
|
||||
}
|
||||
|
||||
if (err instanceof AuthException) {
|
||||
if (isDebug()) {
|
||||
return c.json(err.toJSON(), err.code);
|
||||
}
|
||||
|
||||
return c.json(err.toJSON(), err.getSafeErrorAndCode().code);
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ export class SystemController extends Controller {
|
||||
private registerConfigController(client: Hono<any>): void {
|
||||
const { permission } = this.middlewares;
|
||||
// don't add auth again, it's already added in getController
|
||||
const hono = this.create().use(permission(SystemPermissions.configRead));
|
||||
const hono = this.create(); /* .use(permission(SystemPermissions.configRead)); */
|
||||
|
||||
if (!this.app.isReadOnly()) {
|
||||
const manager = this.app.modules as DbModuleManager;
|
||||
@@ -130,7 +130,11 @@ export class SystemController extends Controller {
|
||||
summary: "Get the raw config",
|
||||
tags: ["system"],
|
||||
}),
|
||||
permission(SystemPermissions.configReadSecrets),
|
||||
permission(SystemPermissions.configReadSecrets, {
|
||||
context: (c) => ({
|
||||
module: c.req.param("module"),
|
||||
}),
|
||||
}),
|
||||
async (c) => {
|
||||
// @ts-expect-error "fetch" is private
|
||||
return c.json(await this.app.modules.fetch().then((r) => r?.configs));
|
||||
@@ -165,7 +169,11 @@ export class SystemController extends Controller {
|
||||
|
||||
hono.post(
|
||||
"/set/:module",
|
||||
permission(SystemPermissions.configWrite),
|
||||
permission(SystemPermissions.configWrite, {
|
||||
context: (c) => ({
|
||||
module: c.req.param("module"),
|
||||
}),
|
||||
}),
|
||||
jsc("query", s.object({ force: s.boolean().optional() }), { skipOpenAPI: true }),
|
||||
async (c) => {
|
||||
const module = c.req.param("module") as any;
|
||||
@@ -194,32 +202,44 @@ export class SystemController extends Controller {
|
||||
},
|
||||
);
|
||||
|
||||
hono.post("/add/:module/:path", permission(SystemPermissions.configWrite), async (c) => {
|
||||
// @todo: require auth (admin)
|
||||
const module = c.req.param("module") as any;
|
||||
const value = await c.req.json();
|
||||
const path = c.req.param("path") as string;
|
||||
hono.post(
|
||||
"/add/:module/:path",
|
||||
permission(SystemPermissions.configWrite, {
|
||||
context: (c) => ({
|
||||
module: c.req.param("module"),
|
||||
}),
|
||||
}),
|
||||
async (c) => {
|
||||
// @todo: require auth (admin)
|
||||
const module = c.req.param("module") as any;
|
||||
const value = await c.req.json();
|
||||
const path = c.req.param("path") as string;
|
||||
|
||||
if (this.app.modules.get(module).schema().has(path)) {
|
||||
return c.json(
|
||||
{ success: false, path, error: "Path already exists" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
if (this.app.modules.get(module).schema().has(path)) {
|
||||
return c.json(
|
||||
{ success: false, path, error: "Path already exists" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
return await handleConfigUpdateResponse(c, async () => {
|
||||
await manager.mutateConfigSafe(module).patch(path, value);
|
||||
return {
|
||||
success: true,
|
||||
module,
|
||||
config: this.app.module[module].config,
|
||||
};
|
||||
});
|
||||
});
|
||||
return await handleConfigUpdateResponse(c, async () => {
|
||||
await manager.mutateConfigSafe(module).patch(path, value);
|
||||
return {
|
||||
success: true,
|
||||
module,
|
||||
config: this.app.module[module].config,
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
hono.patch(
|
||||
"/patch/:module/:path",
|
||||
permission(SystemPermissions.configWrite),
|
||||
permission(SystemPermissions.configWrite, {
|
||||
context: (c) => ({
|
||||
module: c.req.param("module"),
|
||||
}),
|
||||
}),
|
||||
async (c) => {
|
||||
// @todo: require auth (admin)
|
||||
const module = c.req.param("module") as any;
|
||||
@@ -239,7 +259,11 @@ export class SystemController extends Controller {
|
||||
|
||||
hono.put(
|
||||
"/overwrite/:module/:path",
|
||||
permission(SystemPermissions.configWrite),
|
||||
permission(SystemPermissions.configWrite, {
|
||||
context: (c) => ({
|
||||
module: c.req.param("module"),
|
||||
}),
|
||||
}),
|
||||
async (c) => {
|
||||
// @todo: require auth (admin)
|
||||
const module = c.req.param("module") as any;
|
||||
@@ -259,7 +283,11 @@ export class SystemController extends Controller {
|
||||
|
||||
hono.delete(
|
||||
"/remove/:module/:path",
|
||||
permission(SystemPermissions.configWrite),
|
||||
permission(SystemPermissions.configWrite, {
|
||||
context: (c) => ({
|
||||
module: c.req.param("module"),
|
||||
}),
|
||||
}),
|
||||
async (c) => {
|
||||
// @todo: require auth (admin)
|
||||
const module = c.req.param("module") as any;
|
||||
@@ -296,7 +324,7 @@ export class SystemController extends Controller {
|
||||
const { module } = c.req.valid("param");
|
||||
|
||||
if (secrets) {
|
||||
this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets, c, {
|
||||
this.ctx.guard.granted(SystemPermissions.configReadSecrets, c, {
|
||||
module,
|
||||
});
|
||||
}
|
||||
@@ -330,7 +358,11 @@ export class SystemController extends Controller {
|
||||
summary: "Get the schema for a module",
|
||||
tags: ["system"],
|
||||
}),
|
||||
permission(SystemPermissions.schemaRead),
|
||||
permission(SystemPermissions.schemaRead, {
|
||||
context: (c) => ({
|
||||
module: c.req.param("module"),
|
||||
}),
|
||||
}),
|
||||
jsc(
|
||||
"query",
|
||||
s
|
||||
@@ -347,12 +379,12 @@ export class SystemController extends Controller {
|
||||
const readonly = this.app.isReadOnly();
|
||||
|
||||
if (config) {
|
||||
this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead, c, {
|
||||
this.ctx.guard.granted(SystemPermissions.configRead, c, {
|
||||
module,
|
||||
});
|
||||
}
|
||||
if (secrets) {
|
||||
this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets, c, {
|
||||
this.ctx.guard.granted(SystemPermissions.configReadSecrets, c, {
|
||||
module,
|
||||
});
|
||||
}
|
||||
@@ -395,7 +427,7 @@ export class SystemController extends Controller {
|
||||
jsc("query", s.object({ sync: s.boolean().optional(), fetch: s.boolean().optional() })),
|
||||
async (c) => {
|
||||
const options = c.req.valid("query") as Record<string, boolean>;
|
||||
this.ctx.guard.throwUnlessGranted(SystemPermissions.build, c, {});
|
||||
this.ctx.guard.granted(SystemPermissions.build, c);
|
||||
|
||||
await this.app.build(options);
|
||||
return c.json({
|
||||
@@ -467,7 +499,7 @@ export class SystemController extends Controller {
|
||||
const { version, ...appConfig } = this.app.toJSON();
|
||||
|
||||
mcp.resource("system_config", "bknd://system/config", async (c) => {
|
||||
await c.context.ctx().helper.throwUnlessGranted(SystemPermissions.configRead, c);
|
||||
await c.context.ctx().helper.granted(c, SystemPermissions.configRead, {});
|
||||
|
||||
return c.json(this.app.toJSON(), {
|
||||
title: "System Config",
|
||||
@@ -477,7 +509,9 @@ export class SystemController extends Controller {
|
||||
"system_config_module",
|
||||
"bknd://system/config/{module}",
|
||||
async (c, { module }) => {
|
||||
await this.ctx.helper.throwUnlessGranted(SystemPermissions.configRead, c);
|
||||
await this.ctx.helper.granted(c, SystemPermissions.configRead, {
|
||||
module,
|
||||
});
|
||||
|
||||
const m = this.app.modules.get(module as any) as Module;
|
||||
return c.json(m.toJSON(), {
|
||||
@@ -489,7 +523,7 @@ export class SystemController extends Controller {
|
||||
},
|
||||
)
|
||||
.resource("system_schema", "bknd://system/schema", async (c) => {
|
||||
await this.ctx.helper.throwUnlessGranted(SystemPermissions.schemaRead, c);
|
||||
await this.ctx.helper.granted(c, SystemPermissions.schemaRead, {});
|
||||
|
||||
return c.json(this.app.getSchema(), {
|
||||
title: "System Schema",
|
||||
@@ -499,7 +533,9 @@ export class SystemController extends Controller {
|
||||
"system_schema_module",
|
||||
"bknd://system/schema/{module}",
|
||||
async (c, { module }) => {
|
||||
await this.ctx.helper.throwUnlessGranted(SystemPermissions.schemaRead, c);
|
||||
await this.ctx.helper.granted(c, SystemPermissions.schemaRead, {
|
||||
module,
|
||||
});
|
||||
|
||||
const m = this.app.modules.get(module as any);
|
||||
return c.json(m.getSchema().toJSON(), {
|
||||
|
||||
Reference in New Issue
Block a user