diff --git a/apps/trustlab/src/app/preview/route.ts b/apps/trustlab/src/app/preview/route.ts index 655cf83a98..2c6e0a324d 100644 --- a/apps/trustlab/src/app/preview/route.ts +++ b/apps/trustlab/src/app/preview/route.ts @@ -6,7 +6,7 @@ import { redirect } from "next/navigation"; import type { NextRequest } from "next/server"; import configPromise from "@payload-config"; -import { canManageContent } from "@/trustlab/payload/access/abilities"; +import { isAuthor } from "@/trustlab/payload/access/abilities"; export async function GET(req: NextRequest): Promise { const payload = await getPayload({ config: configPromise }); @@ -37,7 +37,7 @@ export async function GET(req: NextRequest): Promise { "Error verifying token for live preview", ); } - if (!(authResult && canManageContent(authResult?.user))) { + if (!(authResult && isAuthor(authResult?.user))) { return new Response("You are not allowed to preview this page", { status: 403, }); diff --git a/apps/trustlab/src/lib/data/rest/index.js b/apps/trustlab/src/lib/data/rest/index.js index 48de54f80c..6150e5dda9 100644 --- a/apps/trustlab/src/lib/data/rest/index.js +++ b/apps/trustlab/src/lib/data/rest/index.js @@ -55,6 +55,8 @@ export const api = { findPage, }; +// pages/_error getInitialProps can run on the server or in the browser. +// Keep this path on REST so it does not import Payload's server-only local API. export async function getErrorPageProps(context) { const slug = context?.params?.slugs?.[0] || "404"; const props = await getProps(api, slug); diff --git a/apps/trustlab/src/payload/access/abilities.js b/apps/trustlab/src/payload/access/abilities.js index 14521b51e9..1f0b596edd 100644 --- a/apps/trustlab/src/payload/access/abilities.js +++ b/apps/trustlab/src/payload/access/abilities.js @@ -1,16 +1,31 @@ import { checkRole } from "./checkRole"; -import { ROLE_ADMIN, ROLE_AUTHOR, ROLE_EDITOR } from "./roles"; +import { hasValidRole, ROLE_ADMIN, ROLE_AUTHOR, ROLE_EDITOR } from "./roles"; -export const canManageContent = (user) => { - if (!user) { +export const isLoggedIn = (user) => hasValidRole(user); + +export const hasLoggedInAccess = ({ req } = {}) => isLoggedIn(req?.user); + +// Admins and editors can manage any content +export const isEditor = (user) => checkRole([ROLE_ADMIN, ROLE_EDITOR], user); + +export const hasEditorAccess = ({ req } = {}) => isEditor(req?.user); + +export const isAuthor = (user) => { + // Any valid CMS user has at least author-level capability. + return isLoggedIn(user); +}; + +export const canAuthor = (user) => { + if (!isLoggedIn(user)) { return false; } - // Admin and editors can manage any content - if (checkRole([ROLE_ADMIN, ROLE_EDITOR], user)) { + if (isEditor(user)) { return true; } - - // Everyone else can only manage their own content + if (!user.id) { + return false; + } + // Authors can manage own content only return { createdBy: { equals: user.id, @@ -18,15 +33,15 @@ export const canManageContent = (user) => { }; }; -export const canManagePages = (user) => - checkRole([ROLE_ADMIN, ROLE_EDITOR], user); +export const hasAuthorAccess = ({ req } = {}) => canAuthor(req?.user); -export const canManageSiteSettings = ({ req: { user } }) => - checkRole([ROLE_ADMIN], user); +export const isAdmin = (user) => checkRole([ROLE_ADMIN], user); + +export const hasAdminAccess = ({ req } = {}) => isAdmin(req?.user); // TODO(@kelvinkipruto): what happens on delete? cascade or not? -export const canManageUsers = (user) => { - if (!user) { +export const canManageUser = (user) => { + if (!isLoggedIn(user)) { return false; } // Admins can manage all users @@ -34,6 +49,9 @@ export const canManageUsers = (user) => { return true; } // All other users can manage their own accounts + if (!user.id) { + return false; + } const orQuery = [ { id: { @@ -50,5 +68,8 @@ export const canManageUsers = (user) => { }; }; -export const canCreateAccounts = (user) => - checkRole([ROLE_ADMIN, ROLE_EDITOR], user); +export const hasManageUserAccess = ({ req } = {}) => canManageUser(req?.user); + +export const hasCreateUserAccess = (args) => hasEditorAccess(args); + +export default undefined; diff --git a/apps/trustlab/src/payload/access/index.js b/apps/trustlab/src/payload/access/index.js index cbb5ecf33b..7325af70de 100644 --- a/apps/trustlab/src/payload/access/index.js +++ b/apps/trustlab/src/payload/access/index.js @@ -1,2 +1,2 @@ +export * from "./abilities"; export * from "./anyone"; -export * from "./loggedIn"; diff --git a/apps/trustlab/src/payload/access/index.test.js b/apps/trustlab/src/payload/access/index.test.js new file mode 100644 index 0000000000..3c12a2191d --- /dev/null +++ b/apps/trustlab/src/payload/access/index.test.js @@ -0,0 +1,194 @@ +import { + canAuthor, + hasAdminAccess, + hasAuthorAccess, + hasCreateUserAccess, + hasManageUserAccess, + hasEditorAccess, + hasLoggedInAccess, + isAuthor, +} from "./abilities"; +import { anyone } from "./anyone"; +import { ROLE_ADMIN, ROLE_AUTHOR, ROLE_EDITOR } from "./roles"; + +describe("payload.access", () => { + describe("abilities.hasAdminAccess", () => { + it("allows requests from administrators", () => { + expect(hasAdminAccess({ req: { user: { role: ROLE_ADMIN } } })).toBe( + true, + ); + }); + + it("denies requests from authors, editors, and anonymous users", () => { + expect(hasAdminAccess({ req: { user: { role: ROLE_AUTHOR } } })).toBe( + false, + ); + expect(hasAdminAccess({ req: { user: { role: ROLE_EDITOR } } })).toBe( + false, + ); + expect(hasAdminAccess({ req: {} })).toBe(false); + expect(hasAdminAccess(undefined)).toBe(false); + }); + }); + + describe("abilities.isAuthor", () => { + it("allows users with administrators, authors and editors roles", () => { + expect(isAuthor({ role: ROLE_ADMIN })).toBe(true); + expect(isAuthor({ role: ROLE_AUTHOR })).toBe(true); + expect(isAuthor({ role: ROLE_EDITOR })).toBe(true); + }); + + it("denies users without a valid role", () => { + expect(isAuthor({ role: "unknown" })).toBe(false); + expect(isAuthor(1)).toBe(false); + expect(isAuthor(undefined)).toBe(false); + }); + }); + + describe("abilities.canAuthor", () => { + it("allows administrators and editors to author any content", () => { + expect(canAuthor({ role: ROLE_ADMIN })).toBe(true); + expect(canAuthor({ role: ROLE_EDITOR })).toBe(true); + }); + + it("limits authors to their own content", () => { + expect(canAuthor({ id: 1, role: ROLE_AUTHOR })).toEqual({ + createdBy: { equals: 1 }, + }); + }); + + it("denies authors without a user id for ownership checks", () => { + expect(canAuthor({ role: ROLE_AUTHOR })).toBe(false); + }); + + it("denies users without a valid role", () => { + expect(canAuthor({ role: "unknown" })).toBe(false); + expect(canAuthor(1)).toBe(false); + expect(canAuthor(undefined)).toBe(false); + }); + }); + + describe("abilities.hasAuthorAccess", () => { + it("allows requests from administrators, authors and editors", () => { + expect(hasAuthorAccess({ req: { user: { role: ROLE_ADMIN } } })).toBe( + true, + ); + expect( + hasAuthorAccess({ req: { user: { id: 1, role: ROLE_AUTHOR } } }), + ).toEqual({ createdBy: { equals: 1 } }); + expect(hasAuthorAccess({ req: { user: { role: ROLE_EDITOR } } })).toBe( + true, + ); + }); + + it("denies requests from anonymous users", () => { + expect(hasAuthorAccess({ req: {} })).toBe(false); + expect(hasAuthorAccess(undefined)).toBe(false); + }); + }); + + describe("abilities.hasCreateUserAccess", () => { + it("allows requests from administrators and editors", () => { + expect(hasCreateUserAccess({ req: { user: { role: ROLE_ADMIN } } })).toBe( + true, + ); + expect( + hasCreateUserAccess({ req: { user: { role: ROLE_EDITOR } } }), + ).toBe(true); + }); + + it("denies requests from authors and anonymous users", () => { + expect( + hasCreateUserAccess({ req: { user: { role: ROLE_AUTHOR } } }), + ).toBe(false); + expect(hasCreateUserAccess({ req: {} })).toBe(false); + expect(hasCreateUserAccess(undefined)).toBe(false); + }); + }); + + describe("abilities.hasManageUserAccess", () => { + it("allows requests from administrators, authors and editors", () => { + expect(hasManageUserAccess({ req: { user: { role: ROLE_ADMIN } } })).toBe( + true, + ); + expect( + hasManageUserAccess({ req: { user: { id: 1, role: ROLE_AUTHOR } } }), + ).toEqual({ or: [{ id: { equals: 1 } }] }); + expect( + hasManageUserAccess({ req: { user: { id: 1, role: ROLE_EDITOR } } }), + ).toEqual({ + or: [{ id: { equals: 1 } }, { role: { equals: ROLE_AUTHOR } }], + }); + }); + + it("denies requests from invalid-role and anonymous users", () => { + expect( + hasManageUserAccess({ req: { user: { id: 1, role: "unknown" } } }), + ).toBe(false); + expect(hasManageUserAccess({ req: {} })).toBe(false); + expect(hasManageUserAccess(undefined)).toBe(false); + }); + + it("denies requests from non-admin users without a user id for ownership checks", () => { + expect( + hasManageUserAccess({ req: { user: { role: ROLE_AUTHOR } } }), + ).toBe(false); + expect( + hasManageUserAccess({ req: { user: { role: ROLE_EDITOR } } }), + ).toBe(false); + }); + }); + + describe("abilities.hasEditorAccess", () => { + it("allows requests from administrators and editors", () => { + expect(hasEditorAccess({ req: { user: { role: ROLE_ADMIN } } })).toBe( + true, + ); + expect(hasEditorAccess({ req: { user: { role: ROLE_EDITOR } } })).toBe( + true, + ); + }); + + it("denies requests from authors and anonymous users", () => { + expect(hasEditorAccess({ req: { user: { role: ROLE_AUTHOR } } })).toBe( + false, + ); + expect(hasEditorAccess({ req: {} })).toBe(false); + expect(hasEditorAccess(undefined)).toBe(false); + }); + }); + + describe("abilities.hasLoggedInAccess", () => { + it("allows requests with users that have valid roles", () => { + expect(hasLoggedInAccess({ req: { user: { role: ROLE_ADMIN } } })).toBe( + true, + ); + expect(hasLoggedInAccess({ req: { user: { role: ROLE_AUTHOR } } })).toBe( + true, + ); + expect(hasLoggedInAccess({ req: { user: { role: ROLE_EDITOR } } })).toBe( + true, + ); + }); + + it("denies requests without a user that has a valid role", () => { + expect(hasLoggedInAccess({ req: { user: { role: "unknown" } } })).toBe( + false, + ); + expect(hasLoggedInAccess({ req: { user: 1 } })).toBe(false); + expect(hasLoggedInAccess({ req: {} })).toBe(false); + expect(hasLoggedInAccess(undefined)).toBe(false); + }); + }); + + describe("anyone.anyone", () => { + it("allows requests from administrators, authors, editors and anonymous users", () => { + expect(anyone({ req: { user: { role: ROLE_ADMIN } } })).toBe(true); + expect(anyone({ req: { user: { role: ROLE_AUTHOR } } })).toBe(true); + expect(anyone({ req: { user: { role: ROLE_EDITOR } } })).toBe(true); + expect(anyone({ req: { user: 1 } })).toBe(true); + expect(anyone({ req: {} })).toBe(true); + expect(anyone(undefined)).toBe(true); + }); + }); +}); diff --git a/apps/trustlab/src/payload/access/loggedIn.js b/apps/trustlab/src/payload/access/loggedIn.js deleted file mode 100644 index 395d1564df..0000000000 --- a/apps/trustlab/src/payload/access/loggedIn.js +++ /dev/null @@ -1,3 +0,0 @@ -export const loggedIn = (user) => Boolean(user); - -export default undefined; diff --git a/apps/trustlab/src/payload/access/roles.js b/apps/trustlab/src/payload/access/roles.js index a04f3d8214..50c8dceff1 100644 --- a/apps/trustlab/src/payload/access/roles.js +++ b/apps/trustlab/src/payload/access/roles.js @@ -6,3 +6,8 @@ export const ROLE_OPTIONS = [ { label: "Editor", value: ROLE_EDITOR }, { label: "Author", value: ROLE_AUTHOR }, ]; + +export const hasValidRole = (user) => + ROLE_OPTIONS.some(({ value }) => user?.role === value); + +export default undefined; diff --git a/apps/trustlab/src/payload/collections/Donors.js b/apps/trustlab/src/payload/collections/Donors.js index e298d3c3ae..3df0917cdd 100644 --- a/apps/trustlab/src/payload/collections/Donors.js +++ b/apps/trustlab/src/payload/collections/Donors.js @@ -6,6 +6,8 @@ import { linkGroup, } from "@commons-ui/payload"; +import { anyone, hasEditorAccess } from "@/trustlab/payload/access"; + const Donors = { slug: "donors", labels: { @@ -23,9 +25,11 @@ const Donors = { useAsTitle: "name", }, access: { - read: () => true, + read: anyone, + create: hasEditorAccess, + update: hasEditorAccess, + delete: hasEditorAccess, }, - fields: [ { name: "name", diff --git a/apps/trustlab/src/payload/collections/Media.js b/apps/trustlab/src/payload/collections/Media.js index 1f6b9f48fa..d3f7461e07 100644 --- a/apps/trustlab/src/payload/collections/Media.js +++ b/apps/trustlab/src/payload/collections/Media.js @@ -2,8 +2,7 @@ import path from "path"; import { createdBy } from "@commons-ui/payload"; -import { canManageContent } from "@/trustlab/payload/access/abilities"; -import { anyone } from "@/trustlab/payload/access/anyone"; +import { anyone, hasAuthorAccess } from "@/trustlab/payload/access"; import { hideAPIURL, slugify } from "@/trustlab/payload/utils"; function slugifyFilename(filename) { @@ -19,9 +18,9 @@ const Media = { slug: "media", access: { read: anyone, - create: ({ req: { user } }) => canManageContent(user), - update: ({ req: { user } }) => canManageContent(user), - delete: ({ req: { user } }) => canManageContent(user), + create: hasAuthorAccess, + update: hasAuthorAccess, + delete: hasAuthorAccess, }, admin: { group: "Publication", diff --git a/apps/trustlab/src/payload/collections/Opportunities.js b/apps/trustlab/src/payload/collections/Opportunities.js index 87afa09cb4..096378ded8 100644 --- a/apps/trustlab/src/payload/collections/Opportunities.js +++ b/apps/trustlab/src/payload/collections/Opportunities.js @@ -1,5 +1,6 @@ import { image, appendPathnameToCollection, slug } from "@commons-ui/payload"; +import { anyone, hasEditorAccess } from "@/trustlab/payload/access"; import blocks from "@/trustlab/payload/blocks"; const pageByType = { @@ -25,7 +26,10 @@ const Opportunities = { defaultColumns: ["title", "type"], }, access: { - read: () => true, + read: anyone, + create: hasEditorAccess, + update: hasEditorAccess, + delete: hasEditorAccess, }, fields: [ { diff --git a/apps/trustlab/src/payload/collections/Organisations.js b/apps/trustlab/src/payload/collections/Organisations.js index 202e785413..ba1999b655 100644 --- a/apps/trustlab/src/payload/collections/Organisations.js +++ b/apps/trustlab/src/payload/collections/Organisations.js @@ -1,5 +1,6 @@ import { slug, image, richText, linkGroup } from "@commons-ui/payload"; +import { anyone, hasEditorAccess } from "@/trustlab/payload/access"; import blocks from "@/trustlab/payload/blocks"; const Organisations = { @@ -14,7 +15,10 @@ const Organisations = { defaultColumns: ["name", "createdAt"], }, access: { - read: () => true, + read: anyone, + create: hasEditorAccess, + update: hasEditorAccess, + delete: hasEditorAccess, }, fields: [ { diff --git a/apps/trustlab/src/payload/collections/Pages.js b/apps/trustlab/src/payload/collections/Pages.js index 6f4842bf4c..be45fc5220 100644 --- a/apps/trustlab/src/payload/collections/Pages.js +++ b/apps/trustlab/src/payload/collections/Pages.js @@ -1,7 +1,6 @@ import { appendPathname, fullTitle, slug } from "@commons-ui/payload"; -import { canManagePages } from "@/trustlab/payload/access/abilities"; -import { anyone } from "@/trustlab/payload/access/anyone"; +import { anyone, hasEditorAccess } from "@/trustlab/payload/access"; import blocks from "@/trustlab/payload/blocks"; import { hideAPIURL, @@ -14,9 +13,9 @@ const Pages = { slug: "pages", access: { read: anyone, - create: ({ req: { user } }) => canManagePages(user), - update: ({ req: { user } }) => canManagePages(user), - delete: ({ req: { user } }) => canManagePages(user), + create: hasEditorAccess, + update: hasEditorAccess, + delete: hasEditorAccess, }, admin: { defaultColumns: ["fullTitle", "updatedAt", "_status"], diff --git a/apps/trustlab/src/payload/collections/Partners.js b/apps/trustlab/src/payload/collections/Partners.js index 4df6b58673..0e5ae19e99 100644 --- a/apps/trustlab/src/payload/collections/Partners.js +++ b/apps/trustlab/src/payload/collections/Partners.js @@ -6,6 +6,8 @@ import { slug, } from "@commons-ui/payload"; +import { anyone, hasEditorAccess } from "@/trustlab/payload/access"; + const Partners = { slug: "partners", labels: { @@ -23,9 +25,11 @@ const Partners = { useAsTitle: "name", }, access: { - read: () => true, + read: anyone, + create: hasEditorAccess, + update: hasEditorAccess, + delete: hasEditorAccess, }, - fields: [ { name: "name", diff --git a/apps/trustlab/src/payload/collections/Playbooks.js b/apps/trustlab/src/payload/collections/Playbooks.js index 5f937e92dd..550deacc3e 100644 --- a/apps/trustlab/src/payload/collections/Playbooks.js +++ b/apps/trustlab/src/payload/collections/Playbooks.js @@ -1,5 +1,7 @@ import { image, richText, slug } from "@commons-ui/payload"; +import { anyone, hasEditorAccess } from "@/trustlab/payload/access"; + const Playbooks = { slug: "playbooks", labels: { singular: "Playbook", plural: "Playbooks" }, @@ -8,10 +10,10 @@ const Playbooks = { defaultColumns: ["title", "updatedAt"], }, access: { - read: () => true, - create: ({ req: { user } }) => Boolean(user), - update: ({ req: { user } }) => Boolean(user), - delete: ({ req: { user } }) => Boolean(user), + read: anyone, + create: hasEditorAccess, + update: hasEditorAccess, + delete: hasEditorAccess, }, fields: [ { diff --git a/apps/trustlab/src/payload/collections/Posts.js b/apps/trustlab/src/payload/collections/Posts.js index 0866299f64..4f5d57de53 100644 --- a/apps/trustlab/src/payload/collections/Posts.js +++ b/apps/trustlab/src/payload/collections/Posts.js @@ -8,8 +8,7 @@ import { } from "@commons-ui/payload"; import { createParentField } from "@payloadcms/plugin-nested-docs"; -import { canManageContent } from "@/trustlab/payload/access/abilities"; -import { anyone } from "@/trustlab/payload/access/anyone"; +import { anyone, hasAuthorAccess } from "@/trustlab/payload/access"; import blocks from "@/trustlab/payload/blocks"; import { hideAPIURL, revalidatePost } from "@/trustlab/payload/utils"; @@ -23,9 +22,9 @@ const Posts = { }, access: { read: anyone, - create: ({ req: { user } }) => canManageContent(user), - update: ({ req: { user } }) => canManageContent(user), - delete: ({ req: { user } }) => canManageContent(user), + create: hasAuthorAccess, + update: hasAuthorAccess, + delete: hasAuthorAccess, }, fields: [ { diff --git a/apps/trustlab/src/payload/collections/Reports.js b/apps/trustlab/src/payload/collections/Reports.js index a68ac27293..02e994ba7f 100644 --- a/apps/trustlab/src/payload/collections/Reports.js +++ b/apps/trustlab/src/payload/collections/Reports.js @@ -5,6 +5,8 @@ import { slug, } from "@commons-ui/payload"; +import { anyone, hasEditorAccess } from "@/trustlab/payload/access"; + const pageByType = { baseline: "baseline-reports", situational: "situational-reports", @@ -27,10 +29,10 @@ const Reports = { defaultColumns: ["title", "reportType", "updatedAt"], }, access: { - read: () => true, - create: ({ req: { user } }) => Boolean(user), - update: ({ req: { user } }) => Boolean(user), - delete: ({ req: { user } }) => Boolean(user), + read: anyone, + create: hasEditorAccess, + update: hasEditorAccess, + delete: hasEditorAccess, }, fields: [ { diff --git a/apps/trustlab/src/payload/collections/Toolkits.js b/apps/trustlab/src/payload/collections/Toolkits.js index aa6aa849e2..c8c4487c6f 100644 --- a/apps/trustlab/src/payload/collections/Toolkits.js +++ b/apps/trustlab/src/payload/collections/Toolkits.js @@ -1,5 +1,7 @@ import { image, richText, slug, linkGroup } from "@commons-ui/payload"; +import { anyone, hasEditorAccess } from "@/trustlab/payload/access"; + const Toolkits = { slug: "toolkits", labels: { singular: "Toolkit", plural: "Toolkits" }, @@ -8,10 +10,10 @@ const Toolkits = { defaultColumns: ["title", "updatedAt"], }, access: { - read: () => true, - create: ({ req: { user } }) => Boolean(user), - update: ({ req: { user } }) => Boolean(user), - delete: ({ req: { user } }) => Boolean(user), + read: anyone, + create: hasEditorAccess, + update: hasEditorAccess, + delete: hasEditorAccess, }, fields: [ { diff --git a/apps/trustlab/src/payload/collections/Users/index.js b/apps/trustlab/src/payload/collections/Users/index.js index faff76ca55..7d121f5309 100644 --- a/apps/trustlab/src/payload/collections/Users/index.js +++ b/apps/trustlab/src/payload/collections/Users/index.js @@ -1,9 +1,9 @@ import { protectRoleField } from "./hooks/protectRoleField"; import { - canCreateAccounts, - canManageUsers, -} from "@/trustlab/payload/access/abilities"; + hasCreateUserAccess, + hasManageUserAccess, +} from "@/trustlab/payload/access"; import { ROLE_AUTHOR, ROLE_OPTIONS } from "@/trustlab/payload/access/roles"; const Users = { @@ -15,11 +15,11 @@ const Users = { hideAPIURL: true, }, access: { - delete: ({ req: { user } }) => canManageUsers(user), - create: ({ req: { user } }) => canCreateAccounts(user), - read: ({ req: { user } }) => canManageUsers(user), - update: ({ req: { user } }) => canManageUsers(user), - unlock: ({ req: { user } }) => canManageUsers(user), + delete: hasManageUserAccess, + create: hasCreateUserAccess, + read: hasManageUserAccess, + update: hasManageUserAccess, + unlock: hasManageUserAccess, }, auth: true, fields: [ @@ -47,7 +47,7 @@ const Users = { beforeChange: [protectRoleField], }, access: { - update: ({ req: { user } }) => user?.role !== ROLE_AUTHOR, + update: hasCreateUserAccess, }, }, ], diff --git a/apps/trustlab/src/payload/globals/index.js b/apps/trustlab/src/payload/globals/index.js index 09e28d9968..194ba7755a 100644 --- a/apps/trustlab/src/payload/globals/index.js +++ b/apps/trustlab/src/payload/globals/index.js @@ -1,7 +1,6 @@ import { EngagementTab, GeneralTab, NavigationTab, SeoTab } from "./tabs"; -import { loggedIn } from "@/trustlab/payload/access"; -import { canManageSiteSettings } from "@/trustlab/payload/access/abilities"; +import { anyone, hasAdminAccess } from "@/trustlab/payload/access"; import { hideAPIURL } from "@/trustlab/payload/utils"; const SiteSettings = { @@ -12,10 +11,10 @@ const SiteSettings = { hideAPIURL, }, access: { - // Since we're using Local APIs, we should still be able to pull data server-side - // See: note in https://payloadcms.com/docs/local-api/overview#transactions - read: loggedIn, - update: canManageSiteSettings, + // Public rendering paths, including the custom error page REST fallback, + // need navbar/footer/settings data before an authenticated CMS session exists. + read: anyone, + update: hasAdminAccess, }, fields: [ {