Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { MemoryAdapter } from "../src/adapters/memory/memory.js";
import { PermissionCache } from "../src/cache/permission-cache/permission-cache.js";
import { type Ability, buildAbility } from "../src/core/ability/ability.js";
import { PermissionManager } from "../src/core/permission-manager/permission-manager.js";
import { PolicyEngine } from "../src/core/policy-engine/policy-engine.js";
import { RoleManager } from "../src/core/role-manager/role-manager.js";
import { CircularInheritanceError } from "../src/errors/permission-error.js";
import {
createAdminRole,
createViewerRole,
} from "../src/presets/role-presets/role-presets.js";
import type { Permission } from "../src/types/permissions.types.js";
function makePerm(id: string, resource = "posts", action = "read"): Permission {
return { id, resource, action };
}
describe("Full permissions flow", () => {
it("role inheritance transitivity", { timeout: 5000 }, async () => {
const adapter = new MemoryAdapter();
const roleManager = new RoleManager(adapter);
const _manager = new PermissionManager(adapter);
const cPerm = makePerm("p-c", "comments", "read");
const bPerm = makePerm("p-b", "posts", "write");
const aPerm = makePerm("p-a", "posts", "read");
await adapter.createRole({ id: "c", name: "C", permissions: [cPerm] });
await adapter.createRole({
id: "b",
name: "B",
permissions: [bPerm],
inherits: ["c"],
});
await adapter.createRole({
id: "a",
name: "A",
permissions: [aPerm],
inherits: ["b"],
});
const perms = await roleManager.getEffectivePermissions("a");
const permIds = perms.map((p) => p.id);
expect(permIds).toContain("p-a");
expect(permIds).toContain("p-b");
expect(permIds).toContain("p-c");
});
it("policy engine with cache", { timeout: 5000 }, async () => {
const adapter = new MemoryAdapter();
await adapter.createRole({
id: "r1",
name: "R",
permissions: [makePerm("p1")],
});
await adapter.assignRole({
id: "ur1",
userId: "u1",
roleId: "r1",
assignedAt: new Date(),
});
const manager = new PermissionManager(adapter);
const cache = new PermissionCache({ ttl: 60_000 });
const engine = new PolicyEngine(manager, { cache });
const r1 = await engine.evaluate("u1", "posts", "read");
const r2 = await engine.evaluate("u1", "posts", "read");
expect(r1.allowed).toBe(true);
expect(r2.matchedRule).toBe("cached-allow");
// Invalidate cache and re-check
cache.invalidate("u1");
const r3 = await engine.evaluate("u1", "posts", "read");
expect(r3.allowed).toBe(true);
expect(r3.matchedRule).toBe("posts:read");
});
it("circular inheritance detection", { timeout: 5000 }, async () => {
const adapter = new MemoryAdapter();
const roleManager = new RoleManager(adapter);
await adapter.createRole({
id: "x",
name: "X",
permissions: [],
inherits: ["y"],
});
await adapter.createRole({
id: "y",
name: "Y",
permissions: [],
inherits: ["x"],
});
await expect(
roleManager.getRoleInheritanceChain("x")
).rejects.toBeInstanceOf(CircularInheritanceError);
});
it("preset roles work with PermissionManager", {
timeout: 5000,
}, async () => {
const adapter = new MemoryAdapter();
await adapter.createRole(createAdminRole());
await adapter.assignRole({
id: "ur-admin",
userId: "admin-user",
roleId: "admin",
assignedAt: new Date(),
});
await adapter.createRole(createViewerRole());
await adapter.assignRole({
id: "ur-viewer",
userId: "viewer-user",
roleId: "viewer",
assignedAt: new Date(),
});
const manager = new PermissionManager(adapter);
expect(await manager.hasPermission("admin-user", "any", "any")).toBe(
true
);
expect(
await manager.hasPermission("viewer-user", "posts", "read")
).toBe(true);
expect(
await manager.hasPermission("viewer-user", "posts", "delete")
).toBe(false);
});
});
describe("Ability system end-to-end", () => {
it("can/cannot consistency", { timeout: 5000 }, () => {
const ability: Ability = buildAbility((b) => {
b.can("read", "posts");
b.cannot("delete", "posts");
});
expect(ability.can("read", "posts")).toBe(true);
expect(ability.cannot("read", "posts")).toBe(false);
expect(ability.can("delete", "posts")).toBe(false);
expect(ability.cannot("delete", "posts")).toBe(true);
});
});

