chore: consolidate canAccessAdmin logic (#13849)

Consolidates the logic for admin access control for server functions,
etc. behind a standard `canAccessAdmin` function.
This commit is contained in:
Jacob Fletcher
2025-09-18 09:35:33 -04:00
committed by GitHub
parent 5241113809
commit 42b5935772
8 changed files with 55 additions and 175 deletions

View File

@@ -3,7 +3,7 @@ import type { DocumentPreferences, VisibleEntities } from 'payload'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { headers as getHeaders } from 'next/headers.js'
import { getAccessResults, isEntityHidden, parseCookies } from 'payload'
import { canAccessAdmin, getAccessResults, isEntityHidden, parseCookies } from 'payload'
import { renderDocument } from './index.js'
@@ -35,38 +35,7 @@ export const renderDocumentHandler: RenderDocumentServerFunction = async (args)
const cookies = parseCookies(headers)
const incomingUserSlug = user?.collection
const adminUserSlug = config.admin.user
// If we have a user slug, test it against the functions
if (incomingUserSlug) {
const adminAccessFunction = payload.collections[incomingUserSlug].config.access?.admin
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction({ req })
if (!canAccessAdmin) {
throw new Error('Unauthorized')
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
throw new Error('Unauthorized')
}
} else {
const hasUsers = await payload.find({
collection: adminUserSlug,
depth: 0,
limit: 1,
pagination: false,
})
// If there are users, we should not allow access because of /create-first-user
if (hasUsers.docs.length) {
throw new Error('Unauthorized')
}
}
await canAccessAdmin({ req })
const clientConfig = getClientConfig({
config,

View File

@@ -2,7 +2,7 @@ import type { CollectionPreferences, ListQuery, ServerFunction, VisibleEntities
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { headers as getHeaders } from 'next/headers.js'
import { getAccessResults, isEntityHidden, parseCookies } from 'payload'
import { canAccessAdmin, getAccessResults, isEntityHidden, parseCookies } from 'payload'
import { renderListView } from './index.js'
@@ -53,38 +53,7 @@ export const renderListHandler: ServerFunction<
const cookies = parseCookies(headers)
const incomingUserSlug = user?.collection
const adminUserSlug = config.admin.user
// If we have a user slug, test it against the functions
if (incomingUserSlug) {
const adminAccessFunction = payload.collections[incomingUserSlug].config.access?.admin
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction({ req })
if (!canAccessAdmin) {
throw new Error('Unauthorized')
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
throw new Error('Unauthorized')
}
} else {
const hasUsers = await payload.find({
collection: adminUserSlug,
depth: 0,
limit: 1,
pagination: false,
})
// If there are users, we should not allow access because of /create-first-user
if (hasUsers.docs.length) {
throw new Error('Unauthorized')
}
}
await canAccessAdmin({ req })
const clientConfig = getClientConfig({
config,

View File

@@ -1658,6 +1658,7 @@ export { _internal_safeFetchGlobal } from './uploads/safeFetch.js'
export type * from './uploads/types.js'
export { addDataAndFileToRequest } from './utilities/addDataAndFileToRequest.js'
export { addLocalesToRequestFromData, sanitizeLocales } from './utilities/addLocalesToRequest.js'
export { canAccessAdmin } from './utilities/canAccessAdmin.js'
export { commitTransaction } from './utilities/commitTransaction.js'
export {
configToJSONSchema,

View File

@@ -0,0 +1,41 @@
import type { PayloadRequest } from '../types/index.js'
/**
* Protects admin-only routes, server functions, etc.
* The requesting user must either:
* a. pass the `access.admin` function on the `users` collection, if defined
* b. match the `config.admin.user` property on the Payload config
* c. if no user is present, and there are no users in the system, allow access (for first user creation)
* @throws {Error} Throws an `Unauthorized` error if access is denied that can be explicitly caught
*/
export const canAccessAdmin = async ({ req }: { req: PayloadRequest }) => {
const incomingUserSlug = req.user?.collection
const adminUserSlug = req.payload.config.admin.user
if (incomingUserSlug) {
const adminAccessFn = req.payload.collections[incomingUserSlug]?.config.access?.admin
if (adminAccessFn) {
const canAccess = await adminAccessFn({ req })
if (!canAccess) {
throw new Error('Unauthorized')
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
throw new Error('Unauthorized')
}
} else {
const hasUsers = await req.payload.find({
collection: adminUserSlug,
depth: 0,
limit: 1,
pagination: false,
})
// If there are users, we should not allow access because of `/create-first-user`
if (hasUsers.docs.length) {
throw new Error('Unauthorized')
}
}
}

View File

@@ -7,7 +7,7 @@ import type {
ServerFunction,
} from 'payload'
import { formatErrors } from 'payload'
import { canAccessAdmin, formatErrors } from 'payload'
import { getSelectMode, reduceFieldsToValues } from 'payload/shared'
import { fieldSchemasToFormState } from '../forms/fieldSchemasToFormState/index.js'
@@ -49,40 +49,10 @@ export const buildFormStateHandler: ServerFunction<
> = async (args) => {
const { req } = args
const incomingUserSlug = req.user?.collection
const adminUserSlug = req.payload.config.admin.user
try {
// If we have a user slug, test it against the functions
if (incomingUserSlug) {
const adminAccessFunction = req.payload.collections[incomingUserSlug].config.access?.admin
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction({ req })
if (!canAccessAdmin) {
throw new Error('Unauthorized')
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
throw new Error('Unauthorized')
}
} else {
const hasUsers = await req.payload.find({
collection: adminUserSlug,
depth: 0,
limit: 1,
pagination: false,
})
// If there are users, we should not allow access because of /create-first-user
if (hasUsers.docs.length) {
throw new Error('Unauthorized')
}
}
await canAccessAdmin({ req })
const res = await buildFormState(args)
return res
} catch (err) {
req.payload.logger.error({ err, msg: `There was an error building form state` })

View File

@@ -11,7 +11,7 @@ import type {
Where,
} from 'payload'
import { APIError, formatErrors } from 'payload'
import { APIError, canAccessAdmin, formatErrors } from 'payload'
import { isNumber } from 'payload/shared'
import { getClientConfig } from './getClientConfig.js'
@@ -91,39 +91,7 @@ const buildTableState = async (
tableAppearance,
} = args
const incomingUserSlug = user?.collection
const adminUserSlug = config.admin.user
// If we have a user slug, test it against the functions
if (incomingUserSlug) {
const adminAccessFunction = payload.collections[incomingUserSlug].config.access?.admin
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction({ req })
if (!canAccessAdmin) {
throw new Error('Unauthorized')
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
throw new Error('Unauthorized')
}
} else {
const hasUsers = await payload.find({
collection: adminUserSlug,
depth: 0,
limit: 1,
pagination: false,
})
// If there are users, we should not allow access because of /create-first-user
if (hasUsers.docs.length) {
throw new Error('Unauthorized')
}
}
await canAccessAdmin({ req })
const clientConfig = getClientConfig({
config,

View File

@@ -1,5 +1,6 @@
import ObjectIdImport from 'bson-objectid'
import {
canAccessAdmin,
type CollectionSlug,
type Data,
type Field,
@@ -245,26 +246,7 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
toLocale,
} = args
const incomingUserSlug = user?.collection
const adminUserSlug = payload.config.admin.user
// If we have a user slug, test it against the functions
if (incomingUserSlug) {
const adminAccessFunction = payload.collections[incomingUserSlug].config.access?.admin
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction({ req: args.req })
if (!canAccessAdmin) {
throw new Error('Unauthorized')
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
throw new Error('Unauthorized')
}
}
await canAccessAdmin({ req })
const [fromLocaleData, toLocaleData] = await Promise.allSettled([
globalSlug

View File

@@ -1,4 +1,4 @@
import type { PayloadRequest, SchedulePublishTaskInput } from 'payload'
import { canAccessAdmin, type PayloadRequest, type SchedulePublishTaskInput } from 'payload'
export type SchedulePublishHandlerArgs = {
date?: Date
@@ -22,27 +22,7 @@ export const schedulePublishHandler = async ({
}: SchedulePublishHandlerArgs) => {
const { i18n, payload, user } = req
const incomingUserSlug = user?.collection
const adminUserSlug = payload.config.admin.user
if (!incomingUserSlug) {
throw new Error('Unauthorized')
}
const adminAccessFunction = payload.collections[incomingUserSlug].config.access?.admin
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction({ req })
if (!canAccessAdmin) {
throw new Error('Unauthorized')
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
throw new Error('Unauthorized')
}
await canAccessAdmin({ req })
try {
if (deleteID) {