fix: respects boolean query preset constraints (#12124)

Returning a boolean value from a constraint-level access control
function does nothing. For example:

```ts
{
  label: 'Noone',
  value: 'noone',
  access: () => false,
},
```

This is because we were only handling query objects, disregarding any
boolean values. The fix is to check if the query is a boolean, and if
so, format a query object to return.
This commit is contained in:
Jacob Fletcher
2025-04-16 09:16:43 -04:00
committed by GitHub
parent e79b20363e
commit a675c04c99
8 changed files with 89 additions and 23 deletions

View File

@@ -71,7 +71,17 @@ export const getAccess = (config: Config): Record<Operation, Access> =>
return { return {
and: [ and: [
...(typeof constraintAccess === 'object' ? [constraintAccess] : []), ...(typeof constraintAccess === 'object'
? [constraintAccess]
: constraintAccess === false
? [
{
id: {
equals: null,
},
},
]
: []),
{ {
[`access.${operation}.constraint`]: { [`access.${operation}.constraint`]: {
equals: constraint.value, equals: constraint.value,

View File

@@ -78,7 +78,7 @@ export const getConstraints = (config: Config): Field => ({
}, },
...(config?.queryPresets?.constraints?.[operation]?.reduce( ...(config?.queryPresets?.constraints?.[operation]?.reduce(
(acc: Field[], option: QueryPresetConstraint) => { (acc: Field[], option: QueryPresetConstraint) => {
option.fields.forEach((field, index) => { option.fields?.forEach((field, index) => {
acc.push({ ...field }) acc.push({ ...field })
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {

View File

@@ -25,7 +25,7 @@ export type QueryPreset = {
export type QueryPresetConstraint = { export type QueryPresetConstraint = {
access: Access<QueryPreset> access: Access<QueryPreset>
fields: Field[] fields?: Field[]
label: string label: string
value: string value: string
} }

View File

@@ -24,9 +24,9 @@ export default buildConfigWithDefaults({
// }, // },
access: { access: {
read: ({ req: { user } }) => read: ({ req: { user } }) =>
user ? !user?.roles?.some((role) => role === 'anonymous') : false, user ? user && !user?.roles?.some((role) => role === 'anonymous') : false,
update: ({ req: { user } }) => update: ({ req: { user } }) =>
user ? !user?.roles?.some((role) => role === 'anonymous') : false, user ? user && !user?.roles?.some((role) => role === 'anonymous') : false,
}, },
constraints: { constraints: {
read: [ read: [
@@ -40,6 +40,11 @@ export default buildConfigWithDefaults({
}, },
}), }),
}, },
{
label: 'Noone',
value: 'noone',
access: () => false,
},
], ],
update: [ update: [
{ {

View File

@@ -503,6 +503,42 @@ describe('Query Presets', () => {
expect((error as Error).message).toBe('You are not allowed to perform this action.') expect((error as Error).message).toBe('You are not allowed to perform this action.')
} }
}) })
it('should respect boolean access control results', async () => {
// create a preset with the read constraint set to "noone"
const presetForNoone = await payload.create({
collection: queryPresetsCollectionSlug,
user,
data: {
relatedCollection: 'pages',
title: 'Noone',
where: {
text: {
equals: 'example page',
},
},
access: {
read: {
constraint: 'noone',
},
},
},
})
try {
const foundPresetWithUser1 = await payload.findByID({
collection: queryPresetsCollectionSlug,
depth: 0,
user,
overrideAccess: false,
id: presetForNoone.id,
})
expect(foundPresetWithUser1).toBeFalsy()
} catch (error: unknown) {
expect((error as Error).message).toBe('Not Found')
}
})
}) })
it.skip('should disable query presets when "enabledQueryPresets" is not true on the collection', async () => { it.skip('should disable query presets when "enabledQueryPresets" is not true on the collection', async () => {

View File

@@ -86,7 +86,7 @@ export interface Config {
'payload-query-presets': PayloadQueryPresetsSelect<false> | PayloadQueryPresetsSelect<true>; 'payload-query-presets': PayloadQueryPresetsSelect<false> | PayloadQueryPresetsSelect<true>;
}; };
db: { db: {
defaultIDType: number; defaultIDType: string;
}; };
globals: {}; globals: {};
globalsSelect: {}; globalsSelect: {};
@@ -122,7 +122,7 @@ export interface UserAuthOperations {
* via the `definition` "pages". * via the `definition` "pages".
*/ */
export interface Page { export interface Page {
id: number; id: string;
text?: string | null; text?: string | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -133,7 +133,7 @@ export interface Page {
* via the `definition` "users". * via the `definition` "users".
*/ */
export interface User { export interface User {
id: number; id: string;
name?: string | null; name?: string | null;
roles?: ('admin' | 'user' | 'anonymous')[] | null; roles?: ('admin' | 'user' | 'anonymous')[] | null;
updatedAt: string; updatedAt: string;
@@ -152,7 +152,7 @@ export interface User {
* via the `definition` "posts". * via the `definition` "posts".
*/ */
export interface Post { export interface Post {
id: number; id: string;
text?: string | null; text?: string | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -163,24 +163,24 @@ export interface Post {
* via the `definition` "payload-locked-documents". * via the `definition` "payload-locked-documents".
*/ */
export interface PayloadLockedDocument { export interface PayloadLockedDocument {
id: number; id: string;
document?: document?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: number | Page; value: string | Page;
} | null) } | null)
| ({ | ({
relationTo: 'users'; relationTo: 'users';
value: number | User; value: string | User;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: number | User; value: string | User;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -190,10 +190,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences". * via the `definition` "payload-preferences".
*/ */
export interface PayloadPreference { export interface PayloadPreference {
id: number; id: string;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: number | User; value: string | User;
}; };
key?: string | null; key?: string | null;
value?: value?:
@@ -213,7 +213,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations". * via the `definition` "payload-migrations".
*/ */
export interface PayloadMigration { export interface PayloadMigration {
id: number; id: string;
name?: string | null; name?: string | null;
batch?: number | null; batch?: number | null;
updatedAt: string; updatedAt: string;
@@ -224,23 +224,23 @@ export interface PayloadMigration {
* via the `definition` "payload-query-presets". * via the `definition` "payload-query-presets".
*/ */
export interface PayloadQueryPreset { export interface PayloadQueryPreset {
id: number; id: string;
title: string; title: string;
isShared?: boolean | null; isShared?: boolean | null;
access?: { access?: {
read?: { read?: {
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles') | null; constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles' | 'noone') | null;
users?: (number | User)[] | null; users?: (string | User)[] | null;
roles?: ('admin' | 'user' | 'anonymous')[] | null; roles?: ('admin' | 'user' | 'anonymous')[] | null;
}; };
update?: { update?: {
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles') | null; constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles') | null;
users?: (number | User)[] | null; users?: (string | User)[] | null;
roles?: ('admin' | 'user' | 'anonymous')[] | null; roles?: ('admin' | 'user' | 'anonymous')[] | null;
}; };
delete?: { delete?: {
constraint?: ('everyone' | 'onlyMe' | 'specificUsers') | null; constraint?: ('everyone' | 'onlyMe' | 'specificUsers') | null;
users?: (number | User)[] | null; users?: (string | User)[] | null;
}; };
}; };
where?: where?:

View File

@@ -167,6 +167,21 @@ export const seed = async (_payload: Payload) => {
overrideAccess: false, overrideAccess: false,
data: seedData.onlyMe, data: seedData.onlyMe,
}), }),
() =>
_payload.create({
collection: 'payload-query-presets',
user: devUser,
overrideAccess: false,
data: {
relatedCollection: 'pages',
title: 'Noone',
access: {
read: {
constraint: 'noone',
},
},
},
}),
], ],
false, false,
) )

View File

@@ -31,7 +31,7 @@
} }
], ],
"paths": { "paths": {
"@payload-config": ["./test/form-state/config.ts"], "@payload-config": ["./test/query-presets/config.ts"],
"@payloadcms/admin-bar": ["./packages/admin-bar/src"], "@payloadcms/admin-bar": ["./packages/admin-bar/src"],
"@payloadcms/live-preview": ["./packages/live-preview/src"], "@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"], "@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],