View File

@@ -0,0 +1,109 @@
import { describe, it } from "@suaveplan/testing/runner";
import * as fc from "fast-check";
import { PermissionCache } from "../src/cache/permission-cache/permission-cache.js";
import { compareValues } from "../src/conditions/condition-matcher/condition-matcher.js";
import { buildAbility } from "../src/core/ability/ability.js";
import type { ConditionOperator } from "../src/types/permissions.types.js";
describe("Property: Ability can/cannot consistency", () => {
it("cannot is always negation of can for any action/resource", {
timeout: 5000,
}, () => {
fc.assert(
fc.property(
fc.string({ minLength: 1, maxLength: 16 }),
fc.string({ minLength: 1, maxLength: 16 }),
fc.array(fc.string({ minLength: 1, maxLength: 16 }), {
minLength: 0,
maxLength: 5,
}),
(action, resource, grantedActions) => {
const ability = buildAbility((b) => {
for (const a of grantedActions) {
b.can(a, resource);
}
});
const canResult = ability.can(action, resource);
const cannotResult = ability.cannot(action, resource);
return canResult !== cannotResult;
}
),
{ numRuns: 200 }
);
});
});
describe("Property: Cache invalidation completeness", () => {
it("after invalidate(userId), no stale entry for that user", {
timeout: 5000,
}, () => {
fc.assert(
fc.property(
fc.string({ minLength: 1, maxLength: 16 }),
fc.array(fc.string({ minLength: 1, maxLength: 16 }), {
minLength: 1,
maxLength: 10,
}),
(userId, resources) => {
const cache = new PermissionCache({ ttl: 99999 });
for (const r of resources) {
cache.set(`${userId}:${r}:read`, true);
}
cache.invalidate(userId);
return resources.every(
(r) => cache.get(`${userId}:${r}:read`) === undefined
);
}
),
{ numRuns: 100 }
);
});
});
describe("Property: compareValues determinism", () => {
it("same inputs always produce same result", { timeout: 5000 }, () => {
const ops: ConditionOperator[] = [
"eq",
"neq",
"contains",
"startsWith",
];
fc.assert(
fc.property(
fc.string({ minLength: 0, maxLength: 32 }),
fc.string({ minLength: 0, maxLength: 32 }),
fc.constantFrom(...ops),
(a, b, op) => {
const r1 = compareValues(a, b, op);
const r2 = compareValues(a, b, op);
return r1 === r2;
}
),
{ numRuns: 300 }
);
});
});
describe("Property: Role inheritance transitivity via Ability", () => {
it("abilities compose additively across rule sets", {
timeout: 5000,
}, () => {
fc.assert(
fc.property(
fc.array(fc.string({ minLength: 1, maxLength: 8 }), {
minLength: 1,
maxLength: 5,
}),
fc.string({ minLength: 1, maxLength: 8 }),
(actions, resource) => {
const ability = buildAbility((b) => {
for (const a of actions) b.can(a, resource);
});
// All granted actions must return can=true
return actions.every((a) => ability.can(a, resource));
}
),
{ numRuns: 100 }
);
});
});

View File

@@ -0,0 +1,7 @@
import { webcrypto } from "node:crypto";
type WritableGlobal = { crypto?: Crypto };
const g = globalThis as WritableGlobal;
if (g.crypto === undefined) {
g.crypto = webcrypto as unknown as Crypto;
}