payloadcms/payload · error · Forbidden

You are not allowed to perform this action.

Error message

You are not allowed to perform this action.

What it means

Thrown by findVersionByIDOperation when the version lookup returns nothing but the caller is subject to a where-based access-control result (access returned a query object rather than a boolean). Payload interprets an empty result under a where-constraint as 'forbidden' rather than 'not found', to avoid leaking the existence of versions the user cannot read. Surfaced as HTTP 403 Forbidden.

Source

Thrown at packages/payload/src/globals/operations/findVersionByID.ts:100

      where: combineQueries({ id: { equals: id } }, accessResults),
    }

    // /////////////////////////////////////
    // Find by ID
    // /////////////////////////////////////

    if (!findGlobalVersionsArgs.where?.and?.[0]?.id) {
      throw new NotFound(req.t)
    }

    const { docs: results } = await payload.db.findGlobalVersions(findGlobalVersionsArgs)
    if (!results || results?.length === 0) {
      if (!disableErrors) {
        if (!hasWhereAccess) {
          throw new NotFound(req.t)
        }
        if (hasWhereAccess) {
          throw new Forbidden(req.t)
        }
      }

      return null!
    }

    // Clone the result - it may have come back memoized
    let result: any = deepCopyObjectSimple(results[0])

    if (!result.version) {
      result.version = {}
    }

    // Patch globalType onto version doc
    result.version.globalType = globalConfig.slug

    // /////////////////////////////////////
    // beforeRead - Collection

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Confirm the authenticated user actually satisfies the readVersions constraint for that version.
  2. If callers legitimately need to probe existence, pass disableErrors:true and treat null as 'not visible'.
  3. Audit the readVersions access function — returning a Where object turns every miss into 403; ensure the query matches how versions store the gating field.
  4. For server-side privileged reads, set overrideAccess:true to bypass the where-constraint path.

Example fix

// before
const v = await payload.findGlobalVersionByID({ id, global: 'branding', overrideAccess: false })

// after
const v = await payload.findGlobalVersionByID({
  id,
  global: 'branding',
  overrideAccess: false,
  disableErrors: true,
})
if (v === null) {
  // could be missing OR not permitted — handle as 'unavailable'
}
Defensive patterns

Strategy: try-catch

Validate before calling

const allowed = await executeAccess(
  { id, slug, req },
  globalConfig.access.readVersions,
)
// if typeof allowed === 'object', a miss becomes Forbidden — handle null

Type guard

function isWhereAccess(
  r: boolean | object,
): r is object {
  return typeof r === 'object'
}

Try / catch

try {
  return await payload.findGlobalVersionByID({ id, global: slug, overrideAccess: false })
} catch (err) {
  if (err instanceof Forbidden) return null // treat as 'not visible'
  throw err
}

Prevention

When it happens

Trigger: Calling findVersionByID with overrideAccess:false where globalConfig.access.readVersions returned a Where query, and either the version does not exist or the user's constraint excludes it. GET /globals/:slug/versions/:id by a user whose readVersions access narrows the visible set.

Common situations: Role-based tenants where readVersions returns { tenant: { equals: user.tenant } } and the requested version belongs to another tenant; tightening access rules after versions were created; a logged-in user whose access function returns a query that no version satisfies.

Related errors


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/e66a5af2579d3cb8. Report an issue: GitHub.