import { describe, it, expect } from "bun:test"; import { s } from "bknd/utils"; import { Permission } from "auth/authorize/Permission"; import { Policy } from "auth/authorize/Policy"; import { Hono } from "hono"; import { getPermissionRoutes, permission } from "auth/middlewares/permission.middleware"; import { auth } from "auth/middlewares/auth.middleware"; import { Guard, mergeFilters, type GuardConfig } from "auth/authorize/Guard"; import { Role, RolePermission } from "auth/authorize/Role"; import { Exception } from "bknd"; import { convert } from "core/object/query/object-query"; describe("Permission", () => { it("works with minimal schema", () => { expect(() => new Permission("test")).not.toThrow(); }); it("parses context", () => { const p = new Permission( "test3", { filterable: true, }, s.object({ a: s.string(), }), ); // @ts-expect-error expect(() => p.parseContext({ a: [] })).toThrow(); expect(p.parseContext({ a: "test" })).toEqual({ a: "test" }); // @ts-expect-error expect(p.parseContext({ a: 1 })).toEqual({ a: "1" }); }); }); describe("Policy", () => { it("works with minimal schema", () => { expect(() => new Policy().toJSON()).not.toThrow(); }); it("checks condition", () => { const p = new Policy({ condition: { a: 1, }, }); expect(p.meetsCondition({ a: 1 })).toBe(true); expect(p.meetsCondition({ a: 2 })).toBe(false); expect(p.meetsCondition({ a: 1, b: 1 })).toBe(true); expect(p.meetsCondition({})).toBe(false); const p2 = new Policy({ condition: { a: { $gt: 1 }, $or: { b: { $lt: 2 }, }, }, }); expect(p2.meetsCondition({ a: 2 })).toBe(true); expect(p2.meetsCondition({ a: 1 })).toBe(false); expect(p2.meetsCondition({ a: 1, b: 1 })).toBe(true); }); it("filters", () => { const p = new Policy({ filter: { age: { $gt: 18 }, }, }); const subjects = [{ age: 19 }, { age: 17 }, { age: 12 }]; expect(p.getFiltered(subjects)).toEqual([{ age: 19 }]); expect(p.meetsFilter({ age: 19 })).toBe(true); expect(p.meetsFilter({ age: 17 })).toBe(false); expect(p.meetsFilter({ age: 12 })).toBe(false); }); it("replaces placeholders", () => { const p = new Policy({ condition: { a: "@auth.username", }, filter: { a: "@auth.username", }, }); const vars = { auth: { username: "test" } }; expect(p.meetsCondition({ a: "test" }, vars)).toBe(true); expect(p.meetsCondition({ a: "test2" }, vars)).toBe(false); expect(p.meetsCondition({ a: "test2" })).toBe(false); expect(p.meetsFilter({ a: "test" }, vars)).toBe(true); expect(p.meetsFilter({ a: "test2" }, vars)).toBe(false); expect(p.meetsFilter({ a: "test2" })).toBe(false); }); }); describe("Guard", () => { it("collects filters", () => { const p = new Permission( "test", { filterable: true, }, s.object({ a: s.number(), }), ); const r = new Role("test", [ new RolePermission(p, [ new Policy({ condition: { a: { $eq: 1 } }, filter: { foo: "bar" }, effect: "filter", }), ]), ]); const guard = new Guard([p], [r], { enabled: true, }); 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.filters(p, {}, { a: 1 }).filter).toBeUndefined(); }); it("collects filters for default role", () => { const p = new Permission( "test", { filterable: true, }, s.object({ a: s.number(), }), ); const r = new Role( "test", [ new RolePermission(p, [ new Policy({ condition: { a: { $eq: 1 } }, filter: { foo: "bar" }, effect: "filter", }), ]), ], true, ); const guard = new Guard([p], [r], { enabled: true, }); 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, the default role is applied // hence it can be found expect(guard.filters(p, {}, { a: 1 }).filter).toEqual({ foo: "bar" }); }); it("merges filters correctly", () => { expect(mergeFilters({ foo: "bar" }, { baz: "qux" })).toEqual({ foo: { $eq: "bar" }, baz: { $eq: "qux" }, }); expect(mergeFilters({ foo: "bar" }, { baz: { $eq: "qux" } })).toEqual({ foo: { $eq: "bar" }, baz: { $eq: "qux" }, }); expect(mergeFilters({ foo: "bar" }, { foo: "baz" })).toEqual({ foo: { $eq: "baz" } }); expect(mergeFilters({ foo: "bar" }, { foo: { $lt: 1 } })).toEqual({ foo: { $eq: "bar", $lt: 1 }, }); // overwrite base $or with priority expect(mergeFilters({ $or: { foo: "one" } }, { foo: "bar" })).toEqual({ $or: { foo: { $eq: "bar", }, }, foo: { $eq: "bar", }, }); // ignore base $or if priority has different key expect(mergeFilters({ $or: { other: "one" } }, { foo: "bar" })).toEqual({ $or: { other: { $eq: "one", }, }, foo: { $eq: "bar", }, }); }); }); describe("permission middleware", () => { const makeApp = ( permissions: Permission[], roles: Role[] = [], config: Partial = {}, ) => { const app = { module: { auth: { enabled: true, }, }, modules: { ctx: () => ({ guard: new Guard(permissions, roles, { enabled: true, ...config, }), }), }, }; return new Hono() .use(async (c, next) => { // @ts-expect-error c.set("app", app); await next(); }) .use(auth()) .onError((err, c) => { if (err instanceof Exception) { return c.json(err.toJSON(), err.code as any); } return c.json({ error: err.message }, "code" in err ? (err.code as any) : 500); }); }; it("allows if guard is disabled", async () => { const p = new Permission("test"); const hono = makeApp([p], [], { enabled: false }).get("/test", permission(p, {}), async (c) => c.text("test"), ); const res = await hono.request("/test"); expect(res.status).toBe(200); expect(await res.text()).toBe("test"); }); it("denies if guard is enabled", async () => { const p = new Permission("test"); const hono = makeApp([p]).get("/test", permission(p, {}), async (c) => c.text("test")); const res = await hono.request("/test"); expect(res.status).toBe(403); }); it("allows if user has (plain) role", async () => { const p = new Permission("test"); const r = Role.create("test", { permissions: [p.name] }); const hono = makeApp([p], [r]) .use(async (c, next) => { // @ts-expect-error c.set("auth", { registered: true, user: { id: 0, role: r.name } }); await next(); }) .get("/test", permission(p, {}), async (c) => c.text("test")); const res = await hono.request("/test"); expect(res.status).toBe(200); }); it("allows if user has role with policy", async () => { const p = new Permission("test"); const r = new Role("test", [ new RolePermission(p, [ new Policy({ condition: { a: { $gte: 1 }, }, }), ]), ]); const hono = makeApp([p], [r], { context: { a: 1, }, }) .use(async (c, next) => { // @ts-expect-error c.set("auth", { registered: true, user: { id: 0, role: r.name } }); await next(); }) .get("/test", permission(p, {}), async (c) => c.text("test")); const res = await hono.request("/test"); expect(res.status).toBe(200); }); it("denies if user with role doesn't meet condition", async () => { const p = new Permission("test"); const r = new Role("test", [ new RolePermission( p, [ new Policy({ condition: { a: { $lt: 1 }, }, // default effect is allow }), ], // change default effect to deny if no condition is met "deny", ), ]); const hono = makeApp([p], [r], { context: { a: 1, }, }) .use(async (c, next) => { // @ts-expect-error c.set("auth", { registered: true, user: { id: 0, role: r.name } }); await next(); }) .get("/test", permission(p, {}), async (c) => c.text("test")); const res = await hono.request("/test"); expect(res.status).toBe(403); }); it("allows if user with role doesn't meet condition (from middleware)", async () => { const p = new Permission( "test", {}, s.object({ a: s.number(), }), ); const r = new Role("test", [ new RolePermission(p, [ new Policy({ condition: { a: { $eq: 1 }, }, }), ]), ]); const hono = makeApp([p], [r]) .use(async (c, next) => { // @ts-expect-error c.set("auth", { registered: true, user: { id: 0, role: r.name } }); await next(); }) .get( "/test", permission(p, { context: (c) => ({ a: 1, }), }), async (c) => c.text("test"), ); const res = await hono.request("/test"); expect(res.status).toBe(200); }); it("throws if permission context is invalid", async () => { const p = new Permission( "test", {}, s.object({ a: s.number({ minimum: 2 }), }), ); const r = new Role("test", [ new RolePermission(p, [ new Policy({ condition: { a: { $eq: 1 }, }, }), ]), ]); const hono = makeApp([p], [r]) .use(async (c, next) => { // @ts-expect-error c.set("auth", { registered: true, user: { id: 0, role: r.name } }); await next(); }) .get( "/test", permission(p, { context: (c) => ({ a: 1, }), }), async (c) => c.text("test"), ); const res = await hono.request("/test"); // expecting 500 because bknd should have handled it correctly expect(res.status).toBe(500); }); it("checks context on routes with permissions", async () => { const make = (user: any) => { const p = new Permission( "test", {}, s.object({ a: s.number(), }), ); const r = new Role("test", [ new RolePermission(p, [ new Policy({ condition: { a: { $eq: 1 }, }, }), ]), ]); return makeApp([p], [r]) .use(async (c, next) => { // @ts-expect-error c.set("auth", { registered: true, user }); await next(); }) .get( "/valid", permission(p, { context: (c) => ({ a: 1, }), }), async (c) => c.text("test"), ) .get( "/invalid", permission(p, { // @ts-expect-error context: (c) => ({ b: "1", }), }), async (c) => c.text("test"), ) .get( "/invalid2", permission(p, { // @ts-expect-error context: (c) => ({}), }), async (c) => c.text("test"), ) .get( "/invalid3", // @ts-expect-error permission(p), async (c) => c.text("test"), ); }; const hono = make({ id: 0, role: "test" }); const valid = await hono.request("/valid"); expect(valid.status).toBe(200); const invalid = await hono.request("/invalid"); expect(invalid.status).toBe(500); const invalid2 = await hono.request("/invalid2"); expect(invalid2.status).toBe(500); const invalid3 = await hono.request("/invalid3"); expect(invalid3.status).toBe(500); { const hono = make(null); const valid = await hono.request("/valid"); expect(valid.status).toBe(403); const invalid = await hono.request("/invalid"); expect(invalid.status).toBe(500); const invalid2 = await hono.request("/invalid2"); expect(invalid2.status).toBe(500); const invalid3 = await hono.request("/invalid3"); expect(invalid3.status).toBe(500); } }); }); describe("Role", () => { it("serializes and deserializes", () => { const p = new Permission( "test", { filterable: true, }, s.object({ a: s.number({ minimum: 2 }), }), ); const r = new Role( "test", [ new RolePermission(p, [ new Policy({ condition: { a: { $eq: 1 }, }, effect: "deny", filter: { b: { $lt: 1 }, }, }), ]), ], true, ); const json = JSON.parse(JSON.stringify(r.toJSON())); const r2 = Role.create(p.name, json); expect(r2.toJSON()).toEqual(r.toJSON()); }); });