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

@@ -115,7 +115,7 @@ export class ModuleHelper {
}
async throwUnlessGranted(
permission: Permission | string,
permission: Permission,
c: { context: ModuleBuildContextMcpContext; raw?: unknown },
) {
invariant(c.context.app, "app is not available in mcp context");

View File

@@ -1,10 +1,29 @@
import { Permission } from "core/security/Permission";
import { s } from "bknd/utils";
export const accessAdmin = new Permission("system.access.admin");
export const accessApi = new Permission("system.access.api");
export const configRead = new Permission("system.config.read");
export const configReadSecrets = new Permission("system.config.read.secrets");
export const configWrite = new Permission("system.config.write");
export const configRead = new Permission(
"system.config.read",
{},
s.object({
module: s.string().optional(),
}),
);
export const configReadSecrets = new Permission(
"system.config.read.secrets",
{},
s.object({
module: s.string().optional(),
}),
);
export const configWrite = new Permission(
"system.config.write",
{},
s.object({
module: s.string().optional(),
}),
);
export const schemaRead = new Permission("system.schema.read");
export const build = new Permission("system.build");
export const mcp = new Permission("system.mcp");

View File

@@ -139,17 +139,18 @@ export class AdminController extends Controller {
}
if (auth_enabled) {
const options = {
onGranted: async (c) => {
// @todo: add strict test to permissions middleware?
if (c.get("auth")?.user) {
$console.log("redirecting to success");
return c.redirect(authRoutes.success);
}
},
};
const redirectRouteParams = [
permission([SystemPermissions.accessAdmin, SystemPermissions.schemaRead], {
// @ts-ignore
onGranted: async (c) => {
// @todo: add strict test to permissions middleware?
if (c.get("auth")?.user) {
$console.log("redirecting to success");
return c.redirect(authRoutes.success);
}
},
}),
permission(SystemPermissions.accessAdmin, options),
permission(SystemPermissions.schemaRead, options),
async (c) => {
return c.html(c.get("html")!);
},

View File

@@ -130,7 +130,7 @@ export class SystemController extends Controller {
summary: "Get the raw config",
tags: ["system"],
}),
permission([SystemPermissions.configReadSecrets]),
permission(SystemPermissions.configReadSecrets),
async (c) => {
// @ts-expect-error "fetch" is private
return c.json(await this.app.modules.fetch().then((r) => r?.configs));
@@ -295,7 +295,11 @@ export class SystemController extends Controller {
const { secrets } = c.req.valid("query");
const { module } = c.req.valid("param");
secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets, c);
if (secrets) {
this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets, c, {
module,
});
}
const config = this.app.toJSON(secrets);
@@ -342,8 +346,16 @@ export class SystemController extends Controller {
const { config, secrets, fresh } = c.req.valid("query");
const readonly = this.app.isReadOnly();
config && this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead, c);
secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets, c);
if (config) {
this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead, c, {
module,
});
}
if (secrets) {
this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets, c, {
module,
});
}
const { version, ...schema } = this.app.getSchema();
@@ -383,7 +395,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.throwUnlessGranted(SystemPermissions.build, c, {});
await this.app.build(options);
return c.json({