test(authz): end-to-end integration coverage, worked example, and docs
Task 15 of the authz permissions plan: proves db store + cache + catalog + middleware + audit compose correctly, wires a real (non-dangling) example into auth-showcase, and documents the declaration/registration/precedence surface in the package README.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { defineAuthz } from "@wrnexus/authz";
|
||||
|
||||
/**
|
||||
* `app/authz/<name>.ts` declarations are discovered automatically and merged
|
||||
* into the process-wide catalog at boot (see `app/middleware/authz.ts`, which
|
||||
* registers the middleware that resolves against it).
|
||||
*/
|
||||
export default defineAuthz({
|
||||
permissions: {
|
||||
"post:read": { title: "View posts", public: true },
|
||||
"post:write": { title: "Create and edit posts" },
|
||||
"post:delete": { title: "Delete posts", risk: "high" },
|
||||
"admin:access": { title: "Reach the admin area", risk: "high" },
|
||||
},
|
||||
roles: {
|
||||
viewer: ["post:read"],
|
||||
editor: ["role:viewer", "post:write"],
|
||||
admin: ["role:editor", "post:delete", "admin:access"],
|
||||
},
|
||||
policies: {
|
||||
ownsPost: async (
|
||||
subject: { id?: string },
|
||||
resource?: { authorId?: string },
|
||||
): Promise<{ allowed: boolean; reason?: string }> =>
|
||||
resource?.authorId === subject?.id
|
||||
? { allowed: true }
|
||||
: { allowed: false, reason: "You are not the author" },
|
||||
},
|
||||
bindings: { "post:delete": ["ownsPost"] },
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { authzMiddleware, getAuthzCatalog, memoryPermissionStore } from "@wrnexus/authz";
|
||||
|
||||
/**
|
||||
* Registers the per-request authorization resolver against the catalog merged
|
||||
* from `app/authz/*.ts` (see `showcase.ts`). This is an eager, module-scope
|
||||
* call — the same shape `authzMiddleware({...})` requires — so it must run
|
||||
* after `getAuthzCatalog()` has been populated. Both the dev server and
|
||||
* `wrnexus build`'s generated production entry guarantee that happens before
|
||||
* any app middleware module evaluates.
|
||||
*
|
||||
* Middleware runs in alphabetical filename order, so `authz.ts` runs after
|
||||
* `auth.ts`, which hydrates `ctx.user` from the session. Route handlers and
|
||||
* pages can then call `can(ctx, "post:write")` or guard a route with
|
||||
* `guardPermission("post:delete")`.
|
||||
*
|
||||
* A real deployment would swap `memoryPermissionStore()` for
|
||||
* `dbPermissionStore(getDb())` from `@wrnexus/authz/db` so role and grant
|
||||
* assignments survive a restart; the showcase keeps everything in memory so
|
||||
* it stays dependency-free.
|
||||
*/
|
||||
export default authzMiddleware({
|
||||
catalog: getAuthzCatalog(),
|
||||
store: memoryPermissionStore(),
|
||||
});
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/auth": "workspace:*",
|
||||
"@wrnexus/authz": "workspace:*",
|
||||
"@wrnexus/captcha": "workspace:*",
|
||||
"@wrnexus/core": "workspace:*",
|
||||
"@wrnexus/validation": "workspace:*"
|
||||
|
||||
@@ -91,3 +91,23 @@ test("package auth schemas are shared by browser forms and API handlers", () =>
|
||||
expect(forgotPassword).toContain("data-schema='{schema}'");
|
||||
expect(forgotPassword).toContain("novalidate");
|
||||
});
|
||||
|
||||
test("authz is wired with a real declaration and a registered middleware, not a dangling file", () => {
|
||||
expect(existsSync(join(root, "app", "authz", "showcase.ts"))).toBe(true);
|
||||
expect(existsSync(join(root, "app", "middleware", "authz.ts"))).toBe(true);
|
||||
|
||||
const declaration = read(root, "app", "authz", "showcase.ts");
|
||||
expect(declaration).toContain("defineAuthz");
|
||||
expect(declaration).toContain('"post:read": { title: "View posts", public: true }');
|
||||
expect(declaration).toContain("bindings:");
|
||||
|
||||
const middleware = read(root, "app", "middleware", "authz.ts");
|
||||
expect(middleware).toContain("authzMiddleware");
|
||||
expect(middleware).toContain("getAuthzCatalog()");
|
||||
expect(middleware).toContain("memoryPermissionStore()");
|
||||
|
||||
const manifest = JSON.parse(read(root, "package.json")) as {
|
||||
dependencies?: Record<string, string>;
|
||||
};
|
||||
expect(manifest.dependencies?.["@wrnexus/authz"]).toBe("workspace:*");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user