From 66d05873fe8f9e82266fdd81036a974fed5d437d Mon Sep 17 00:00:00 2001 From: zumbrunn Date: Tue, 10 Nov 2026 13:20:45 +0100 Subject: [PATCH] feat: EditorIdentity.relation; the author limit follows it, not role names (duneorg/dune#139) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 ++ src/http.ts | 4 +++- tests/http_test.ts | 21 ++++++++++++++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 200adf9..bb33f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ or "Open in admin": the admin contributes those through `bar.actions`. `commit()` and `patchFields()` stay on the manager object but are not part of core's `InlineEditManager`. `onPageUpdate` carries the editor's name as `actor`. + The author limit (an `author` writes only the pages it created) follows `EditorIdentity.relation`, + which core derives from `system.role_grants`, not role names. - **The bar declares its own extension point, and reads what it needs per request (#127).** `bar.actions` is declared here, as the typed key `BAR_ACTIONS`, with its types `BarAction` and diff --git a/src/http.ts b/src/http.ts index 241de6c..47cb559 100644 --- a/src/http.ts +++ b/src/http.ts @@ -82,7 +82,9 @@ export function createRequestHandler(opts: RequestHandlerOptions) { error: "TSX format requires admin role. TSX pages execute server-side code and must be edited by trusted authors.", }, 403); } - if (!editor.roles.includes("admin") && !editor.roles.includes("editor")) { + // `relation` is core's decision from system.role_grants: admin and editor edit any page, an + // author only the pages it created. + if (editor.relation === "author") { const frontmatter = await ctx.pageFrontmatter(sourcePath); if (!frontmatter) return json({ error: "Page not found" }, 404); if (frontmatter[PAGE_OWNER_KEY] !== editor.id) return json({ error: "Forbidden" }, 403); diff --git a/tests/http_test.ts b/tests/http_test.ts index 36c0bb8..1d8df89 100644 --- a/tests/http_test.ts +++ b/tests/http_test.ts @@ -27,7 +27,7 @@ function setup(opts: { allowTsxFormat?: string[]; frontmatter?: Record handle( @@ -36,7 +36,14 @@ function setup(opts: { allowTsxFormat?: string[]; frontmatter?: Record Promise.resolve(frontmatter), }, @@ -134,10 +141,18 @@ Deno.test("a failing commit is a 500 that does not leak the error", async () => patchFields: () => Promise.resolve(), }); const res = await handle(new Request("http://localhost/x", { method: "POST" }), { - editor: { id: "u1", username: "alice", roles: ["admin"], source: "site" }, + editor: { id: "u1", username: "alice", roles: ["admin"], relation: "admin", source: "site" }, path: `content/${PAGE}/commit`, pageFrontmatter: () => Promise.resolve({}), }); assertEquals(res.status, 500); assertEquals(await res.json(), { error: "Internal error" }); }); + +Deno.test("ownership follows the relation core decided, not the role name: a mapped group is an editor", async () => { + const theirs = setup({ frontmatter: { createdBy: "someone-else" } }); + // role_grants: { staff: editor } makes "staff" an editor relation, so it may write any page... + assertEquals((await theirs.run("POST", `content/${PAGE}/commit`, { roles: ["staff"], relation: "editor" })).status, 200); + // ...and an "editor" role the site mapped down to author is limited to its own pages. + assertEquals((await theirs.run("POST", `content/${PAGE}/commit`, { roles: ["editor"], relation: "author" })).status, 403); +});