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:
@@ -65,10 +65,12 @@ export const getConstraints = (config: Config): Field => ({
|
|||||||
hooks: {
|
hooks: {
|
||||||
beforeChange: [
|
beforeChange: [
|
||||||
({ data, req }) => {
|
({ data, req }) => {
|
||||||
if (data?.access?.[operation]?.constraint === 'onlyMe') {
|
if (data?.access?.[operation]?.constraint === 'onlyMe' && req.user) {
|
||||||
if (req.user) {
|
return [req.user.id]
|
||||||
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
|
return data?.access?.[operation]?.users
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const preventLockout: Validate = async (
|
|||||||
canUpdate = true
|
canUpdate = true
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
if (!canRead || !canUpdate) {
|
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 {
|
} finally {
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
|
|||||||
@@ -379,27 +379,25 @@ describe('Query Presets', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should prevent accidental lockout', async () => {
|
it('should prevent accidental lockout', async () => {
|
||||||
// attempt to create a preset without access to read or update
|
|
||||||
try {
|
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({
|
const presetWithoutAccess = await payload.create({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
user: adminUser,
|
user: editorUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Prevent Lockout',
|
title: 'Prevent Lockout',
|
||||||
relatedCollection: 'pages',
|
relatedCollection: 'pages',
|
||||||
access: {
|
access: {
|
||||||
read: {
|
read: {
|
||||||
constraint: 'specificUsers',
|
constraint: 'specificRoles',
|
||||||
users: [],
|
roles: ['admin'],
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
constraint: 'specificUsers',
|
constraint: 'specificRoles',
|
||||||
users: [],
|
roles: ['admin'],
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
constraint: 'specificUsers',
|
|
||||||
users: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -407,9 +405,49 @@ describe('Query Presets', () => {
|
|||||||
|
|
||||||
expect(presetWithoutAccess).toBeFalsy()
|
expect(presetWithoutAccess).toBeFalsy()
|
||||||
} catch (error: unknown) {
|
} 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({
|
const presetWithUser1 = await payload.create({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
user: adminUser,
|
user: adminUser,
|
||||||
@@ -419,16 +457,12 @@ describe('Query Presets', () => {
|
|||||||
relatedCollection: 'pages',
|
relatedCollection: 'pages',
|
||||||
access: {
|
access: {
|
||||||
read: {
|
read: {
|
||||||
constraint: 'specificUsers',
|
constraint: 'specificRoles',
|
||||||
users: [adminUser.id],
|
roles: ['admin'],
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
constraint: 'specificUsers',
|
constraint: 'specificRoles',
|
||||||
users: [adminUser.id],
|
roles: ['admin'],
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
constraint: 'specificUsers',
|
|
||||||
users: [adminUser.id],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -445,16 +479,12 @@ describe('Query Presets', () => {
|
|||||||
title: 'Prevent Lockout (Updated)',
|
title: 'Prevent Lockout (Updated)',
|
||||||
access: {
|
access: {
|
||||||
read: {
|
read: {
|
||||||
constraint: 'specificUsers',
|
constraint: 'specificRoles',
|
||||||
users: [],
|
roles: ['user'],
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
constraint: 'specificUsers',
|
constraint: 'specificRoles',
|
||||||
users: [],
|
roles: ['user'],
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
constraint: 'specificUsers',
|
|
||||||
users: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -462,7 +492,7 @@ describe('Query Presets', () => {
|
|||||||
|
|
||||||
expect(presetUpdatedByUser1).toBeFalsy()
|
expect(presetUpdatedByUser1).toBeFalsy()
|
||||||
} catch (error: unknown) {
|
} 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.')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user