From a0bb13a4123b51d770b364ddaee3dde1c5a3da53 Mon Sep 17 00:00:00 2001 From: James Mikrut Date: Mon, 1 May 2023 17:15:14 -0400 Subject: [PATCH] fix: #2592, allows usage of hidden fields within access query constraints (#2599) Co-authored-by: Dan Ribbens --- src/collections/operations/delete.ts | 27 +---- src/collections/operations/deleteByID.ts | 21 +--- src/collections/operations/find.ts | 27 +---- src/collections/operations/findByID.ts | 23 +--- src/collections/operations/findVersionByID.ts | 23 +--- src/collections/operations/findVersions.ts | 35 +----- src/collections/operations/restoreVersion.ts | 23 +--- src/collections/operations/update.ts | 26 +--- src/collections/operations/updateByID.ts | 23 ++-- src/globals/operations/findOne.ts | 24 +--- src/globals/operations/findVersionByID.ts | 23 +--- src/globals/operations/findVersions.ts | 36 +----- src/globals/operations/update.ts | 23 +--- src/mongoose/buildQuery.ts | 111 +++++++++++------- src/versions/drafts/queryDrafts.ts | 13 +- .../drafts/replaceWithDraftIfAvailable.ts | 6 +- test/access-control/config.ts | 57 ++++++++- test/access-control/int.spec.ts | 66 ++++++++++- 18 files changed, 262 insertions(+), 325 deletions(-) diff --git a/src/collections/operations/delete.ts b/src/collections/operations/delete.ts index e6a6d8bbf3..17f0aa5b11 100644 --- a/src/collections/operations/delete.ts +++ b/src/collections/operations/delete.ts @@ -7,7 +7,6 @@ import { APIError } from '../../errors'; import executeAccess from '../../auth/executeAccess'; import { BeforeOperationHook, Collection } from '../config/types'; import { Where } from '../../types'; -import { hasWhereAccessResult } from '../../auth/types'; import { afterRead } from '../../fields/hooks/afterRead'; import { deleteCollectionVersions } from '../../versions/deleteCollectionVersions'; import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles'; @@ -55,7 +54,6 @@ async function deleteOperation(inc // Retrieve document // ///////////////////////////////////// - const queryToBuild: Where = { - and: [ - { - id: { - equals: id, - }, - }, - ], - }; - - if (hasWhereAccessResult(accessResults)) { - queryToBuild.and.push(accessResults); - } - const query = await Model.buildQuery({ req, - where: queryToBuild, + where: { + id: { + equals: id, + }, + }, + access: accessResults, overrideAccess, }); diff --git a/src/collections/operations/find.ts b/src/collections/operations/find.ts index 622430b7d1..6a119fbcca 100644 --- a/src/collections/operations/find.ts +++ b/src/collections/operations/find.ts @@ -4,7 +4,6 @@ import executeAccess from '../../auth/executeAccess'; import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; import { Collection, TypeWithID } from '../config/types'; import { PaginatedDocs } from '../../mongoose/types'; -import { hasWhereAccessResult } from '../../auth/types'; import flattenWhereConstraints from '../../utilities/flattenWhereConstraints'; import { buildSortParam } from '../../mongoose/buildSortParam'; import { AccessResult } from '../../config/types'; @@ -72,27 +71,10 @@ async function find>( // Access // ///////////////////////////////////// - let queryToBuild: Where = { - and: [], - }; - let useEstimatedCount = false; if (where) { - queryToBuild = { - and: [], - ...where, - }; - - if (Array.isArray(where.AND)) { - queryToBuild.and = [ - ...queryToBuild.and, - ...where.AND, - ]; - } - - const constraints = flattenWhereConstraints(queryToBuild); - + const constraints = flattenWhereConstraints(where); useEstimatedCount = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')); } @@ -116,16 +98,13 @@ async function find>( limit, }; } - - if (hasWhereAccessResult(accessResult)) { - queryToBuild.and.push(accessResult); - } } const query = await Model.buildQuery({ req, - where: queryToBuild, + where, overrideAccess, + access: accessResult, }); // ///////////////////////////////////// diff --git a/src/collections/operations/findByID.ts b/src/collections/operations/findByID.ts index 78881a9af2..5609fdfdd5 100644 --- a/src/collections/operations/findByID.ts +++ b/src/collections/operations/findByID.ts @@ -5,8 +5,6 @@ import { Collection, TypeWithID } from '../config/types'; import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; import { NotFound } from '../../errors'; import executeAccess from '../../auth/executeAccess'; -import { Where } from '../../types'; -import { hasWhereAccessResult } from '../../auth/types'; import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable'; import { afterRead } from '../../fields/hooks/afterRead'; @@ -68,22 +66,13 @@ async function findByID( // If errors are disabled, and access returns false, return null if (accessResult === false) return null; - const queryToBuild: Where = { - and: [ - { - _id: { - equals: id, - }, - }, - ], - }; - - if (hasWhereAccessResult(accessResult)) { - queryToBuild.and.push(accessResult); - } - const query = await Model.buildQuery({ - where: queryToBuild, + where: { + _id: { + equals: id, + }, + }, + access: accessResult, req, overrideAccess, }); diff --git a/src/collections/operations/findVersionByID.ts b/src/collections/operations/findVersionByID.ts index 45a2a273e7..67a7d1abdc 100644 --- a/src/collections/operations/findVersionByID.ts +++ b/src/collections/operations/findVersionByID.ts @@ -5,8 +5,6 @@ import { Collection, CollectionModel } from '../config/types'; import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; import { APIError, Forbidden, NotFound } from '../../errors'; import executeAccess from '../../auth/executeAccess'; -import { Where } from '../../types'; -import { hasWhereAccessResult } from '../../auth/types'; import { TypeWithVersion } from '../../versions/types'; import { afterRead } from '../../fields/hooks/afterRead'; @@ -56,22 +54,13 @@ async function findVersionByID = any>(args: Argumen const hasWhereAccess = typeof accessResults === 'object'; - const queryToBuild: Where = { - and: [ - { - _id: { - equals: id, - }, - }, - ], - }; - - if (hasWhereAccessResult(accessResults)) { - queryToBuild.and.push(accessResults); - } - const query = await VersionsModel.buildQuery({ - where: queryToBuild, + where: { + _id: { + equals: id, + }, + }, + access: accessResults, req, overrideAccess, }); diff --git a/src/collections/operations/findVersions.ts b/src/collections/operations/findVersions.ts index a5c92410fd..72d43b5490 100644 --- a/src/collections/operations/findVersions.ts +++ b/src/collections/operations/findVersions.ts @@ -3,7 +3,6 @@ import { PayloadRequest } from '../../express/types'; import executeAccess from '../../auth/executeAccess'; import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; import { Collection, CollectionModel } from '../config/types'; -import { hasWhereAccessResult } from '../../auth/types'; import flattenWhereConstraints from '../../utilities/flattenWhereConstraints'; import { buildSortParam } from '../../mongoose/buildSortParam'; import { PaginatedDocs } from '../../mongoose/types'; @@ -49,45 +48,23 @@ async function findVersions>( // Access // ///////////////////////////////////// - let queryToBuild: Where = {}; let useEstimatedCount = false; if (where) { - let and = []; - - if (Array.isArray(where.and)) and = where.and; - if (Array.isArray(where.AND)) and = where.AND; - - queryToBuild = { - ...where, - and: [ - ...and, - ], - }; - - const constraints = flattenWhereConstraints(queryToBuild); + const constraints = flattenWhereConstraints(where); useEstimatedCount = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')); } - if (!overrideAccess) { - const accessResults = await executeAccess({ req }, collectionConfig.access.readVersions); + let accessResults; - if (hasWhereAccessResult(accessResults)) { - if (!where) { - queryToBuild = { - and: [ - accessResults, - ], - }; - } else { - queryToBuild.and.push(accessResults); - } - } + if (!overrideAccess) { + accessResults = await executeAccess({ req }, collectionConfig.access.readVersions); } const query = await VersionsModel.buildQuery({ - where: queryToBuild, + where, + access: accessResults, req, overrideAccess, }); diff --git a/src/collections/operations/restoreVersion.ts b/src/collections/operations/restoreVersion.ts index 015c3139fc..984ca5bdbb 100644 --- a/src/collections/operations/restoreVersion.ts +++ b/src/collections/operations/restoreVersion.ts @@ -5,7 +5,6 @@ import { Collection, TypeWithID } from '../config/types'; import { APIError, Forbidden, NotFound } from '../../errors'; import executeAccess from '../../auth/executeAccess'; import { hasWhereAccessResult } from '../../auth/types'; -import { Where } from '../../types'; import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; import { afterChange } from '../../fields/hooks/afterChange'; import { afterRead } from '../../fields/hooks/afterRead'; @@ -34,7 +33,6 @@ async function restoreVersion(args: Arguments): Prom depth, req: { t, - locale, payload, }, req, @@ -73,22 +71,13 @@ async function restoreVersion(args: Arguments): Prom // Retrieve document // ///////////////////////////////////// - const queryToBuild: Where = { - and: [ - { - id: { - equals: parentDocID, - }, - }, - ], - }; - - if (hasWhereAccessResult(accessResults)) { - queryToBuild.and.push(accessResults); - } - const query = await Model.buildQuery({ - where: queryToBuild, + where: { + id: { + equals: parentDocID, + }, + }, + access: accessResults, req, overrideAccess, }); diff --git a/src/collections/operations/update.ts b/src/collections/operations/update.ts index 6a798da6e6..c9b81b94b9 100644 --- a/src/collections/operations/update.ts +++ b/src/collections/operations/update.ts @@ -7,7 +7,6 @@ import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; import executeAccess from '../../auth/executeAccess'; import { APIError, ValidationError } from '../../errors'; import { PayloadRequest } from '../../express/types'; -import { hasWhereAccessResult } from '../../auth/types'; import { saveVersion } from '../../versions/saveVersion'; import { uploadFiles } from '../../uploads/uploadFiles'; import { beforeChange } from '../../fields/hooks/beforeChange'; @@ -84,36 +83,15 @@ async function update( // Access // ///////////////////////////////////// - let queryToBuild: Where = { - and: [], - }; - - if (where) { - queryToBuild = { - and: [], - ...where, - }; - - if (Array.isArray(where.AND)) { - queryToBuild.and = [ - ...queryToBuild.and, - ...where.AND, - ]; - } - } - let accessResult: AccessResult; if (!overrideAccess) { accessResult = await executeAccess({ req }, collectionConfig.access.update); - - if (hasWhereAccessResult(accessResult)) { - queryToBuild.and.push(accessResult); - } } const query = await Model.buildQuery({ - where: queryToBuild, + where, + access: accessResult, req, overrideAccess, }); diff --git a/src/collections/operations/updateByID.ts b/src/collections/operations/updateByID.ts index ca8f026254..c5c1b4ae96 100644 --- a/src/collections/operations/updateByID.ts +++ b/src/collections/operations/updateByID.ts @@ -1,7 +1,7 @@ import httpStatus from 'http-status'; import { Config as GeneratedTypes } from 'payload/generated-types'; import { DeepPartial } from 'ts-essentials'; -import { Where, Document } from '../../types'; +import { Document } from '../../types'; import { Collection } from '../config/types'; import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; import executeAccess from '../../auth/executeAccess'; @@ -96,22 +96,13 @@ async function updateByID( // Retrieve document // ///////////////////////////////////// - const queryToBuild: Where = { - and: [ - { - id: { - equals: id, - }, - }, - ], - }; - - if (hasWhereAccessResult(accessResults)) { - queryToBuild.and.push(accessResults); - } - const query = await Model.buildQuery({ - where: queryToBuild, + where: { + id: { + equals: id, + }, + }, + access: accessResults, req, overrideAccess, }); diff --git a/src/globals/operations/findOne.ts b/src/globals/operations/findOne.ts index b3e7974a04..b91c904a88 100644 --- a/src/globals/operations/findOne.ts +++ b/src/globals/operations/findOne.ts @@ -1,6 +1,4 @@ -import { hasWhereAccessResult } from '../../auth'; import executeAccess from '../../auth/executeAccess'; -import { Where } from '../../types'; import { AccessResult } from '../../config/types'; import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable'; @@ -22,7 +20,6 @@ type Args = { async function findOne>(args: Args): Promise { const { globalConfig, - locale, req, req: { payload, @@ -40,28 +37,19 @@ async function findOne>(args: Args): Promise = any>(args: Argumen const hasWhereAccess = typeof accessResults === 'object'; - const queryToBuild: Where = { - and: [ - { - _id: { - equals: id, - }, - }, - ], - }; - - if (hasWhereAccessResult(accessResults)) { - queryToBuild.and.push(accessResults); - } - const query = await VersionsModel.buildQuery({ - where: queryToBuild, + where: { + _id: { + equals: id, + }, + }, + access: accessResults, req, overrideAccess, globalSlug: globalConfig.slug, diff --git a/src/globals/operations/findVersions.ts b/src/globals/operations/findVersions.ts index 5ddceaea57..de43de4991 100644 --- a/src/globals/operations/findVersions.ts +++ b/src/globals/operations/findVersions.ts @@ -3,7 +3,6 @@ import { PayloadRequest } from '../../express/types'; import executeAccess from '../../auth/executeAccess'; import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; import { PaginatedDocs } from '../../mongoose/types'; -import { hasWhereAccessResult } from '../../auth/types'; import flattenWhereConstraints from '../../utilities/flattenWhereConstraints'; import { buildSortParam } from '../../mongoose/buildSortParam'; import { SanitizedGlobalConfig } from '../config/types'; @@ -47,45 +46,18 @@ async function findVersions>( // Access // ///////////////////////////////////// - let queryToBuild: Where = {}; let useEstimatedCount = false; if (where) { - let and = []; - - if (Array.isArray(where.and)) and = where.and; - if (Array.isArray(where.AND)) and = where.AND; - - queryToBuild = { - ...where, - and: [ - ...and, - ], - }; - - const constraints = flattenWhereConstraints(queryToBuild); - + const constraints = flattenWhereConstraints(where); useEstimatedCount = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')); } - if (!overrideAccess) { - const accessResults = await executeAccess({ req }, globalConfig.access.readVersions); - - if (hasWhereAccessResult(accessResults)) { - if (!where) { - queryToBuild = { - and: [ - accessResults, - ], - }; - } else { - queryToBuild.and.push(accessResults); - } - } - } + const accessResults = !overrideAccess ? await executeAccess({ req }, globalConfig.access.readVersions) : true; const query = await VersionsModel.buildQuery({ - where: queryToBuild, + where, + access: accessResults, req, overrideAccess, globalSlug: globalConfig.slug, diff --git a/src/globals/operations/update.ts b/src/globals/operations/update.ts index 349944052f..c139bbf32f 100644 --- a/src/globals/operations/update.ts +++ b/src/globals/operations/update.ts @@ -1,9 +1,7 @@ import { Config as GeneratedTypes } from 'payload/generated-types'; import { DeepPartial } from 'ts-essentials'; -import { Where } from '../../types'; import { SanitizedGlobalConfig } from '../config/types'; import executeAccess from '../../auth/executeAccess'; -import { hasWhereAccessResult } from '../../auth'; import { beforeChange } from '../../fields/hooks/beforeChange'; import { beforeValidate } from '../../fields/hooks/beforeValidate'; import { afterChange } from '../../fields/hooks/afterChange'; @@ -61,22 +59,13 @@ async function update( // Retrieve document // ///////////////////////////////////// - const queryToBuild: Where = { - and: [ - { - globalType: { - equals: slug, - }, - }, - ], - }; - - if (hasWhereAccessResult(accessResults)) { - queryToBuild.and.push(accessResults); - } - const query = await Model.buildQuery({ - where: queryToBuild, + where: { + globalType: { + equals: slug, + }, + }, + access: accessResults, req, overrideAccess, globalSlug: slug, diff --git a/src/mongoose/buildQuery.ts b/src/mongoose/buildQuery.ts index 8e8a72cf5c..928b8739c8 100644 --- a/src/mongoose/buildQuery.ts +++ b/src/mongoose/buildQuery.ts @@ -44,6 +44,7 @@ type ParamParserArgs = { versionsFields?: Field[] model: any where: Where + access?: Where | boolean overrideAccess?: boolean } @@ -56,6 +57,8 @@ export class ParamParser { req: PayloadRequest; + access?: Where | boolean; + where: Where; model: any; @@ -82,6 +85,7 @@ export class ParamParser { versionsFields, model, where, + access, overrideAccess, }: ParamParserArgs) { this.req = req; @@ -90,6 +94,7 @@ export class ParamParser { this.parse = this.parse.bind(this); this.model = model; this.where = where; + this.access = access; this.overrideAccess = overrideAccess; this.localizationConfig = req.payload.config.localization; this.policies = { @@ -113,65 +118,77 @@ export class ParamParser { // Entry point to the ParamParser class async parse(): Promise> { - if (typeof this.where === 'object') { - const query = await this.parsePathOrRelation(this.where); - return query; + const query = await this.parsePathOrRelation(this.where, this.overrideAccess); + + const result = { + $and: [], + }; + + if (query) result.$and.push(query); + + if (typeof this.access === 'object') { + const accessQuery = await this.parsePathOrRelation(this.access, true); + if (accessQuery) result.$and.push(accessQuery); } - return {}; + return result; } - async parsePathOrRelation(object: Where): Promise> { + async parsePathOrRelation(object: Where, overrideAccess: boolean): Promise> { let result = {} as FilterQuery; - // We need to determine if the whereKey is an AND, OR, or a schema path - for (const relationOrPath of Object.keys(object)) { - if (relationOrPath.toLowerCase() === 'and') { - const andConditions = object[relationOrPath]; - const builtAndConditions = await this.buildAndOrConditions(andConditions); - if (builtAndConditions.length > 0) result.$and = builtAndConditions; - } else if (relationOrPath.toLowerCase() === 'or' && Array.isArray(object[relationOrPath])) { - const orConditions = object[relationOrPath]; - const builtOrConditions = await this.buildAndOrConditions(orConditions); - if (builtOrConditions.length > 0) result.$or = builtOrConditions; - } else { - // It's a path - and there can be multiple comparisons on a single path. - // For example - title like 'test' and title not equal to 'tester' - // So we need to loop on keys again here to handle each operator independently - const pathOperators = object[relationOrPath]; - if (typeof pathOperators === 'object') { - for (const operator of Object.keys(pathOperators)) { - if (validOperators.includes(operator)) { - const searchParam = await this.buildSearchParam({ - fields: this.fields, - incomingPath: relationOrPath, - val: pathOperators[operator], - operator, - }); - if (searchParam?.value && searchParam?.path) { - result = { - ...result, - [searchParam.path]: searchParam.value, - }; - } else if (typeof searchParam?.value === 'object') { - result = deepmerge(result, searchParam.value, { arrayMerge: combineMerge }); + if (typeof object === 'object') { + // We need to determine if the whereKey is an AND, OR, or a schema path + for (const relationOrPath of Object.keys(object)) { + const condition = object[relationOrPath]; + if (relationOrPath.toLowerCase() === 'and' && Array.isArray(condition)) { + const builtAndConditions = await this.buildAndOrConditions(condition, overrideAccess); + if (builtAndConditions.length > 0) result.$and = builtAndConditions; + } else if (relationOrPath.toLowerCase() === 'or' && Array.isArray(condition)) { + const builtOrConditions = await this.buildAndOrConditions(condition, overrideAccess); + if (builtOrConditions.length > 0) result.$or = builtOrConditions; + } else { + // It's a path - and there can be multiple comparisons on a single path. + // For example - title like 'test' and title not equal to 'tester' + // So we need to loop on keys again here to handle each operator independently + const pathOperators = object[relationOrPath]; + if (typeof pathOperators === 'object') { + for (const operator of Object.keys(pathOperators)) { + if (validOperators.includes(operator)) { + const searchParam = await this.buildSearchParam({ + fields: this.fields, + incomingPath: relationOrPath, + val: pathOperators[operator], + operator, + overrideAccess, + }); + + if (searchParam?.value && searchParam?.path) { + result = { + ...result, + [searchParam.path]: searchParam.value, + }; + } else if (typeof searchParam?.value === 'object') { + result = deepmerge(result, searchParam.value, { arrayMerge: combineMerge }); + } } } } } } } + return result; } - async buildAndOrConditions(conditions) { + async buildAndOrConditions(conditions: Where[], overrideAccess: boolean): Promise[]> { const completedConditions = []; // Loop over all AND / OR operations and add them to the AND / OR query param // Operations should come through as an array for (const condition of conditions) { // If the operation is properly formatted as an object if (typeof condition === 'object') { - const result = await this.parsePathOrRelation(condition); + const result = await this.parsePathOrRelation(condition, overrideAccess); if (Object.keys(result).length > 0) { completedConditions.push(result); } @@ -186,11 +203,13 @@ export class ParamParser { incomingPath, val, operator, + overrideAccess, }: { fields: Field[], incomingPath: string, val: unknown, operator: string + overrideAccess: boolean }): Promise { // Replace GraphQL nested field double underscore formatting let sanitizedPath = incomingPath.replace(/__/gi, '.'); @@ -228,6 +247,7 @@ export class ParamParser { globalSlug: this.globalSlug, fields, incomingPath: sanitizedPath, + overrideAccess, }); } @@ -268,7 +288,7 @@ export class ParamParser { }, }, req: this.req, - overrideAccess: this.overrideAccess, + overrideAccess, }); const result = await SubModel.find(subQuery, subQueryOptions); @@ -334,11 +354,13 @@ export class ParamParser { globalSlug, fields, incomingPath, + overrideAccess, }: { collectionSlug?: string globalSlug?: string fields: Field[] incomingPath: string + overrideAccess: boolean }): Promise { const pathSegments = incomingPath.split('.'); @@ -353,7 +375,7 @@ export class ParamParser { }, ]; - if (!this.overrideAccess) { + if (!overrideAccess) { if (collectionSlug) { const collection = { ...this.req.payload.collections[collectionSlug].config }; collection.fields = fields; @@ -431,7 +453,7 @@ export class ParamParser { } if (matchedField) { - if (!this.overrideAccess) { + if (!overrideAccess) { const fieldAccess = lastIncompletePath.fieldPolicies[matchedField.name].read.permission; if (!fieldAccess || ('hidden' in matchedField && matchedField.hidden)) { @@ -492,6 +514,7 @@ export class ParamParser { collectionSlug: relatedCollection.slug, fields: relatedCollection.fields, incomingPath: nestedPathToQuery, + overrideAccess, }); paths = [ @@ -512,7 +535,7 @@ export class ParamParser { lastIncompletePath.fields = flattenFields(lastIncompletePath.field.fields, false); } - if (!this.overrideAccess && 'fields' in lastIncompletePath.fieldPolicies[lastIncompletePath.field.name]) { + if (!overrideAccess && 'fields' in lastIncompletePath.fieldPolicies[lastIncompletePath.field.name]) { lastIncompletePath.fieldPolicies = lastIncompletePath.fieldPolicies[lastIncompletePath.field.name].fields; } @@ -542,6 +565,7 @@ export type BuildQueryArgs = { req: PayloadRequest where: Where overrideAccess: boolean + access?: Where | boolean globalSlug?: string } @@ -553,7 +577,7 @@ const getBuildQueryPlugin = ({ }: GetBuildQueryPluginArgs = {}) => { return function buildQueryPlugin(schema) { const modifiedSchema = schema; - async function buildQuery({ req, where, overrideAccess = false, globalSlug }: BuildQueryArgs): Promise> { + async function buildQuery({ req, where, overrideAccess = false, access, globalSlug }: BuildQueryArgs): Promise> { const paramParser = new ParamParser({ req, collectionSlug, @@ -561,6 +585,7 @@ const getBuildQueryPlugin = ({ versionsFields, model: this, where, + access, overrideAccess, }); const result = await paramParser.parse(); diff --git a/src/versions/drafts/queryDrafts.ts b/src/versions/drafts/queryDrafts.ts index 421437132f..4105eb9f71 100644 --- a/src/versions/drafts/queryDrafts.ts +++ b/src/versions/drafts/queryDrafts.ts @@ -37,20 +37,15 @@ export const queryDrafts = async ({ const where = appendVersionToQueryKey(incomingWhere || {}); - const versionQueryToBuild: Where = { - ...where, - and: [ - ...where?.and || [], - ], - }; + let versionAccessResult; if (hasWhereAccessResult(accessResult)) { - const versionAccessResult = appendVersionToQueryKey(accessResult); - versionQueryToBuild.and.push(versionAccessResult); + versionAccessResult = appendVersionToQueryKey(accessResult); } const versionQuery = await VersionModel.buildQuery({ - where: versionQueryToBuild, + where, + access: versionAccessResult, req, overrideAccess, }); diff --git a/src/versions/drafts/replaceWithDraftIfAvailable.ts b/src/versions/drafts/replaceWithDraftIfAvailable.ts index f1531f4239..ca32829d7f 100644 --- a/src/versions/drafts/replaceWithDraftIfAvailable.ts +++ b/src/versions/drafts/replaceWithDraftIfAvailable.ts @@ -54,13 +54,15 @@ const replaceWithDraftIfAvailable = async ({ }); } + let versionAccessResult; + if (hasWhereAccessResult(accessResult)) { - const versionAccessResult = appendVersionToQueryKey(accessResult); - queryToBuild.and.push(versionAccessResult); + versionAccessResult = appendVersionToQueryKey(accessResult); } const query = await VersionModel.buildQuery({ where: queryToBuild, + access: versionAccessResult, req, overrideAccess, globalSlug: entityType === 'global' ? entity.slug : undefined, diff --git a/test/access-control/config.ts b/test/access-control/config.ts index 0fe54403d5..274895b0e8 100644 --- a/test/access-control/config.ts +++ b/test/access-control/config.ts @@ -15,6 +15,8 @@ export const relyOnRequestHeadersSlug = 'rely-on-request-headers'; export const docLevelAccessSlug = 'doc-level-access'; export const hiddenFieldsSlug = 'hidden-fields'; +export const hiddenAccessSlug = 'hidden-access'; + const openAccess = { create: () => true, read: () => true, @@ -187,9 +189,31 @@ export default buildConfig({ name: 'name', type: 'text', }, + { + name: 'hidden', + type: 'checkbox', + hidden: true, + }, ], access: { - readVersions: () => false, + read: ({ req: { user } }) => { + if (user) return true; + + return { + hidden: { + not_equals: true, + }, + }; + }, + readVersions: ({ req: { user } }) => { + if (user) return true; + + return { + 'version.hidden': { + not_equals: true, + }, + }; + }, }, }, { @@ -320,6 +344,37 @@ export default buildConfig({ }, ], }, + { + name: 'hidden', + type: 'checkbox', + hidden: true, + }, + ], + }, + { + slug: hiddenAccessSlug, + access: { + read: ({ req: { user } }) => { + if (user) return true; + + return { + hidden: { + not_equals: true, + }, + }; + }, + }, + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + { + name: 'hidden', + type: 'checkbox', + hidden: true, + }, ], }, ], diff --git a/test/access-control/int.spec.ts b/test/access-control/int.spec.ts index ce3673aa8b..cb6abe05ba 100644 --- a/test/access-control/int.spec.ts +++ b/test/access-control/int.spec.ts @@ -3,8 +3,17 @@ import payload from '../../src'; import { Forbidden } from '../../src/errors'; import type { PayloadRequest } from '../../src/types'; import { initPayloadTest } from '../helpers/configHelpers'; -import { hiddenFieldsSlug, relyOnRequestHeadersSlug, requestHeaders, restrictedSlug, siblingDataSlug, slug } from './config'; -import type { Restricted, Post, RelyOnRequestHeader } from './payload-types'; +import { + hiddenAccessSlug, + hiddenFieldsSlug, + relyOnRequestHeadersSlug, + requestHeaders, + restrictedSlug, + restrictedVersionsSlug, + siblingDataSlug, + slug, +} from './config'; +import type { Post, RelyOnRequestHeader, Restricted } from './payload-types'; import { firstArrayText, secondArrayText } from './shared'; describe('Access Control', () => { @@ -359,6 +368,59 @@ describe('Access Control', () => { }); }); }); + + describe('Querying', () => { + it('should respect query constraint using hidden field', async () => { + await payload.create({ + collection: hiddenAccessSlug, + data: { + title: 'hello', + }, + }); + + await payload.create({ + collection: hiddenAccessSlug, + data: { + title: 'hello', + hidden: true, + }, + }); + + const { docs } = await payload.find({ + collection: hiddenAccessSlug, + overrideAccess: false, + }); + + expect(docs).toHaveLength(1); + }); + + it('should respect query constraint using hidden field on versions', async () => { + await payload.create({ + collection: restrictedVersionsSlug, + data: { + name: 'match', + hidden: true, + }, + }); + + await payload.create({ + collection: restrictedVersionsSlug, + data: { + name: 'match', + hidden: false, + }, + }); + const { docs } = await payload.findVersions({ + where: { + 'version.name': { equals: 'match' }, + }, + collection: restrictedVersionsSlug, + overrideAccess: false, + }); + + expect(docs).toHaveLength(1); + }); + }); }); async function createDoc(data: Partial, overrideSlug = slug, options?: Partial): Promise {