Gate a route or nav node on a session, not only a permission
CI / full-gate (push) Successful in 3m6s
CI / full-gate (push) Successful in 3m6s
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import type { User } from "../http/context.ts";
|
||||
import { allows, gatesSet } from "./gate.ts";
|
||||
|
||||
const holder: User = { email: "holder@example.test", id: "01a06091-ba9f-765f-abf4-b5144c314bc7", permissions: ["x:read"] };
|
||||
const stranger: User = { email: "stranger@example.test", id: "01a06091-baa3-7b4d-810a-c9ee7e559d98", permissions: [] };
|
||||
|
||||
test("allows: ungated and public are open to anyone; session needs a user; permission needs the token", () => {
|
||||
assert.equal(allows({}, null), true);
|
||||
assert.equal(allows({ public: true }, null), true);
|
||||
|
||||
assert.equal(allows({ session: true }, null), false);
|
||||
assert.equal(allows({ session: true }, stranger), true); // signed in is the whole gate — no grant
|
||||
|
||||
assert.equal(allows({ permission: "x:read" }, null), false);
|
||||
assert.equal(allows({ permission: "x:read" }, stranger), false);
|
||||
assert.equal(allows({ permission: "x:read" }, holder), true);
|
||||
});
|
||||
|
||||
test("gatesSet names the gates a declaration sets, so discovery can refuse more than one", () => {
|
||||
assert.deepEqual(gatesSet({}), []);
|
||||
assert.deepEqual(gatesSet({ session: true }), ["session"]);
|
||||
assert.deepEqual(gatesSet({ permission: "x:read", public: true }), ["public", "permission"]);
|
||||
assert.deepEqual(gatesSet({ permission: "x:read", public: true, session: true }), ["public", "session", "permission"]);
|
||||
// `false` is not a gate — only a set one counts, so { session: false } is an ungated route.
|
||||
assert.deepEqual(gatesSet({ public: false, session: false }), []);
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
// The coarse gate a route or nav node declares. One home for the rule, so the router and the menu
|
||||
// can never disagree about what a visitor may reach.
|
||||
import type { User } from "../http/context.ts";
|
||||
|
||||
// Widest first: whoever passes an earlier gate passes it without holding anything.
|
||||
const GATES = ["public", "session", "permission"] as const;
|
||||
|
||||
export interface Gate {
|
||||
permission?: string | undefined; // the Keto Permission the caller must hold
|
||||
public?: boolean | undefined; // anyone, signed in or not
|
||||
session?: boolean | undefined; // any signed-in user, no grant needed
|
||||
}
|
||||
|
||||
export function allows(gate: Gate, user: User | null): boolean {
|
||||
if (gate.public === true) return true;
|
||||
if (gate.session === true) return user !== null;
|
||||
return gate.permission == null || (user?.permissions.includes(gate.permission) ?? false);
|
||||
}
|
||||
|
||||
// Which gates a declaration sets — discovery refuses more than one, since they contradict.
|
||||
export function gatesSet(gate: Gate | null | undefined): string[] {
|
||||
if (gate == null) return [];
|
||||
return GATES.filter((name) => (name === "permission" ? gate.permission != null : gate[name] === true));
|
||||
}
|
||||
Reference in New Issue
Block a user