Update permissions handling and enhance Guard functionality

- Bump `jsonv-ts` dependency to 0.8.6.
- Refactor permission checks in the `Guard` class to improve context validation and error handling.
- Update tests to reflect changes in permission handling, ensuring robust coverage for new scenarios.
- Introduce new test cases for data permissions, enhancing overall test coverage and reliability.
This commit is contained in:
dswbx
2025-10-21 16:44:08 +02:00
parent 0347efa592
commit 38902ebcba
20 changed files with 859 additions and 153 deletions

View File

@@ -9,6 +9,7 @@ import {
pickKeys,
mcpTool,
convertNumberedObjectToArray,
mergeObject,
} from "bknd/utils";
import * as SystemPermissions from "modules/permissions";
import type { AppDataConfig } from "../data-schema";
@@ -95,7 +96,9 @@ export class DataController extends Controller {
// read entity schema
hono.get(
"/schema.json",
permission(DataPermissions.entityRead, {}),
permission(DataPermissions.entityRead, {
context: (c) => ({ entity: c.req.param("entity") }),
}),
describeRoute({
summary: "Retrieve data schema",
tags: ["data"],
@@ -121,7 +124,9 @@ export class DataController extends Controller {
// read schema
hono.get(
"/schemas/:entity/:context?",
permission(DataPermissions.entityRead, {}),
permission(DataPermissions.entityRead, {
context: (c) => ({ entity: c.req.param("entity") }),
}),
describeRoute({
summary: "Retrieve entity schema",
tags: ["data"],
@@ -161,7 +166,9 @@ export class DataController extends Controller {
*/
hono.get(
"/info/:entity",
permission(DataPermissions.entityRead, {}),
permission(DataPermissions.entityRead, {
context: (c) => ({ entity: c.req.param("entity") }),
}),
describeRoute({
summary: "Retrieve entity info",
tags: ["data"],
@@ -213,7 +220,9 @@ export class DataController extends Controller {
// fn: count
hono.post(
"/:entity/fn/count",
permission(DataPermissions.entityRead, {}),
permission(DataPermissions.entityRead, {
context: (c) => ({ entity: c.req.param("entity") }),
}),
describeRoute({
summary: "Count entities",
tags: ["data"],
@@ -236,7 +245,9 @@ export class DataController extends Controller {
// fn: exists
hono.post(
"/:entity/fn/exists",
permission(DataPermissions.entityRead, {}),
permission(DataPermissions.entityRead, {
context: (c) => ({ entity: c.req.param("entity") }),
}),
describeRoute({
summary: "Check if entity exists",
tags: ["data"],
@@ -285,16 +296,26 @@ export class DataController extends Controller {
parameters: saveRepoQueryParams(["limit", "offset", "sort", "select", "join"]),
tags: ["data"],
}),
permission(DataPermissions.entityRead, {}),
jsc("param", s.object({ entity: entitiesEnum })),
jsc("query", repoQuery, { skipOpenAPI: true }),
permission(DataPermissions.entityRead, {
context: (c) => ({ entity: c.req.param("entity") }),
}),
async (c) => {
const { entity } = c.req.valid("param");
if (!this.entityExists(entity)) {
return this.notFound(c);
}
const { merge } = this.ctx.guard.filters(DataPermissions.entityRead, c, {
entity,
});
const options = c.req.valid("query") as RepoQuery;
const result = await this.em.repository(entity).findMany(options);
const result = await this.em.repository(entity).findMany({
...options,
where: merge(options.where),
});
return c.json(result, { status: result.data ? 200 : 404 });
},
@@ -308,7 +329,9 @@ export class DataController extends Controller {
parameters: saveRepoQueryParams(["offset", "sort", "select"]),
tags: ["data"],
}),
permission(DataPermissions.entityRead, {}),
permission(DataPermissions.entityRead, {
context: (c) => ({ ...c.req.param() }) as any,
}),
mcpTool("data_entity_read_one", {
inputSchema: {
param: s.object({ entity: entitiesEnum, id: idType }),
@@ -326,11 +349,19 @@ export class DataController extends Controller {
jsc("query", repoQuery, { skipOpenAPI: true }),
async (c) => {
const { entity, id } = c.req.valid("param");
if (!this.entityExists(entity)) {
if (!this.entityExists(entity) || !id) {
return this.notFound(c);
}
const options = c.req.valid("query") as RepoQuery;
const result = await this.em.repository(entity).findId(id, options);
const { merge } = this.ctx.guard.filters(
DataPermissions.entityRead,
c,
c.req.valid("param"),
);
const id_name = this.em.entity(entity).getPrimaryField().name;
const result = await this.em
.repository(entity)
.findOne(merge({ [id_name]: id }), options);
return c.json(result, { status: result.data ? 200 : 404 });
},
@@ -344,7 +375,9 @@ export class DataController extends Controller {
parameters: saveRepoQueryParams(),
tags: ["data"],
}),
permission(DataPermissions.entityRead, {}),
permission(DataPermissions.entityRead, {
context: (c) => ({ ...c.req.param() }) as any,
}),
jsc(
"param",
s.object({
@@ -361,9 +394,20 @@ export class DataController extends Controller {
}
const options = c.req.valid("query") as RepoQuery;
const result = await this.em
const { entity: newEntity } = this.em
.repository(entity)
.findManyByReference(id, reference, options);
.getEntityByReference(reference);
const { merge } = this.ctx.guard.filters(DataPermissions.entityRead, c, {
entity: newEntity.name,
id,
reference,
});
const result = await this.em.repository(entity).findManyByReference(id, reference, {
...options,
where: merge(options.where),
});
return c.json(result, { status: result.data ? 200 : 404 });
},
@@ -390,7 +434,9 @@ export class DataController extends Controller {
},
tags: ["data"],
}),
permission(DataPermissions.entityRead, {}),
permission(DataPermissions.entityRead, {
context: (c) => ({ entity: c.req.param("entity") }),
}),
mcpTool("data_entity_read_many", {
inputSchema: {
param: s.object({ entity: entitiesEnum }),
@@ -405,7 +451,13 @@ export class DataController extends Controller {
return this.notFound(c);
}
const options = c.req.valid("json") as RepoQuery;
const result = await this.em.repository(entity).findMany(options);
const { merge } = this.ctx.guard.filters(DataPermissions.entityRead, c, {
entity,
});
const result = await this.em.repository(entity).findMany({
...options,
where: merge(options.where),
});
return c.json(result, { status: result.data ? 200 : 404 });
},
@@ -421,7 +473,9 @@ export class DataController extends Controller {
summary: "Insert one or many",
tags: ["data"],
}),
permission(DataPermissions.entityCreate, {}),
permission(DataPermissions.entityCreate, {
context: (c) => ({ ...c.req.param() }) as any,
}),
mcpTool("data_entity_insert"),
jsc("param", s.object({ entity: entitiesEnum })),
jsc("json", s.anyOf([s.object({}), s.array(s.object({}))])),
@@ -438,6 +492,12 @@ export class DataController extends Controller {
// to transform all validation targets into a single object
const body = convertNumberedObjectToArray(_body);
this.ctx.guard
.filters(DataPermissions.entityCreate, c, {
entity,
})
.matches(body, { throwOnError: true });
if (Array.isArray(body)) {
const result = await this.em.mutator(entity).insertMany(body);
return c.json(result, 201);
@@ -455,7 +515,9 @@ export class DataController extends Controller {
summary: "Update many",
tags: ["data"],
}),
permission(DataPermissions.entityUpdate, {}),
permission(DataPermissions.entityUpdate, {
context: (c) => ({ ...c.req.param() }) as any,
}),
mcpTool("data_entity_update_many", {
inputSchema: {
param: s.object({ entity: entitiesEnum }),
@@ -482,7 +544,10 @@ export class DataController extends Controller {
update: EntityData;
where: RepoQuery["where"];
};
const result = await this.em.mutator(entity).updateWhere(update, where);
const { merge } = this.ctx.guard.filters(DataPermissions.entityUpdate, c, {
entity,
});
const result = await this.em.mutator(entity).updateWhere(update, merge(where));
return c.json(result);
},
@@ -495,7 +560,9 @@ export class DataController extends Controller {
summary: "Update one",
tags: ["data"],
}),
permission(DataPermissions.entityUpdate, {}),
permission(DataPermissions.entityUpdate, {
context: (c) => ({ ...c.req.param() }) as any,
}),
mcpTool("data_entity_update_one"),
jsc("param", s.object({ entity: entitiesEnum, id: idType })),
jsc("json", s.object({})),
@@ -505,6 +572,17 @@ export class DataController extends Controller {
return this.notFound(c);
}
const body = (await c.req.json()) as EntityData;
const fns = this.ctx.guard.filters(DataPermissions.entityUpdate, c, {
entity,
id,
});
// if it has filters attached, fetch entry and make the check
if (fns.filters.length > 0) {
const { data } = await this.em.repository(entity).findId(id);
fns.matches(data, { throwOnError: true });
}
const result = await this.em.mutator(entity).updateOne(id, body);
return c.json(result);
@@ -518,7 +596,9 @@ export class DataController extends Controller {
summary: "Delete one",
tags: ["data"],
}),
permission(DataPermissions.entityDelete, {}),
permission(DataPermissions.entityDelete, {
context: (c) => ({ ...c.req.param() }) as any,
}),
mcpTool("data_entity_delete_one"),
jsc("param", s.object({ entity: entitiesEnum, id: idType })),
async (c) => {
@@ -526,6 +606,18 @@ export class DataController extends Controller {
if (!this.entityExists(entity)) {
return this.notFound(c);
}
const fns = this.ctx.guard.filters(DataPermissions.entityDelete, c, {
entity,
id,
});
// if it has filters attached, fetch entry and make the check
if (fns.filters.length > 0) {
const { data } = await this.em.repository(entity).findId(id);
fns.matches(data, { throwOnError: true });
}
const result = await this.em.mutator(entity).deleteOne(id);
return c.json(result);
@@ -539,7 +631,9 @@ export class DataController extends Controller {
summary: "Delete many",
tags: ["data"],
}),
permission(DataPermissions.entityDelete, {}),
permission(DataPermissions.entityDelete, {
context: (c) => ({ ...c.req.param() }) as any,
}),
mcpTool("data_entity_delete_many", {
inputSchema: {
param: s.object({ entity: entitiesEnum }),
@@ -554,7 +648,10 @@ export class DataController extends Controller {
return this.notFound(c);
}
const where = (await c.req.json()) as RepoQuery["where"];
const result = await this.em.mutator(entity).deleteWhere(where);
const { merge } = this.ctx.guard.filters(DataPermissions.entityDelete, c, {
entity,
});
const result = await this.em.mutator(entity).deleteWhere(merge(where));
return c.json(result);
},