mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
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:
@@ -152,12 +152,12 @@ describe("authorize", () => {
|
||||
expect(() => guard.granted(read, { role: "member", enabled: false })).toThrow();
|
||||
|
||||
// get the filter for member role
|
||||
expect(guard.getPolicyFilter(read, { role: "member" })).toEqual({
|
||||
expect(guard.filters(read, { role: "member" }).filter).toEqual({
|
||||
type: "member",
|
||||
});
|
||||
|
||||
// get filter for guest
|
||||
expect(guard.getPolicyFilter(read, {})).toBeUndefined();
|
||||
expect(guard.filters(read, {}).filter).toBeUndefined();
|
||||
});
|
||||
|
||||
test("guest should only read posts that are public", () => {
|
||||
@@ -226,7 +226,7 @@ describe("authorize", () => {
|
||||
expect(() => guard.granted(read, {}, { entity: "users" })).toThrow();
|
||||
|
||||
// and guests can only read public posts
|
||||
expect(guard.getPolicyFilter(read, {}, { entity: "posts" })).toEqual({
|
||||
expect(guard.filters(read, {}, { entity: "posts" }).filter).toEqual({
|
||||
public: true,
|
||||
});
|
||||
|
||||
@@ -236,7 +236,7 @@ describe("authorize", () => {
|
||||
|
||||
// member should not have a filter
|
||||
expect(
|
||||
guard.getPolicyFilter(read, { role: "member" }, { entity: "posts" }),
|
||||
guard.filters(read, { role: "member" }, { entity: "posts" }).filter,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
327
app/__test__/auth/authorize/data.permissions.test.ts
Normal file
327
app/__test__/auth/authorize/data.permissions.test.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
||||
import { createApp } from "core/test/utils";
|
||||
import type { CreateAppConfig } from "App";
|
||||
import * as proto from "data/prototype";
|
||||
import { mergeObject } from "core/utils/objects";
|
||||
import type { App, DB } from "bknd";
|
||||
import type { CreateUserPayload } from "auth/AppAuth";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(() => enableConsoleLog());
|
||||
|
||||
async function makeApp(config: Partial<CreateAppConfig["config"]> = {}) {
|
||||
const app = createApp({
|
||||
config: mergeObject(
|
||||
{
|
||||
data: proto
|
||||
.em(
|
||||
{
|
||||
users: proto.systemEntity("users", {}),
|
||||
posts: proto.entity("posts", {
|
||||
title: proto.text(),
|
||||
content: proto.text(),
|
||||
}),
|
||||
comments: proto.entity("comments", {
|
||||
content: proto.text(),
|
||||
}),
|
||||
},
|
||||
({ relation }, { users, posts, comments }) => {
|
||||
relation(posts).manyToOne(users);
|
||||
relation(comments).manyToOne(posts);
|
||||
},
|
||||
)
|
||||
.toJSON(),
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
config,
|
||||
),
|
||||
});
|
||||
await app.build();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
async function createUsers(app: App, users: CreateUserPayload[]) {
|
||||
return Promise.all(
|
||||
users.map(async (user) => {
|
||||
return await app.createUser(user);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function loadFixtures(app: App, fixtures: Record<string, any[]> = {}) {
|
||||
const results = {} as any;
|
||||
for (const [entity, data] of Object.entries(fixtures)) {
|
||||
results[entity] = await app.em
|
||||
.mutator(entity as any)
|
||||
.insertMany(data)
|
||||
.then((result) => result.data);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
describe("data permissions", async () => {
|
||||
const app = await makeApp({
|
||||
server: {
|
||||
mcp: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
guard: {
|
||||
enabled: true,
|
||||
},
|
||||
roles: {
|
||||
guest: {
|
||||
is_default: true,
|
||||
permissions: [
|
||||
{
|
||||
permission: "system.access.api",
|
||||
},
|
||||
{
|
||||
permission: "data.entity.read",
|
||||
policies: [
|
||||
{
|
||||
condition: {
|
||||
entity: "posts",
|
||||
},
|
||||
effect: "filter",
|
||||
filter: {
|
||||
users_id: { $isnull: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
permission: "data.entity.create",
|
||||
policies: [
|
||||
{
|
||||
condition: {
|
||||
entity: "posts",
|
||||
},
|
||||
effect: "filter",
|
||||
filter: {
|
||||
users_id: { $isnull: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
permission: "data.entity.update",
|
||||
policies: [
|
||||
{
|
||||
condition: {
|
||||
entity: "posts",
|
||||
},
|
||||
effect: "filter",
|
||||
filter: {
|
||||
users_id: { $isnull: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
permission: "data.entity.delete",
|
||||
policies: [
|
||||
{
|
||||
condition: { entity: "posts" },
|
||||
},
|
||||
{
|
||||
condition: { entity: "posts" },
|
||||
effect: "filter",
|
||||
filter: {
|
||||
users_id: { $isnull: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const users = [
|
||||
{ email: "foo@example.com", password: "password" },
|
||||
{ email: "bar@example.com", password: "password" },
|
||||
];
|
||||
const fixtures = {
|
||||
posts: [
|
||||
{ content: "post 1", users_id: 1 },
|
||||
{ content: "post 2", users_id: 2 },
|
||||
{ content: "post 3", users_id: null },
|
||||
],
|
||||
comments: [
|
||||
{ content: "comment 1", posts_id: 1 },
|
||||
{ content: "comment 2", posts_id: 2 },
|
||||
{ content: "comment 3", posts_id: 3 },
|
||||
],
|
||||
};
|
||||
await createUsers(app, users);
|
||||
const results = await loadFixtures(app, fixtures);
|
||||
|
||||
describe("http", async () => {
|
||||
it("read many", async () => {
|
||||
// many only includes posts with users_id is null
|
||||
const res = await app.server.request("/api/data/entity/posts");
|
||||
const data = await res.json().then((r: any) => r.data);
|
||||
expect(data).toEqual([results.posts[2]]);
|
||||
|
||||
// same with /query
|
||||
{
|
||||
const res = await app.server.request("/api/data/entity/posts/query", {
|
||||
method: "POST",
|
||||
});
|
||||
const data = await res.json().then((r: any) => r.data);
|
||||
expect(data).toEqual([results.posts[2]]);
|
||||
}
|
||||
});
|
||||
|
||||
it("read one", async () => {
|
||||
// one only includes posts with users_id is null
|
||||
{
|
||||
const res = await app.server.request("/api/data/entity/posts/1");
|
||||
const data = await res.json().then((r: any) => r.data);
|
||||
expect(res.status).toBe(404);
|
||||
expect(data).toBeUndefined();
|
||||
}
|
||||
|
||||
// read one by allowed id
|
||||
{
|
||||
const res = await app.server.request("/api/data/entity/posts/3");
|
||||
const data = await res.json().then((r: any) => r.data);
|
||||
expect(res.status).toBe(200);
|
||||
expect(data).toEqual(results.posts[2]);
|
||||
}
|
||||
});
|
||||
|
||||
it("read many by reference", async () => {
|
||||
const res = await app.server.request("/api/data/entity/posts/1/comments");
|
||||
const data = await res.json().then((r: any) => r.data);
|
||||
expect(res.status).toBe(200);
|
||||
expect(data).toEqual(results.comments.filter((c: any) => c.posts_id === 1));
|
||||
});
|
||||
|
||||
it("mutation create one", async () => {
|
||||
// not allowed
|
||||
{
|
||||
const res = await app.server.request("/api/data/entity/posts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "post 4" }),
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
}
|
||||
// allowed
|
||||
{
|
||||
const res = await app.server.request("/api/data/entity/posts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "post 4", users_id: null }),
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
}
|
||||
});
|
||||
|
||||
it("mutation update one", async () => {
|
||||
// update one: not allowed
|
||||
const res = await app.server.request("/api/data/entity/posts/1", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ content: "post 4" }),
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
|
||||
{
|
||||
// update one: allowed
|
||||
const res = await app.server.request("/api/data/entity/posts/3", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ content: "post 3 (updated)" }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json().then((r: any) => r.data.content)).toBe("post 3 (updated)");
|
||||
}
|
||||
});
|
||||
|
||||
it("mutation update many", async () => {
|
||||
// update many: not allowed
|
||||
const res = await app.server.request("/api/data/entity/posts", {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
update: { content: "post 4" },
|
||||
where: { users_id: { $isnull: 0 } },
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200); // because filtered
|
||||
const _data = await res.json().then((r: any) => r.data.map((p: any) => p.users_id));
|
||||
expect(_data.every((u: any) => u === null)).toBe(true);
|
||||
|
||||
// verify
|
||||
const data = await app.em
|
||||
.repo("posts")
|
||||
.findMany({ select: ["content", "users_id"] })
|
||||
.then((r) => r.data);
|
||||
|
||||
// expect non null users_id to not have content "post 4"
|
||||
expect(
|
||||
data.filter((p: any) => p.users_id !== null).every((p: any) => p.content !== "post 4"),
|
||||
).toBe(true);
|
||||
// expect null users_id to have content "post 4"
|
||||
expect(
|
||||
data.filter((p: any) => p.users_id === null).every((p: any) => p.content === "post 4"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
const count = async () => {
|
||||
const {
|
||||
data: { count: _count },
|
||||
} = await app.em.repo("posts").count();
|
||||
return _count;
|
||||
};
|
||||
it("mutation delete one", async () => {
|
||||
const initial = await count();
|
||||
|
||||
// delete one: not allowed
|
||||
const res = await app.server.request("/api/data/entity/posts/1", {
|
||||
method: "DELETE",
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
expect(await count()).toBe(initial);
|
||||
|
||||
{
|
||||
// delete one: allowed
|
||||
const res = await app.server.request("/api/data/entity/posts/3", {
|
||||
method: "DELETE",
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(await count()).toBe(initial - 1);
|
||||
}
|
||||
});
|
||||
|
||||
it("mutation delete many", async () => {
|
||||
// delete many: not allowed
|
||||
const res = await app.server.request("/api/data/entity/posts", {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
where: {},
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// only deleted posts with users_id is null
|
||||
const remaining = await app.em
|
||||
.repo("posts")
|
||||
.findMany()
|
||||
.then((r) => r.data);
|
||||
expect(remaining.every((p: any) => p.users_id !== null)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ async function makeApp(config: Partial<CreateAppConfig> = {}) {
|
||||
return app;
|
||||
}
|
||||
|
||||
describe("SystemController", () => {
|
||||
describe.skip("SystemController", () => {
|
||||
it("...", async () => {
|
||||
const app = await makeApp();
|
||||
const controller = new SystemController(app);
|
||||
@@ -122,26 +122,10 @@ describe("Guard", () => {
|
||||
const guard = new Guard([p], [r], {
|
||||
enabled: true,
|
||||
});
|
||||
expect(
|
||||
guard.getPolicyFilter(
|
||||
p,
|
||||
{
|
||||
role: r.name,
|
||||
},
|
||||
{ a: 1 },
|
||||
),
|
||||
).toEqual({ foo: "bar" });
|
||||
expect(
|
||||
guard.getPolicyFilter(
|
||||
p,
|
||||
{
|
||||
role: r.name,
|
||||
},
|
||||
{ a: 2 },
|
||||
),
|
||||
).toBeUndefined();
|
||||
expect(guard.filters(p, { role: r.name }, { a: 1 }).filter).toEqual({ foo: "bar" });
|
||||
expect(guard.filters(p, { role: r.name }, { a: 2 }).filter).toBeUndefined();
|
||||
// if no user context given, filter cannot be applied
|
||||
expect(guard.getPolicyFilter(p, {}, { a: 1 })).toBeUndefined();
|
||||
expect(guard.filters(p, {}, { a: 1 }).filter).toBeUndefined();
|
||||
});
|
||||
|
||||
it("collects filters for default role", () => {
|
||||
@@ -172,26 +156,26 @@ describe("Guard", () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
guard.getPolicyFilter(
|
||||
guard.filters(
|
||||
p,
|
||||
{
|
||||
role: r.name,
|
||||
},
|
||||
{ a: 1 },
|
||||
),
|
||||
).filter,
|
||||
).toEqual({ foo: "bar" });
|
||||
expect(
|
||||
guard.getPolicyFilter(
|
||||
guard.filters(
|
||||
p,
|
||||
{
|
||||
role: r.name,
|
||||
},
|
||||
{ a: 2 },
|
||||
),
|
||||
).filter,
|
||||
).toBeUndefined();
|
||||
// if no user context given, the default role is applied
|
||||
// hence it can be found
|
||||
expect(guard.getPolicyFilter(p, {}, { a: 1 })).toEqual({ foo: "bar" });
|
||||
expect(guard.filters(p, {}, { a: 1 }).filter).toEqual({ foo: "bar" });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -272,6 +272,7 @@ describe("Core Utils", async () => {
|
||||
},
|
||||
/^@([a-z\.]+)$/,
|
||||
variables7,
|
||||
null,
|
||||
);
|
||||
expect(result7).toEqual({
|
||||
number: 123,
|
||||
@@ -288,20 +289,85 @@ describe("Core Utils", async () => {
|
||||
);
|
||||
expect(result8).toEqual({ message: "The value is 123!" });
|
||||
|
||||
// test mixed scenarios
|
||||
// test with fallback parameter
|
||||
const obj9 = { user: "@user.id", config: "@config.theme" };
|
||||
const variables9 = {}; // empty context
|
||||
const result9 = utils.recursivelyReplacePlaceholders(
|
||||
{
|
||||
fullMatch: "@test.value", // should preserve number type
|
||||
partialMatch: "Value: @test.value", // should convert to string
|
||||
noMatch: "static text",
|
||||
},
|
||||
obj9,
|
||||
/^@([a-z\.]+)$/,
|
||||
variables7,
|
||||
variables9,
|
||||
null,
|
||||
);
|
||||
expect(result9).toEqual({
|
||||
fullMatch: 123, // number preserved
|
||||
partialMatch: "Value: @test.value", // no replacement (pattern requires full match)
|
||||
noMatch: "static text",
|
||||
expect(result9).toEqual({ user: null, config: null });
|
||||
|
||||
// test with fallback for partial matches
|
||||
const obj10 = { message: "Hello @user.name, welcome!" };
|
||||
const variables10 = {}; // empty context
|
||||
const result10 = utils.recursivelyReplacePlaceholders(
|
||||
obj10,
|
||||
/@([a-z\.]+)/g,
|
||||
variables10,
|
||||
"Guest",
|
||||
);
|
||||
expect(result10).toEqual({ message: "Hello Guest, welcome!" });
|
||||
|
||||
// test with different fallback types
|
||||
const obj11 = {
|
||||
stringFallback: "@missing.string",
|
||||
numberFallback: "@missing.number",
|
||||
booleanFallback: "@missing.boolean",
|
||||
objectFallback: "@missing.object",
|
||||
};
|
||||
const variables11 = {};
|
||||
const result11 = utils.recursivelyReplacePlaceholders(
|
||||
obj11,
|
||||
/^@([a-z\.]+)$/,
|
||||
variables11,
|
||||
"default",
|
||||
);
|
||||
expect(result11).toEqual({
|
||||
stringFallback: "default",
|
||||
numberFallback: "default",
|
||||
booleanFallback: "default",
|
||||
objectFallback: "default",
|
||||
});
|
||||
|
||||
// test fallback with arrays
|
||||
const obj12 = { items: ["@item1", "@item2", "static"] };
|
||||
const variables12 = { item1: "found" }; // item2 is missing
|
||||
const result12 = utils.recursivelyReplacePlaceholders(
|
||||
obj12,
|
||||
/^@([a-zA-Z0-9\.]+)$/,
|
||||
variables12,
|
||||
"missing",
|
||||
);
|
||||
expect(result12).toEqual({ items: ["found", "missing", "static"] });
|
||||
|
||||
// test fallback with nested objects
|
||||
const obj13 = {
|
||||
user: "@user.id",
|
||||
settings: {
|
||||
theme: "@theme.name",
|
||||
nested: {
|
||||
value: "@deep.value",
|
||||
},
|
||||
},
|
||||
};
|
||||
const variables13 = {}; // empty context
|
||||
const result13 = utils.recursivelyReplacePlaceholders(
|
||||
obj13,
|
||||
/^@([a-z\.]+)$/,
|
||||
variables13,
|
||||
null,
|
||||
);
|
||||
expect(result13).toEqual({
|
||||
user: null,
|
||||
settings: {
|
||||
theme: null,
|
||||
nested: {
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,9 +30,9 @@ describe("some tests", async () => {
|
||||
const query = await em.repository(users).findId(1);
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? order by "users"."id" asc limit ? offset ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([1, 1]);
|
||||
expect(query.parameters).toEqual([1, 1, 0]);
|
||||
expect(query.data).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user