fix: auto inject req.user into query preset constraints (#12461)

In #12322 we prevented against accidental query preset lockout by
throwing a validation error when the user is going to change the preset
in a way that removes their own access to it. This, however, puts the
responsibility on the user to make the corrections and is an unnecessary
step.

For example, the API currently forbids leaving yourself out of the
`users` array when specifying the `specificUsers` constraint, but when
you encounter this error, have to update the field manually and try
again.

To improve the experience, we now automatically inject the requesting
user onto the `users` array when this constraint is selected. This will
guarantee they have access and prevent an accidental lockout while also
avoiding the API error feedback loop.
This commit is contained in:
Jacob Fletcher
2025-05-20 17:15:18 -04:00
committed by GitHub
parent 2ab8e2e194
commit 22b1858ee8
3 changed files with 65 additions and 33 deletions

View File

@@ -65,10 +65,12 @@ export const getConstraints = (config: Config): Field => ({
hooks: {
beforeChange: [
({ data, req }) => {
if (data?.access?.[operation]?.constraint === 'onlyMe') {
if (req.user) {
return [req.user.id]
}
if (data?.access?.[operation]?.constraint === 'onlyMe' && req.user) {
return [req.user.id]
}
if (data?.access?.[operation]?.constraint === 'specificUsers' && req.user) {
return [...(data?.access?.[operation]?.users || []), req.user.id]
}
return data?.access?.[operation]?.users

View File

@@ -72,7 +72,7 @@ export const preventLockout: Validate = async (
canUpdate = true
} catch (_err) {
if (!canRead || !canUpdate) {
throw new APIError('Cannot remove yourself from this preset.', 403, {}, true)
throw new APIError('This action will lock you out of this preset.', 403, {}, true)
}
} finally {
if (transaction) {

View File

@@ -379,27 +379,25 @@ describe('Query Presets', () => {
})
it('should prevent accidental lockout', async () => {
// attempt to create a preset without access to read or update
try {
// create a preset using "specificRoles"
// this will ensure the user on the request is _NOT_ automatically added to the `users` list
// and will throw a validation error instead
const presetWithoutAccess = await payload.create({
collection: queryPresetsCollectionSlug,
user: adminUser,
user: editorUser,
overrideAccess: false,
data: {
title: 'Prevent Lockout',
relatedCollection: 'pages',
access: {
read: {
constraint: 'specificUsers',
users: [],
constraint: 'specificRoles',
roles: ['admin'],
},
update: {
constraint: 'specificUsers',
users: [],
},
delete: {
constraint: 'specificUsers',
users: [],
constraint: 'specificRoles',
roles: ['admin'],
},
},
},
@@ -407,9 +405,49 @@ describe('Query Presets', () => {
expect(presetWithoutAccess).toBeFalsy()
} catch (error: unknown) {
expect((error as Error).message).toBe('Cannot remove yourself from this preset.')
expect((error as Error).message).toBe('This action will lock you out of this preset.')
}
// create a preset using "specificUsers"
// this will ensure the user on the request _IS_ automatically added to the `users` list
// this will avoid a validation error
const presetWithoutAccess = await payload.create({
collection: queryPresetsCollectionSlug,
user: adminUser,
overrideAccess: false,
data: {
title: 'Prevent Lockout',
relatedCollection: 'pages',
access: {
read: {
constraint: 'specificUsers',
users: [],
},
update: {
constraint: 'specificUsers',
users: [],
},
delete: {
constraint: 'specificUsers',
users: [],
},
},
},
})
// the user on the request is automatically added to the `users` array
expect(
presetWithoutAccess.access?.read?.users?.find(
(user) => (typeof user === 'string' ? user : user.id) === adminUser.id,
),
).toBeTruthy()
expect(
presetWithoutAccess.access?.update?.users?.find(
(user) => (typeof user === 'string' ? user : user.id) === adminUser.id,
),
).toBeTruthy()
const presetWithUser1 = await payload.create({
collection: queryPresetsCollectionSlug,
user: adminUser,
@@ -419,16 +457,12 @@ describe('Query Presets', () => {
relatedCollection: 'pages',
access: {
read: {
constraint: 'specificUsers',
users: [adminUser.id],
constraint: 'specificRoles',
roles: ['admin'],
},
update: {
constraint: 'specificUsers',
users: [adminUser.id],
},
delete: {
constraint: 'specificUsers',
users: [adminUser.id],
constraint: 'specificRoles',
roles: ['admin'],
},
},
},
@@ -445,16 +479,12 @@ describe('Query Presets', () => {
title: 'Prevent Lockout (Updated)',
access: {
read: {
constraint: 'specificUsers',
users: [],
constraint: 'specificRoles',
roles: ['user'],
},
update: {
constraint: 'specificUsers',
users: [],
},
delete: {
constraint: 'specificUsers',
users: [],
constraint: 'specificRoles',
roles: ['user'],
},
},
},
@@ -462,7 +492,7 @@ describe('Query Presets', () => {
expect(presetUpdatedByUser1).toBeFalsy()
} catch (error: unknown) {
expect((error as Error).message).toBe('Cannot remove yourself from this preset.')
expect((error as Error).message).toBe('This action will lock you out of this preset.')
}
})
})