feat(authz): add PermissionStore contract with memory adapter and conformance suite
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import type { PermissionStore } from "../src/store.ts";
|
||||
|
||||
/**
|
||||
* Every PermissionStore adapter must pass this suite, so the memory and db
|
||||
* implementations cannot drift apart.
|
||||
*/
|
||||
export function runStoreConformance(name: string, makeStore: () => Promise<PermissionStore>): void {
|
||||
describe(`PermissionStore conformance: ${name}`, () => {
|
||||
let store: PermissionStore;
|
||||
beforeEach(async () => {
|
||||
store = await makeStore();
|
||||
});
|
||||
|
||||
test("an unknown subject has empty assignments", async () => {
|
||||
expect(await store.assignmentsFor("nobody")).toEqual({
|
||||
roles: [],
|
||||
grants: [],
|
||||
denies: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("assignRole then assignmentsFor round-trips", async () => {
|
||||
await store.assignRole("u1", "editor");
|
||||
expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]);
|
||||
});
|
||||
|
||||
test("assignRole is idempotent", async () => {
|
||||
await store.assignRole("u1", "editor");
|
||||
await store.assignRole("u1", "editor");
|
||||
expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]);
|
||||
});
|
||||
|
||||
test("revokeRole removes only that role", async () => {
|
||||
await store.assignRole("u1", "editor");
|
||||
await store.assignRole("u1", "admin");
|
||||
await store.revokeRole("u1", "editor");
|
||||
expect((await store.assignmentsFor("u1")).roles).toEqual(["admin"]);
|
||||
});
|
||||
|
||||
test("revoking a role that was never assigned is a no-op", async () => {
|
||||
await store.revokeRole("u1", "ghost");
|
||||
expect((await store.assignmentsFor("u1")).roles).toEqual([]);
|
||||
});
|
||||
|
||||
test("scoped assignments do not leak across tenants", async () => {
|
||||
await store.assignRole("u1", "editor", { tenantId: "t1" });
|
||||
expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles).toEqual(["editor"]);
|
||||
expect((await store.assignmentsFor("u1", { tenantId: "t2" })).roles).toEqual([]);
|
||||
});
|
||||
|
||||
test("a global assignment is visible inside every tenant", async () => {
|
||||
await store.assignRole("u1", "superadmin");
|
||||
expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles).toEqual(["superadmin"]);
|
||||
});
|
||||
|
||||
test("global and scoped roles union within a tenant", async () => {
|
||||
await store.assignRole("u1", "viewer");
|
||||
await store.assignRole("u1", "editor", { tenantId: "t1" });
|
||||
expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles.sort()).toEqual([
|
||||
"editor",
|
||||
"viewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("grant with allow and deny land in the right buckets", async () => {
|
||||
await store.grant("u1", "post:write", "allow");
|
||||
await store.grant("u1", "post:delete", "deny");
|
||||
const assignments = await store.assignmentsFor("u1");
|
||||
expect(assignments.grants).toEqual(["post:write"]);
|
||||
expect(assignments.denies).toEqual(["post:delete"]);
|
||||
});
|
||||
|
||||
test("re-granting the same permission replaces its effect", async () => {
|
||||
await store.grant("u1", "post:write", "allow");
|
||||
await store.grant("u1", "post:write", "deny");
|
||||
const assignments = await store.assignmentsFor("u1");
|
||||
expect(assignments.grants).toEqual([]);
|
||||
expect(assignments.denies).toEqual(["post:write"]);
|
||||
});
|
||||
|
||||
test("revokeGrant removes the permission entirely", async () => {
|
||||
await store.grant("u1", "post:write", "allow");
|
||||
await store.revokeGrant("u1", "post:write");
|
||||
expect((await store.assignmentsFor("u1")).grants).toEqual([]);
|
||||
});
|
||||
|
||||
test("listSubjects returns everyone with an assignment in scope", async () => {
|
||||
await store.assignRole("u1", "editor", { tenantId: "t1" });
|
||||
await store.assignRole("u2", "editor", { tenantId: "t1" });
|
||||
await store.assignRole("u3", "editor", { tenantId: "t2" });
|
||||
expect((await store.listSubjects({ tenantId: "t1" })).sort()).toEqual(["u1", "u2"]);
|
||||
});
|
||||
|
||||
test("listSubjects with no scope returns global assignees only", async () => {
|
||||
await store.assignRole("g1", "viewer");
|
||||
await store.assignRole("s1", "editor", { tenantId: "t1" });
|
||||
expect(await store.listSubjects()).toEqual(["g1"]);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { memoryPermissionStore } from "../src/store.ts";
|
||||
import { runStoreConformance } from "./store-conformance.ts";
|
||||
|
||||
runStoreConformance("memory", async () => memoryPermissionStore());
|
||||
Reference in New Issue
Block a user