diff --git a/.changeset/errors-now-equal.md b/.changeset/errors-now-equal.md new file mode 100644 index 00000000000..70fea2df793 --- /dev/null +++ b/.changeset/errors-now-equal.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': major +--- + +Changes access-control error messages to only show the list key and operation diff --git a/examples/ecommerce/tests/mutations.test.ts b/examples/ecommerce/tests/mutations.test.ts index 79265bda621..bf250a9ddd1 100644 --- a/examples/ecommerce/tests/mutations.test.ts +++ b/examples/ecommerce/tests/mutations.test.ts @@ -156,7 +156,7 @@ describe(`Custom mutations`, () => { expect(data).toEqual({ addToCart: null }); expect(errors).toHaveLength(1); expect(errors![0].message).toEqual( - `An error occured while resolving relationship fields.\n - CartItem.product: Access denied: You cannot perform the 'connect' operation on the item '{"id":"${FAKE_ID}"}'. It may not exist.\n - CartItem.user: Access denied: You cannot perform the 'connect' operation on the item '{"id":"${FAKE_ID}"}'. It may not exist.` + `An error occured while resolving relationship fields.\n - CartItem.product: Access denied: You cannot connect that Product - it may not exist\n - CartItem.user: Access denied: You cannot connect that User - it may not exist` ); }); @@ -192,7 +192,7 @@ describe(`Custom mutations`, () => { expect(data).toEqual({ addToCart: null }); expect(errors).toHaveLength(1); expect(errors![0].message).toEqual( - `An error occured while resolving relationship fields.\n - CartItem.product: Access denied: You cannot perform the 'connect' operation on the item '{"id":"${product.id}"}'. It may not exist.` + `An error occured while resolving relationship fields.\n - CartItem.product: Access denied: You cannot connect that Product - it may not exist` ); }); @@ -216,7 +216,7 @@ describe(`Custom mutations`, () => { expect(data).toEqual({ addToCart: null }); expect(errors).toHaveLength(1); expect(errors![0].message).toEqual( - `An error occured while resolving relationship fields.\n - CartItem.product: Access denied: You cannot perform the 'connect' operation on the item '{"id":"${product.id}"}'. It may not exist.` + `An error occured while resolving relationship fields.\n - CartItem.product: Access denied: You cannot connect that Product - it may not exist` ); }); diff --git a/examples/testing/example.test.ts b/examples/testing/example.test.ts index 27f0d1f479c..5da9a5f5cc1 100644 --- a/examples/testing/example.test.ts +++ b/examples/testing/example.test.ts @@ -89,7 +89,7 @@ test('Check access control by running updateTask as a specific user via context. expect(errors).toHaveLength(1); expect(errors![0].path).toEqual(['updateTask']); expect(errors![0].message).toEqual( - `Access denied: You cannot perform the 'update' operation on the item '{"id":"${task.id}"}'. It may not exist.` + `Access denied: You cannot update that Task - it may not exist` ); } @@ -123,7 +123,7 @@ test('Check access control by running updateTask as a specific user via context. expect(errors).toHaveLength(1); expect(errors![0].path).toEqual(['updateTask']); expect(errors![0].message).toEqual( - `Access denied: You cannot perform the 'update' operation on the item '{"id":"${task.id}"}'. It may not exist.` + `Access denied: You cannot update that Task - it may not exist` ); } }); diff --git a/packages/core/src/lib/core/access-control.ts b/packages/core/src/lib/core/access-control.ts index 120fed9058e..bbd97795590 100644 --- a/packages/core/src/lib/core/access-control.ts +++ b/packages/core/src/lib/core/access-control.ts @@ -20,6 +20,23 @@ import { accessReturnError, extensionError } from './graphql-errors'; import { InitialisedList } from './types-for-lists'; import { InputFilter } from './where-inputs'; +export function cannotForItem(operation: string, list: InitialisedList) { + return ( + `You cannot ${operation} that ${list.listKey}` + + (operation === 'create' ? '' : ' - it may not exist') + ); +} + +export function cannotForItemFields( + operation: string, + list: InitialisedList, + fieldsDenied: string[] +) { + return `You cannot ${operation} that ${ + list.listKey + } - you cannot ${operation} the fields ${JSON.stringify(fieldsDenied)}`; +} + export async function getOperationAccess( list: InitialisedList, context: KeystoneContext, @@ -56,60 +73,77 @@ export async function getAccessFilters( context: KeystoneContext, operation: 'update' | 'query' | 'delete' ): Promise { - const args = { operation, session: context.session, listKey: list.listKey, context }; - // Check the mutation access const access = list.access.filter[operation]; try { - // @ts-ignore - let filters = typeof access === 'function' ? await access(args) : access; - if (typeof filters === 'boolean') { - return filters; - } + const filters = await access({ + operation, + session: context.session, + list: list.listKey, + context, + } as any); // TODO: FIXME + if (typeof filters === 'boolean') return filters; + const schema = context.sudo().graphql.schema; const whereInput = assertInputObjectType(schema.getType(getGqlNames(list).whereInputName)); const result = coerceAndValidateForGraphQLInput(schema, whereInput, filters); - if (result.kind === 'valid') { - return result.value; - } + if (result.kind === 'valid') return result.value; throw result.error; } catch (error: any) { throw extensionError('Access control', [ - { error, tag: `${args.listKey}.access.filter.${args.operation}` }, + { error, tag: `${list.listKey}.access.filter.${operation}` }, ]); } } +export type ResolvedFieldAccessControl = { + create: IndividualFieldAccessControl>; + read: IndividualFieldAccessControl>; + update: IndividualFieldAccessControl>; +}; + export function parseFieldAccessControl( access: FieldAccessControl | undefined ): ResolvedFieldAccessControl { - if (typeof access === 'boolean' || typeof access === 'function') { + if (typeof access === 'function') { return { read: access, create: access, update: access }; } - // note i'm intentionally not using spread here because typescript can't express an optional property which cannot be undefined so spreading would mean there is a possibility that someone could pass {access: undefined} or {access:{read: undefined}} and bad things would happen + return { read: access?.read ?? (() => true), create: access?.create ?? (() => true), update: access?.update ?? (() => true), - // delete: not supported }; } -export type ResolvedFieldAccessControl = { - read: IndividualFieldAccessControl>; - create: IndividualFieldAccessControl>; - update: IndividualFieldAccessControl>; +export type ResolvedListAccessControl = { + operation: { + query: ListOperationAccessControl<'query', BaseListTypeInfo>; + create: ListOperationAccessControl<'create', BaseListTypeInfo>; + update: ListOperationAccessControl<'update', BaseListTypeInfo>; + delete: ListOperationAccessControl<'delete', BaseListTypeInfo>; + }; + filter: { + query: ListFilterAccessControl<'query', BaseListTypeInfo>; + // create: not supported + update: ListFilterAccessControl<'update', BaseListTypeInfo>; + delete: ListFilterAccessControl<'delete', BaseListTypeInfo>; + }; + item: { + // query: not supported + create: CreateListItemAccessControl; + update: UpdateListItemAccessControl; + delete: DeleteListItemAccessControl; + }; }; export function parseListAccessControl( access: ListAccessControl ): ResolvedListAccessControl { - let item, filter, operation; - if (typeof access === 'function') { return { operation: { - create: access, query: access, + create: access, update: access, delete: access, }, @@ -126,67 +160,34 @@ export function parseListAccessControl( }; } - if (typeof access?.operation === 'function') { + let { operation, filter, item } = access; + if (typeof operation === 'function') { operation = { - create: access.operation, - query: access.operation, - update: access.operation, - delete: access.operation, - }; - } else { - // Note I'm intentionally not using spread here because typescript can't express - // an optional property which cannot be undefined so spreading would mean there - // is a possibility that someone could pass { access: undefined } or - // { access: { read: undefined } } and bad things would happen. - operation = { - create: access?.operation?.create ?? (() => true), - query: access?.operation?.query ?? (() => true), - update: access?.operation?.update ?? (() => true), - delete: access?.operation?.delete ?? (() => true), + query: operation, + create: operation, + update: operation, + delete: operation, }; } - if (typeof access?.filter === 'boolean' || typeof access?.filter === 'function') { - filter = { query: access.filter, update: access.filter, delete: access.filter }; - } else { - filter = { + return { + operation: { + query: operation.query ?? (() => true), + create: operation.create ?? (() => true), + update: operation.update ?? (() => true), + delete: operation.delete ?? (() => true), + }, + filter: { + query: filter?.query ?? (() => true), // create: not supported - query: access?.filter?.query ?? (() => true), - update: access?.filter?.update ?? (() => true), - delete: access?.filter?.delete ?? (() => true), - }; - } - - if (typeof access?.item === 'boolean' || typeof access?.item === 'function') { - item = { create: access.item, update: access.item, delete: access.item }; - } else { - item = { - create: access?.item?.create ?? (() => true), - // read: not supported - update: access?.item?.update ?? (() => true), - delete: access?.item?.delete ?? (() => true), - }; - } - return { operation, filter, item }; -} - -export type ResolvedListAccessControl = { - operation: { - create: ListOperationAccessControl<'create', BaseListTypeInfo>; - query: ListOperationAccessControl<'query', BaseListTypeInfo>; - update: ListOperationAccessControl<'update', BaseListTypeInfo>; - delete: ListOperationAccessControl<'delete', BaseListTypeInfo>; - }; - filter: { - // create: not supported - query: ListFilterAccessControl<'query', BaseListTypeInfo>; - update: ListFilterAccessControl<'update', BaseListTypeInfo>; - delete: ListFilterAccessControl<'delete', BaseListTypeInfo>; + update: filter?.update ?? (() => true), + delete: filter?.delete ?? (() => true), + }, + item: { + // query: not supported + create: item?.create ?? (() => true), + update: item?.update ?? (() => true), + delete: item?.delete ?? (() => true), + }, }; - item: { - create: CreateListItemAccessControl; - // query: not supported - update: UpdateListItemAccessControl; - delete: DeleteListItemAccessControl; - }; -}; +} diff --git a/packages/core/src/lib/core/mutations/access-control.ts b/packages/core/src/lib/core/mutations/access-control.ts index c278eaa58bc..a02b9fa28e1 100644 --- a/packages/core/src/lib/core/mutations/access-control.ts +++ b/packages/core/src/lib/core/mutations/access-control.ts @@ -1,8 +1,9 @@ -import { KeystoneContext } from '../../../types'; +import { BaseItem, KeystoneContext } from '../../../types'; import { accessDeniedError, accessReturnError, extensionError } from '../graphql-errors'; import { mapUniqueWhereToWhere } from '../queries/resolvers'; import { InitialisedList } from '../types-for-lists'; import { runWithPrisma } from '../utils'; +import { cannotForItem, cannotForItemFields } from '../access-control'; import { InputFilter, resolveUniqueWhereInput, @@ -11,13 +12,6 @@ import { UniquePrismaFilter, } from '../where-inputs'; -const missingItem = (operation: string, uniqueWhere: UniquePrismaFilter) => - accessDeniedError( - `You cannot perform the '${operation}' operation on the item '${JSON.stringify( - uniqueWhere - )}'. It may not exist.` - ); - async function getFilteredItem( list: InitialisedList, context: KeystoneContext, @@ -25,23 +19,21 @@ async function getFilteredItem( accessFilters: boolean | InputFilter, operation: 'update' | 'delete' ) { + // early exit if they want to exclude everything if (accessFilters === false) { - // Early exit if they want to exclude everything - throw accessDeniedError( - `You cannot perform the '${operation}' operation on the list '${list.listKey}'.` - ); + throw accessDeniedError(cannotForItem(operation, list)); } - // Merge the filter access control and try to get the item. + // merge the filter access control and try to get the item let where = mapUniqueWhereToWhere(uniqueWhere); if (typeof accessFilters === 'object') { where = { AND: [where, await resolveWhereInput(accessFilters, list, context)] }; } + const item = await runWithPrisma(context, list, model => model.findFirst({ where })); - if (item === null) { - throw missingItem(operation, uniqueWhere); - } - return item; + if (item !== null) return item; + + throw accessDeniedError(cannotForItem(operation, list)); } export async function checkUniqueItemExists( @@ -52,164 +44,156 @@ export async function checkUniqueItemExists( ) { // Validate and resolve the input filter const uniqueWhere = await resolveUniqueWhereInput(uniqueInput, foreignList, context); + // Check whether the item exists (from this users POV). try { const item = await context.db[foreignList.listKey].findOne({ where: uniqueInput }); - if (item === null) { - throw missingItem(operation, uniqueWhere); - } - } catch (err) { - throw missingItem(operation, uniqueWhere); - } - return uniqueWhere; -} + if (item !== null) return uniqueWhere; + } catch (err) {} -export async function getAccessControlledItemForDelete( - list: InitialisedList, - context: KeystoneContext, - uniqueWhere: UniquePrismaFilter, - accessFilters: boolean | InputFilter -) { - const operation = 'delete' as const; - // Apply the filter access control. Will throw an accessDeniedError if the item isn't found. - const item = await getFilteredItem(list, context, uniqueWhere!, accessFilters, operation); - - // Apply item level access control - const access = list.access.item[operation]; - const args = { operation, session: context.session, listKey: list.listKey, context, item }; + throw accessDeniedError(cannotForItem(operation, foreignList)); +} - // List level 'item' access control - let result; +async function enforceListLevelAccessControl({ + context, + operation, + list, + item, + inputData, +}: { + context: KeystoneContext; + operation: 'create' | 'update' | 'delete'; + list: InitialisedList; + item: BaseItem | undefined; + inputData: Record; +}) { + let accepted: unknown; // should be boolean, but dont trust, it might accidentally be a filter try { - result = await access(args); + // apply access.item.* controls + if (operation === 'create') { + const itemAccessControl = list.access.item[operation]; + accepted = await itemAccessControl({ + operation, + session: context.session, + listKey: list.listKey, + context, + inputData, + }); + } else if (operation === 'update' && item !== undefined) { + const itemAccessControl = list.access.item[operation]; + accepted = await itemAccessControl({ + operation, + session: context.session, + listKey: list.listKey, + context, + item, + inputData, + }); + } else if (operation === 'delete' && item !== undefined) { + const itemAccessControl = list.access.item[operation]; + accepted = await itemAccessControl({ + operation, + session: context.session, + listKey: list.listKey, + context, + item, + }); + } } catch (error: any) { throw extensionError('Access control', [ - { error, tag: `${args.listKey}.access.item.${args.operation}` }, + { error, tag: `${list.listKey}.access.item.${operation}` }, ]); } - const resultType = typeof result; + // short circuit the safe path + if (accepted === true) return; - // It's important that we don't cast objects to truthy values, as there's a strong chance that the user - // has accidentally tried to return a filter. - if (resultType !== 'boolean') { + if (typeof accepted !== 'boolean') { throw accessReturnError([ { - tag: `${args.listKey}.access.item.${args.operation}`, - returned: resultType, + tag: `${list.listKey}.access.item.${operation}`, + returned: typeof accepted, }, ]); } - if (!result) { - throw accessDeniedError( - `You cannot perform the '${operation}' operation on the item '${JSON.stringify( - uniqueWhere - )}'. It may not exist.` - ); - } - - // No field level access control for delete - - return item; + throw accessDeniedError(cannotForItem(operation, list)); } -export async function getAccessControlledItemForUpdate( - list: InitialisedList, - context: KeystoneContext, - uniqueWhere: UniquePrismaFilter, - accessFilters: boolean | InputFilter, - inputData: Record -) { - const operation = 'update' as const; - // Apply the filter access control. Will throw an accessDeniedError if the item isn't found. - const item = await getFilteredItem(list, context, uniqueWhere!, accessFilters, operation); - - // Apply item level access control - const access = list.access.item[operation]; - const args = { - operation, - session: context.session, - listKey: list.listKey, - context, - item, - inputData, - }; - - // List level 'item' access control - let result; - try { - result = await access(args); - } catch (error: any) { - throw extensionError('Access control', [ - { error, tag: `${args.listKey}.access.item.${args.operation}` }, - ]); - } - const resultType = typeof result; - - // It's important that we don't cast objects to truthy values, as there's a strong chance that the user - // has accidentally tried to return a filter. - if (resultType !== 'boolean') { - throw accessReturnError([ - { - tag: `${args.listKey}.access.item.${args.operation}`, - returned: resultType, - }, - ]); - } - - if (!result) { - throw accessDeniedError( - `You cannot perform the '${operation}' operation on the item '${JSON.stringify( - uniqueWhere - )}'. It may not exist.` - ); - } - - // Field level 'item' access control +async function enforceFieldLevelAccessControl({ + context, + operation, + list, + item, + inputData, +}: { + context: KeystoneContext; + operation: 'create' | 'update'; + list: InitialisedList; + item: BaseItem | undefined; + inputData: Record; +}) { const nonBooleans: { tag: string; returned: string }[] = []; const fieldsDenied: string[] = []; const accessErrors: { error: Error; tag: string }[] = []; - await Promise.all( + + await Promise.allSettled( Object.keys(inputData).map(async fieldKey => { - let result; + let accepted: unknown; // should be boolean, but dont trust try { - result = - typeof list.fields[fieldKey].access[operation] === 'function' - ? await list.fields[fieldKey].access[operation]({ ...args, fieldKey }) - : access; + // apply fields.[fieldKey].access.* controls + if (operation === 'create') { + const fieldAccessControl = list.fields[fieldKey].access[operation]; + accepted = await fieldAccessControl({ + operation, + session: context.session, + listKey: list.listKey, + fieldKey, + context, + inputData: inputData as any, // FIXME + }); + } else if (operation === 'update' && item !== undefined) { + const fieldAccessControl = list.fields[fieldKey].access[operation]; + accepted = await fieldAccessControl({ + operation, + session: context.session, + listKey: list.listKey, + fieldKey, + context, + item, + inputData, + }); + } } catch (error: any) { - accessErrors.push({ error, tag: `${args.listKey}.${fieldKey}.access.${args.operation}` }); + accessErrors.push({ error, tag: `${list.listKey}.${fieldKey}.access.${operation}` }); return; } - if (typeof result !== 'boolean') { + + // short circuit the safe path + if (accepted === true) return; + fieldsDenied.push(fieldKey); + + // wrong type? + if (typeof accepted !== 'boolean') { nonBooleans.push({ - tag: `${args.listKey}.${fieldKey}.access.${args.operation}`, - returned: typeof result, + tag: `${list.listKey}.${fieldKey}.access.${operation}`, + returned: typeof accepted, }); - } else if (!result) { - fieldsDenied.push(fieldKey); } }) ); - if (accessErrors.length) { - throw extensionError('Access control', accessErrors); - } - if (nonBooleans.length) { throw accessReturnError(nonBooleans); } - if (fieldsDenied.length) { - throw accessDeniedError( - `You cannot perform the '${operation}' operation on the item '${JSON.stringify( - uniqueWhere - )}'. You cannot ${operation} the fields ${JSON.stringify(fieldsDenied)}.` - ); + if (accessErrors.length) { + throw extensionError('Access control', accessErrors); } - return item; + if (fieldsDenied.length) { + throw accessDeniedError(cannotForItemFields(operation, list, fieldsDenied)); + } } export async function applyAccessControlForCreate( @@ -217,87 +201,70 @@ export async function applyAccessControlForCreate( context: KeystoneContext, inputData: Record ) { - const operation = 'create' as const; + await enforceListLevelAccessControl({ + context, + operation: 'create', + list, + inputData, + item: undefined, + }); - // Apply item level access control - const access = list.access.item[operation]; - const args = { - operation, - session: context.session, - listKey: list.listKey, + await enforceFieldLevelAccessControl({ context, + operation: 'create', + list, inputData, - }; + item: undefined, + }); +} - // List level 'item' access control - let result; - try { - result = await access(args); - } catch (error: any) { - throw extensionError('Access control', [ - { error, tag: `${args.listKey}.access.item.${args.operation}` }, - ]); - } +export async function getAccessControlledItemForUpdate( + list: InitialisedList, + context: KeystoneContext, + uniqueWhere: UniquePrismaFilter, + accessFilters: boolean | InputFilter, + inputData: Record +) { + // apply access.filter.* controls + const item = await getFilteredItem(list, context, uniqueWhere!, accessFilters, 'update'); - const resultType = typeof result; + await enforceListLevelAccessControl({ + context, + operation: 'update', + list, + inputData, + item, + }); - // It's important that we don't cast objects to truthy values, as there's a strong chance that the user - // has accidentally tried to return a filter. - if (resultType !== 'boolean') { - throw accessReturnError([ - { - tag: `${args.listKey}.access.item.${args.operation}`, - returned: resultType, - }, - ]); - } + await enforceFieldLevelAccessControl({ + context, + operation: 'update', + list, + inputData, + item, + }); - if (!result) { - throw accessDeniedError( - `You cannot perform the '${operation}' operation on the item '${JSON.stringify(inputData)}'.` - ); - } + return item; +} - // Field level 'item' access control - const nonBooleans: { tag: string; returned: string }[] = []; - const fieldsDenied: string[] = []; - const accessErrors: { error: Error; tag: string }[] = []; - await Promise.all( - Object.keys(inputData).map(async fieldKey => { - let result; - try { - result = - typeof list.fields[fieldKey].access[operation] === 'function' - ? await list.fields[fieldKey].access[operation]({ ...args, fieldKey }) - : access; - } catch (error: any) { - accessErrors.push({ error, tag: `${args.listKey}.${fieldKey}.access.${args.operation}` }); - return; - } - if (typeof result !== 'boolean') { - nonBooleans.push({ - tag: `${args.listKey}.${fieldKey}.access.${args.operation}`, - returned: typeof result, - }); - } else if (!result) { - fieldsDenied.push(fieldKey); - } - }) - ); +export async function getAccessControlledItemForDelete( + list: InitialisedList, + context: KeystoneContext, + uniqueWhere: UniquePrismaFilter, + accessFilters: boolean | InputFilter +) { + // apply access.filter.* controls + const item = await getFilteredItem(list, context, uniqueWhere!, accessFilters, 'delete'); - if (accessErrors.length) { - throw extensionError('Access control', accessErrors); - } + await enforceListLevelAccessControl({ + context, + operation: 'delete', + list, + item, + inputData: {}, + }); - if (nonBooleans.length) { - throw accessReturnError(nonBooleans); - } + // no field level access control for delete - if (fieldsDenied.length) { - throw accessDeniedError( - `You cannot perform the '${operation}' operation on the item '${JSON.stringify( - inputData - )}'. You cannot ${operation} the fields ${JSON.stringify(fieldsDenied)}.` - ); - } + return item; } diff --git a/packages/core/src/lib/core/mutations/create-update.ts b/packages/core/src/lib/core/mutations/create-update.ts index 945cda25ba2..1b474b99503 100644 --- a/packages/core/src/lib/core/mutations/create-update.ts +++ b/packages/core/src/lib/core/mutations/create-update.ts @@ -16,7 +16,7 @@ import { relationshipError, resolverError, } from '../graphql-errors'; -import { getOperationAccess, getAccessFilters } from '../access-control'; +import { cannotForItem, getOperationAccess, getAccessFilters } from '../access-control'; import { checkFilterOrderAccess } from '../filter-order-access'; import { RelationshipErrors, @@ -34,16 +34,8 @@ import { validateUpdateCreate } from './validation'; async function createSingle( { data: rawData }: { data: Record }, list: InitialisedList, - context: KeystoneContext, - operationAccess: boolean + context: KeystoneContext ) { - // Operation level access control - if (!operationAccess) { - throw accessDeniedError( - `You cannot perform the 'create' operation on the list '${list.listKey}'.` - ); - } - // Item access control. Will throw an accessDeniedError if not allowed. await applyAccessControlForCreate(list, context, rawData); @@ -74,10 +66,10 @@ export class NestedMutationState { async create(data: Record, list: InitialisedList) { const context = this.#context; - // Check operation permission to pass into single operation const operationAccess = await getOperationAccess(list, context, 'create'); + if (!operationAccess) throw accessDeniedError(cannotForItem('create', list)); - const { item, afterOperation } = await createSingle({ data }, list, context, operationAccess); + const { item, afterOperation } = await createSingle({ data }, list, context); this.#afterOperations.push(() => afterOperation(item)); return { id: item.id as IdType }; @@ -93,10 +85,10 @@ export async function createOne( list: InitialisedList, context: KeystoneContext ) { - // Check operation permission to pass into single operation const operationAccess = await getOperationAccess(list, context, 'create'); + if (!operationAccess) throw accessDeniedError(cannotForItem('create', list)); - const { item, afterOperation } = await createSingle(createInput, list, context, operationAccess); + const { item, afterOperation } = await createSingle(createInput, list, context); await afterOperation(item); @@ -108,14 +100,14 @@ export async function createMany( list: InitialisedList, context: KeystoneContext ) { - // Check operation permission to pass into single operation const operationAccess = await getOperationAccess(list, context, 'create'); return createInputs.data.map(async data => { - const { item, afterOperation } = await createSingle({ data }, list, context, operationAccess); + // throw for each attempt + if (!operationAccess) throw accessDeniedError(cannotForItem('create', list)); + const { item, afterOperation } = await createSingle({ data }, list, context); await afterOperation(item); - return item; }); } @@ -124,16 +116,8 @@ async function updateSingle( updateInput: { where: UniqueInputFilter; data: Record }, list: InitialisedList, context: KeystoneContext, - accessFilters: boolean | InputFilter, - operationAccess: boolean + accessFilters: boolean | InputFilter ) { - // Operation level access control - if (!operationAccess) { - throw accessDeniedError( - `You cannot perform the 'update' operation on the list '${list.listKey}'.` - ); - } - const { where: uniqueInput, data: rawData } = updateInput; // Validate and resolve the input filter const uniqueWhere = await resolveUniqueWhereInput(uniqueInput, list, context); @@ -174,13 +158,13 @@ export async function updateOne( list: InitialisedList, context: KeystoneContext ) { - // Check operation permission to pass into single operation const operationAccess = await getOperationAccess(list, context, 'update'); + if (!operationAccess) throw accessDeniedError(cannotForItem('update', list)); // Get list-level access control filters const accessFilters = await getAccessFilters(list, context, 'update'); - return updateSingle(updateInput, list, context, accessFilters, operationAccess); + return updateSingle(updateInput, list, context, accessFilters); } export async function updateMany( @@ -188,15 +172,17 @@ export async function updateMany( list: InitialisedList, context: KeystoneContext ) { - // Check operation permission to pass into single operation const operationAccess = await getOperationAccess(list, context, 'update'); // Get list-level access control filters const accessFilters = await getAccessFilters(list, context, 'update'); - return data.map(async updateInput => - updateSingle(updateInput, list, context, accessFilters, operationAccess) - ); + return data.map(async updateInput => { + // throw for each attempt + if (!operationAccess) throw accessDeniedError(cannotForItem('update', list)); + + return updateSingle(updateInput, list, context, accessFilters); + }); } async function getResolvedData( diff --git a/packages/core/src/lib/core/mutations/delete.ts b/packages/core/src/lib/core/mutations/delete.ts index cfb1b8a098a..64d2cc2a6ff 100644 --- a/packages/core/src/lib/core/mutations/delete.ts +++ b/packages/core/src/lib/core/mutations/delete.ts @@ -1,5 +1,5 @@ import { KeystoneContext } from '../../../types'; -import { getOperationAccess, getAccessFilters } from '../access-control'; +import { cannotForItem, getOperationAccess, getAccessFilters } from '../access-control'; import { checkFilterOrderAccess } from '../filter-order-access'; import { accessDeniedError } from '../graphql-errors'; import { InitialisedList } from '../types-for-lists'; @@ -13,16 +13,8 @@ async function deleteSingle( uniqueInput: UniqueInputFilter, list: InitialisedList, context: KeystoneContext, - accessFilters: boolean | InputFilter, - operationAccess: boolean + accessFilters: boolean | InputFilter ) { - // Operation level access control - if (!operationAccess) { - throw accessDeniedError( - `You cannot perform the 'delete' operation on the list '${list.listKey}'.` - ); - } - // Validate and resolve the input filter const uniqueWhere = await resolveUniqueWhereInput(uniqueInput, list, context); @@ -68,15 +60,17 @@ export async function deleteMany( list: InitialisedList, context: KeystoneContext ) { - // Check operation permission to pass into single operation const operationAccess = await getOperationAccess(list, context, 'delete'); // Check filter permission to pass into single operation const accessFilters = await getAccessFilters(list, context, 'delete'); - return uniqueInputs.map(async uniqueInput => - deleteSingle(uniqueInput, list, context, accessFilters, operationAccess) - ); + return uniqueInputs.map(async uniqueInput => { + // throw for each item + if (!operationAccess) throw accessDeniedError(cannotForItem('delete', list)); + + return deleteSingle(uniqueInput, list, context, accessFilters); + }); } export async function deleteOne( @@ -84,11 +78,11 @@ export async function deleteOne( list: InitialisedList, context: KeystoneContext ) { - // Check operation permission to pass into single operation const operationAccess = await getOperationAccess(list, context, 'delete'); + if (!operationAccess) throw accessDeniedError(cannotForItem('delete', list)); // Check filter permission to pass into single operation const accessFilters = await getAccessFilters(list, context, 'delete'); - return deleteSingle(uniqueInput, list, context, accessFilters, operationAccess); + return deleteSingle(uniqueInput, list, context, accessFilters); } diff --git a/packages/core/src/types/config/access-control.ts b/packages/core/src/types/config/access-control.ts index eeb1b27f9bb..5be5d5f5220 100644 --- a/packages/core/src/types/config/access-control.ts +++ b/packages/core/src/types/config/access-control.ts @@ -8,67 +8,59 @@ export type BaseAccessArgs = { context: KeystoneContextFromListTypeInfo; }; -// List Filter Access +export type AccessOperation = 'create' | 'query' | 'update' | 'delete'; +export type FilterOperation = 'query' | 'update' | 'delete'; +export type ItemOperation = 'create' | 'update' | 'delete'; -type FilterOutput = - | boolean - | ListTypeInfo['inputs']['where']; +export type ListOperationAccessControl< + Operation extends AccessOperation, + ListTypeInfo extends BaseListTypeInfo +> = (args: BaseAccessArgs & { operation: Operation }) => MaybePromise; export type ListFilterAccessControl< - Operation extends 'query' | 'update' | 'delete', + Operation extends FilterOperation, ListTypeInfo extends BaseListTypeInfo > = ( args: BaseAccessArgs & { operation: Operation } -) => MaybePromise>; - -// List Item Access - -type CreateItemAccessArgs = BaseAccessArgs & { - operation: 'create'; - /** - * The input passed in from the GraphQL API - */ - inputData: ListTypeInfo['inputs']['create']; -}; +) => MaybePromise; export type CreateListItemAccessControl = ( - args: CreateItemAccessArgs + args: BaseAccessArgs & { + operation: 'create'; + + /** + * The input passed in from the GraphQL API + */ + inputData: ListTypeInfo['inputs']['create']; + } ) => MaybePromise; -type UpdateItemAccessArgs = BaseAccessArgs & { - operation: 'update'; - /** - * The item being updated - */ - item: ListTypeInfo['item']; - /** - * The input passed in from the GraphQL API - */ - inputData: ListTypeInfo['inputs']['update']; -}; - export type UpdateListItemAccessControl = ( - args: UpdateItemAccessArgs -) => MaybePromise; + args: BaseAccessArgs & { + operation: 'update'; -type DeleteItemAccessArgs = BaseAccessArgs & { - operation: 'delete'; - /** - * The item being deleted - */ - item: ListTypeInfo['item']; -}; + /** + * The item being updated + */ + item: ListTypeInfo['item']; -export type DeleteListItemAccessControl = ( - args: DeleteItemAccessArgs + /** + * The input passed in from the GraphQL API + */ + inputData: ListTypeInfo['inputs']['update']; + } ) => MaybePromise; -export type AccessOperation = 'create' | 'query' | 'update' | 'delete'; +export type DeleteListItemAccessControl = ( + args: BaseAccessArgs & { + operation: 'delete'; -export type ListOperationAccessControl< - Operation extends AccessOperation, - ListTypeInfo extends BaseListTypeInfo -> = (args: BaseAccessArgs & { operation: Operation }) => MaybePromise; + /** + * The item being deleted + */ + item: ListTypeInfo['item']; + } +) => MaybePromise; type ListAccessControlFunction = ( args: BaseAccessArgs & { operation: AccessOperation } @@ -77,7 +69,7 @@ type ListAccessControlFunction = ( type ListAccessControlObject = { // These functions should return `true` if access is allowed or `false` if access is denied. operation: - | ListOperationAccessControl + | ListOperationAccessControl | { query: ListOperationAccessControl<'query', ListTypeInfo>; create: ListOperationAccessControl<'create', ListTypeInfo>; @@ -91,16 +83,15 @@ type ListAccessControlObject = { // - boolean true/false. If false, treated as a filter that never matches. filter?: { query?: ListFilterAccessControl<'query', ListTypeInfo>; + // create?: not supported update?: ListFilterAccessControl<'update', ListTypeInfo>; delete?: ListFilterAccessControl<'delete', ListTypeInfo>; - // create: not supported: FIXME: Add explicit check that people don't try this. - // FIXME: Write tests for parseAccessControl. }; // These rules are applied to each item being operated on individually. They return `true` or `false`, // and if false, an access denied error will be returned for the individual operation. item?: { - // query: not supported + // read?: not supported create?: CreateListItemAccessControl; update?: UpdateListItemAccessControl; delete?: DeleteListItemAccessControl; @@ -133,28 +124,49 @@ export type ListAccessControl = export type IndividualFieldAccessControl = (args: Args) => MaybePromise; export type FieldCreateItemAccessArgs = - CreateItemAccessArgs & { fieldKey: string }; + BaseAccessArgs & { + operation: 'create'; + fieldKey: string; + /** + * The input passed in from the GraphQL API + */ + inputData: ListTypeInfo['item']; + }; export type FieldReadItemAccessArgs = BaseAccessArgs & { operation: 'read'; fieldKey: string; + /** + * The item being read + */ item: ListTypeInfo['item']; }; export type FieldUpdateItemAccessArgs = - UpdateItemAccessArgs & { fieldKey: string }; + BaseAccessArgs & { + operation: 'update'; + fieldKey: string; + /** + * The item being updated + */ + item: ListTypeInfo['item']; + /** + * The input passed in from the GraphQL API + */ + inputData: ListTypeInfo['inputs']['update']; + }; export type FieldAccessControl = + | IndividualFieldAccessControl< + | FieldReadItemAccessArgs + | FieldCreateItemAccessArgs + | FieldUpdateItemAccessArgs + // delete: not supported + > | { read?: IndividualFieldAccessControl>; create?: IndividualFieldAccessControl>; update?: IndividualFieldAccessControl>; - // filter?: COMING SOON - // orderBy?: COMING SOON - } - | IndividualFieldAccessControl< - | FieldCreateItemAccessArgs - | FieldReadItemAccessArgs - | FieldUpdateItemAccessArgs - >; + // delete: not supported + }; diff --git a/tests/api-tests/access-control/field-access.test.ts b/tests/api-tests/access-control/field-access.test.ts index 71a7e622068..5daeebabf8c 100644 --- a/tests/api-tests/access-control/field-access.test.ts +++ b/tests/api-tests/access-control/field-access.test.ts @@ -86,7 +86,8 @@ describe(`Field access`, () => { describe('create', () => { fieldMatrix.forEach(access => { test(`field not allowed: ${JSON.stringify(access)}`, async () => { - const createMutationName = `create${nameFn[mode](listAccess)}`; + const listKey = nameFn[mode](listAccess); + const createMutationName = `create${listKey}`; const fieldName = getFieldName(access); const query = `mutation { ${createMutationName}(data: { ${fieldName}: "bar" }) { id ${fieldName} } }`; const { data, errors } = await context.graphql.raw({ query }); @@ -95,7 +96,7 @@ describe(`Field access`, () => { expectAccessDenied(errors, [ { path: [createMutationName], - msg: `You cannot perform the 'create' operation on the item '{"${fieldName}":"bar"}'. You cannot create the fields ["${fieldName}"].`, + msg: `You cannot create that ${listKey} - you cannot create the fields ["${fieldName}"]`, }, ]); } else { @@ -123,9 +124,10 @@ describe(`Field access`, () => { describe('update', () => { fieldMatrix.forEach(access => { test(`field not allowed: ${JSON.stringify(access)}`, async () => { + const listKey = nameFn[mode](listAccess); const item = items[listKey][0]; const fieldName = getFieldName(access); - const updateMutationName = `update${nameFn[mode](listAccess)}`; + const updateMutationName = `update${listKey}`; const query = `mutation { ${updateMutationName}(where: { id: "${item.id}" }, data: { ${fieldName}: "bar" }) { id ${fieldName} } }`; const { data, errors } = await context.graphql.raw({ query }); if (!access.update) { @@ -133,7 +135,7 @@ describe(`Field access`, () => { expectAccessDenied(errors, [ { path: [updateMutationName], - msg: `You cannot perform the 'update' operation on the item '{"id":"${item.id}"}'. You cannot update the fields ["${fieldName}"].`, + msg: `You cannot update that ${listKey} - you cannot update the fields ["${fieldName}"]`, }, ]); } else { diff --git a/tests/api-tests/access-control/list-access.test.ts b/tests/api-tests/access-control/list-access.test.ts index f6943c6f17f..64322a6a919 100644 --- a/tests/api-tests/access-control/list-access.test.ts +++ b/tests/api-tests/access-control/list-access.test.ts @@ -78,21 +78,7 @@ describe(`List access`, () => { const query = `mutation { ${createMutationName}(data: { name: "bar" }) { id } }`; const { data, errors } = (await context.graphql.raw({ query })) as ExecutionResult; if (!access.create) { - if (mode === 'operation') { - expectNoAccess( - data, - errors, - createMutationName, - `You cannot perform the 'create' operation on the list '${nameFn[mode](access)}'.` - ); - } else { - expectNoAccess( - data, - errors, - createMutationName, - `You cannot perform the 'create' operation on the item '{"name":"bar"}'.` - ); - } + expectNoAccess(data, errors, createMutationName, `You cannot create that ${listKey}`); } else { expect(errors).toBe(undefined); expect(data![createMutationName]).not.toEqual(null); @@ -107,21 +93,12 @@ describe(`List access`, () => { const query = `mutation { ${createMutationName}(data: [{ name: "bar" }]) { id } }`; const { data, errors } = (await context.graphql.raw({ query })) as ExecutionResult; if (!access.create) { - if (mode === 'operation') { - expectNoAccessMany( - data, - errors, - createMutationName, - `You cannot perform the 'create' operation on the list '${nameFn[mode](access)}'.` - ); - } else { - expectNoAccessMany( - data, - errors, - createMutationName, - `You cannot perform the 'create' operation on the item '{"name":"bar"}'.` - ); - } + expectNoAccessMany( + data, + errors, + createMutationName, + `You cannot create that ${listKey}` + ); } else { expect(errors).toBe(undefined); expect(data![createMutationName]).not.toEqual(null); @@ -140,14 +117,16 @@ describe(`List access`, () => { describe(mode, () => { listAccessVariations.forEach(access => { test(`single not existing: ${JSON.stringify(access)}`, async () => { - const { itemQueryName } = context.gqlNames(nameFn[mode](access)); + const listKey = nameFn[mode](access); + const { itemQueryName } = context.gqlNames(listKey); const query = `query { ${itemQueryName}(where: { id: "${FAKE_ID}" }) { id } }`; const { data, errors } = (await context.graphql.raw({ query })) as ExecutionResult; expect(errors).toBe(undefined); expect(data![itemQueryName]).toBe(null); }); test(`multiple not existing: ${JSON.stringify(access)}`, async () => { - const _items = await context.query[nameFn[mode](access)].findMany({ + const listKey = nameFn[mode](access); + const _items = await context.query[listKey].findMany({ where: { id: { in: [FAKE_ID, FAKE_ID_2] } }, }); expect(_items).toHaveLength(0); @@ -160,7 +139,8 @@ describe(`List access`, () => { describe(mode, () => { listAccessVariations.forEach(access => { test(`'all' denied: ${JSON.stringify(access)}`, async () => { - const allQueryName = context.gqlNames(nameFn[mode](access)).listQueryName; + const listKey = nameFn[mode](access); + const allQueryName = context.gqlNames(listKey).listQueryName; const query = `query { ${allQueryName} { id } }`; const { data, errors } = (await context.graphql.raw({ query })) as ExecutionResult; expect(errors).toBe(undefined); @@ -172,9 +152,8 @@ describe(`List access`, () => { }); test(`count denied: ${JSON.stringify(access)}`, async () => { - const countName = `${ - nameFn[mode](access).slice(0, 1).toLowerCase() + nameFn[mode](access).slice(1) - }sCount`; + const listKey = nameFn[mode](access); + const countName = `${listKey.slice(0, 1).toLowerCase() + listKey.slice(1)}sCount`; const query = `query { ${countName} }`; const { data, errors } = (await context.graphql.raw({ query })) as ExecutionResult; expect(errors).toBe(undefined); @@ -186,8 +165,9 @@ describe(`List access`, () => { }); test(`single denied: ${JSON.stringify(access)}`, async () => { - const item = await context.sudo().query[nameFn[mode](access)].createOne({ data: {} }); - const singleQueryName = context.gqlNames(nameFn[mode](access)).itemQueryName; + const listKey = nameFn[mode](access); + const item = await context.sudo().query[listKey].createOne({ data: {} }); + const singleQueryName = context.gqlNames(listKey).itemQueryName; const query = `query { ${singleQueryName}(where: { id: "${item.id}" }) { id } }`; const { data, errors } = (await context.graphql.raw({ query })) as ExecutionResult; expect(errors).toBe(undefined); @@ -196,7 +176,7 @@ describe(`List access`, () => { } else { expect(data![singleQueryName]).toEqual({ id: item.id }); } - await context.sudo().query[nameFn[mode](access)].deleteOne({ where: { id: item.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item.id } }); }); }); }); @@ -205,7 +185,8 @@ describe(`List access`, () => { describe(mode, () => { listAccessVariations.forEach(access => { test(`'all' denied: ${JSON.stringify(access)}`, async () => { - const allQueryName = context.gqlNames(nameFn[mode](access)).listQueryName; + const listKey = nameFn[mode](access); + const allQueryName = context.gqlNames(listKey).listQueryName; const query = `query { ${allQueryName} { id } }`; const { data, errors } = (await context.graphql.raw({ query })) as ExecutionResult; expect(errors).toBe(undefined); @@ -217,9 +198,8 @@ describe(`List access`, () => { }); test(`count denied: ${JSON.stringify(access)}`, async () => { - const countName = `${ - nameFn[mode](access).slice(0, 1).toLowerCase() + nameFn[mode](access).slice(1) - }sCount`; + const listKey = nameFn[mode](access); + const countName = `${listKey.slice(0, 1).toLowerCase() + listKey.slice(1)}sCount`; const query = `query { ${countName} }`; const { data, errors } = (await context.graphql.raw({ query })) as ExecutionResult; expect(errors).toBe(undefined); @@ -231,13 +211,12 @@ describe(`List access`, () => { }); test(`single denied: ${JSON.stringify(access)}`, async () => { - const item1 = await context - .sudo() - .query[nameFn[mode](access)].createOne({ data: { name: 'foo' } }); + const listKey = nameFn[mode](access); + const item1 = await context.sudo().query[listKey].createOne({ data: { name: 'foo' } }); const item2 = await context .sudo() - .query[nameFn[mode](access)].createOne({ data: { name: 'Hello' } }); - const singleQueryName = context.gqlNames(nameFn[mode](access)).itemQueryName; + .query[listKey].createOne({ data: { name: 'Hello' } }); + const singleQueryName = context.gqlNames(listKey).itemQueryName; // Run a query where we expect a filter miss const query = `query { ${singleQueryName}(where: { id: "${item1.id}" }) { id } }`; @@ -249,7 +228,7 @@ describe(`List access`, () => { // Filtered out expect(data![singleQueryName]).toBe(null); } - await context.sudo().query[nameFn[mode](access)].deleteOne({ where: { id: item1.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item1.id } }); // Run a query where we expect a filter match const _query = `query { ${singleQueryName}(where: { id: "${item2.id}" }) { id } }`; @@ -261,7 +240,7 @@ describe(`List access`, () => { // Filtered in expect(result.data![singleQueryName]).toEqual({ id: item2.id }); } - await context.sudo().query[nameFn[mode](access)].deleteOne({ where: { id: item2.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item2.id } }); }); }); }); @@ -273,24 +252,16 @@ describe(`List access`, () => { describe(mode, () => { listAccessVariations.forEach(access => { test(`denies missing: ${JSON.stringify(access)}`, async () => { - const updateMutationName = `update${nameFn[mode](access)}`; + const listKey = nameFn[mode](access); + const updateMutationName = `update${listKey}`; const query = `mutation { ${updateMutationName}(where: { id: "${FAKE_ID}" }, data: { name: "bar" }) { id } }`; const { data, errors } = await context.graphql.raw({ query }); - if (access.update || mode === 'item') { - expectNoAccess( - data, - errors, - updateMutationName, - `You cannot perform the 'update' operation on the item '{"id":"${FAKE_ID}"}'. It may not exist.` - ); - } else { - expectNoAccess( - data, - errors, - updateMutationName, - `You cannot perform the 'update' operation on the list '${nameFn[mode](access)}'.` - ); - } + expectNoAccess( + data, + errors, + updateMutationName, + `You cannot update that ${listKey} - it may not exist` + ); }); }); }); @@ -299,59 +270,43 @@ describe(`List access`, () => { describe(mode, () => { listAccessVariations.forEach(access => { test(`denies: - single - ${JSON.stringify(access)}`, async () => { - const item = await context.sudo().query[nameFn[mode](access)].createOne({ data: {} }); - const updateMutationName = `update${nameFn[mode](access)}`; + const listKey = nameFn[mode](access); + const item = await context.sudo().query[listKey].createOne({ data: {} }); + const updateMutationName = `update${listKey}`; const query = `mutation { ${updateMutationName}(where: { id: "${item.id}" }, data: { name: "bar" }) { id name } }`; const { data, errors } = (await context.graphql.raw({ query })) as ExecutionResult; if (!access.update) { - if (mode === 'filterBool' || mode === 'operation') { - expectNoAccess( - data, - errors, - updateMutationName, - `You cannot perform the 'update' operation on the list '${nameFn[mode](access)}'.` - ); - } else { - expectNoAccess( - data, - errors, - updateMutationName, - `You cannot perform the 'update' operation on the item '{"id":"${item.id}"}'. It may not exist.` - ); - } + expectNoAccess( + data, + errors, + updateMutationName, + `You cannot update that ${listKey} - it may not exist` + ); } else { expect(errors).toBe(undefined); expect(data![updateMutationName]).toEqual({ id: item.id, name: 'bar' }); } - await context.sudo().query[nameFn[mode](access)].deleteOne({ where: { id: item.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item.id } }); }); test(`denies: - many - ${JSON.stringify(access)}`, async () => { - const item = await context.sudo().query[nameFn[mode](access)].createOne({ data: {} }); - const updateMutationName = `update${nameFn[mode](access)}s`; + const listKey = nameFn[mode](access); + const item = await context.sudo().query[listKey].createOne({ data: {} }); + const updateMutationName = `update${listKey}s`; const query = `mutation { ${updateMutationName}(data: [{ where: { id: "${item.id}" }, data: { name: "bar" } }]) { id name } }`; const { data, errors } = (await context.graphql.raw({ query })) as ExecutionResult; if (!access.update) { - if (mode === 'filterBool' || mode === 'operation') { - expectNoAccessMany( - data, - errors, - updateMutationName, - `You cannot perform the 'update' operation on the list '${nameFn[mode](access)}'.` - ); - } else { - expectNoAccessMany( - data, - errors, - updateMutationName, - `You cannot perform the 'update' operation on the item '{"id":"${item.id}"}'. It may not exist.` - ); - } + expectNoAccessMany( + data, + errors, + updateMutationName, + `You cannot update that ${listKey} - it may not exist` + ); } else { expect(errors).toBe(undefined); expect(data![updateMutationName]).toEqual([{ id: item.id, name: 'bar' }]); } - await context.sudo().query[nameFn[mode](access)].deleteOne({ where: { id: item.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item.id } }); }); }); }); @@ -360,34 +315,23 @@ describe(`List access`, () => { describe(mode, () => { listAccessVariations.forEach(access => { test(`denies: - single - ${JSON.stringify(access)}`, async () => { - const item1 = await context - .sudo() - .query[nameFn[mode](access)].createOne({ data: { name: 'foo' } }); + const listKey = nameFn[mode](access); + const item1 = await context.sudo().query[listKey].createOne({ data: { name: 'foo' } }); const item2 = await context .sudo() - .query[nameFn[mode](access)].createOne({ data: { name: 'Hello' } }); + .query[listKey].createOne({ data: { name: 'Hello' } }); - const updateMutationName = `update${nameFn[mode](access)}`; + const updateMutationName = `update${listKey}`; const query = `mutation { ${updateMutationName}(where: { id: "${item1.id}" }, data: { name: "bar" }) { id } }`; const { data, errors } = await context.graphql.raw({ query }); - if (!access.update) { - expectNoAccess( - data, - errors, - updateMutationName, - `You cannot perform the 'update' operation on the list '${nameFn[mode](access)}'.` - ); - } else { - // Filtered out - expectNoAccess( - data, - errors, - updateMutationName, - `You cannot perform the 'update' operation on the item '{"id":"${item1.id}"}'. It may not exist.` - ); - } - await context.sudo().query[nameFn[mode](access)].deleteOne({ where: { id: item1.id } }); + expectNoAccess( + data, + errors, + updateMutationName, + `You cannot update that ${listKey} - it may not exist` + ); + await context.sudo().query[listKey].deleteOne({ where: { id: item1.id } }); const _query = `mutation { ${updateMutationName}(where: { id: "${item2.id}" }, data: { name: "bar" }) { id } }`; const result = (await context.graphql.raw({ query: _query })) as ExecutionResult; @@ -396,43 +340,33 @@ describe(`List access`, () => { result.data, result.errors, updateMutationName, - `You cannot perform the 'update' operation on the list '${nameFn[mode](access)}'.` + `You cannot update that ${listKey} - it may not exist` ); } else { // Filtered in expect(result.errors).toBe(undefined); expect(result.data![updateMutationName]).not.toEqual(null); } - await context.sudo().query[nameFn[mode](access)].deleteOne({ where: { id: item2.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item2.id } }); }); + test(`denies: - many - ${JSON.stringify(access)}`, async () => { - const item1 = await context - .sudo() - .query[nameFn[mode](access)].createOne({ data: { name: 'foo' } }); + const listKey = nameFn[mode](access); + const item1 = await context.sudo().query[listKey].createOne({ data: { name: 'foo' } }); const item2 = await context .sudo() - .query[nameFn[mode](access)].createOne({ data: { name: 'Hello' } }); + .query[listKey].createOne({ data: { name: 'Hello' } }); - const updateMutationName = `update${nameFn[mode](access)}s`; + const updateMutationName = `update${listKey}s`; const query = `mutation { ${updateMutationName}(data: [{ where: { id: "${item1.id}" }, data: { name: "bar" } }]) { id } }`; const { data, errors } = await context.graphql.raw({ query }); - if (!access.update) { - expectNoAccessMany( - data, - errors, - updateMutationName, - `You cannot perform the 'update' operation on the list '${nameFn[mode](access)}'.` - ); - } else { - // Filtered out - expectNoAccessMany( - data, - errors, - updateMutationName, - `You cannot perform the 'update' operation on the item '{"id":"${item1.id}"}'. It may not exist.` - ); - } - await context.sudo().query[nameFn[mode](access)].deleteOne({ where: { id: item1.id } }); + expectNoAccessMany( + data, + errors, + updateMutationName, + `You cannot update that ${listKey} - it may not exist` + ); + await context.sudo().query[listKey].deleteOne({ where: { id: item1.id } }); const _query = `mutation { ${updateMutationName}(data: [{ where: { id: "${item2.id}" }, data: { name: "bar" } }]) { id } }`; const result = (await context.graphql.raw({ query: _query })) as ExecutionResult; @@ -441,14 +375,14 @@ describe(`List access`, () => { result.data, result.errors, updateMutationName, - `You cannot perform the 'update' operation on the list '${nameFn[mode](access)}'.` + `You cannot update that ${listKey} - it may not exist` ); } else { // Filtered in expect(result.errors).toBe(undefined); expect(result.data![updateMutationName][0]).not.toEqual(null); } - await context.sudo().query[nameFn[mode](access)].deleteOne({ where: { id: item2.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item2.id } }); }); }); }); @@ -460,53 +394,43 @@ describe(`List access`, () => { describe(mode, () => { listAccessVariations.forEach(access => { test(`single denies missing: ${JSON.stringify(access)}`, async () => { - const deleteMutationName = `delete${nameFn[mode](access)}`; + const listKey = nameFn[mode](access); + const deleteMutationName = `delete${listKey}`; const query = `mutation { ${deleteMutationName}(where: { id: "${FAKE_ID}" }) { id } }`; const { data, errors } = await context.graphql.raw({ query }); - if (access.delete || mode === 'item') { - expectNoAccess( - data, - errors, - deleteMutationName, - `You cannot perform the 'delete' operation on the item '{"id":"${FAKE_ID}"}'. It may not exist.` - ); - } else { - expectNoAccess( - data, - errors, - deleteMutationName, - `You cannot perform the 'delete' operation on the list '${nameFn[mode](access)}'.` - ); - } + expectNoAccess( + data, + errors, + deleteMutationName, + `You cannot delete that ${listKey} - it may not exist` + ); }); + test(`multi denies missing: ${JSON.stringify(access)}`, async () => { - const multiDeleteMutationName = `delete${nameFn[mode](access)}s`; + const listKey = nameFn[mode](access); + const multiDeleteMutationName = `delete${listKey}s`; const query = `mutation { ${multiDeleteMutationName}(where: [{ id: "${FAKE_ID}" }, { id: "${FAKE_ID_2}" }]) { id } }`; const { data, errors } = await context.graphql.raw({ query }); if (access.delete || mode === 'item') { expectAccessDenied(errors, [ { path: [multiDeleteMutationName, 0], - msg: `You cannot perform the 'delete' operation on the item '{"id":"${FAKE_ID}"}'. It may not exist.`, + msg: `You cannot delete that ${listKey} - it may not exist`, }, { path: [multiDeleteMutationName, 1], - msg: `You cannot perform the 'delete' operation on the item '{"id":"${FAKE_ID_2}"}'. It may not exist.`, + msg: `You cannot delete that ${listKey} - it may not exist`, }, ]); } else { expectAccessDenied(errors, [ { path: [multiDeleteMutationName, 0], - msg: `You cannot perform the 'delete' operation on the list '${nameFn[mode]( - access - )}'.`, + msg: `You cannot delete that ${listKey} - it may not exist`, }, { path: [multiDeleteMutationName, 1], - msg: `You cannot perform the 'delete' operation on the list '${nameFn[mode]( - access - )}'.`, + msg: `You cannot delete that ${listKey} - it may not exist`, }, ]); } @@ -520,71 +444,51 @@ describe(`List access`, () => { describe(mode, () => { listAccessVariations.forEach(access => { test(`single denied: ${JSON.stringify(access)}`, async () => { - const item = await context.sudo().query[nameFn[mode](access)].createOne({ data: {} }); + const listKey = nameFn[mode](access); + const item = await context.sudo().query[listKey].createOne({ data: {} }); - const deleteMutationName = `delete${nameFn[mode](access)}`; + const deleteMutationName = `delete${listKey}`; const query = `mutation { ${deleteMutationName}(where: {id: "${item.id}" }) { id } }`; const { data, errors } = (await context.graphql.raw({ query })) as ExecutionResult; if (!access.delete) { - if (mode === 'filterBool' || mode === 'operation') { - expectNoAccess( - data, - errors, - deleteMutationName, - `You cannot perform the 'delete' operation on the list '${nameFn[mode](access)}'.` - ); - } else { - expectNoAccess( - data, - errors, - deleteMutationName, - `You cannot perform the 'delete' operation on the item '{"id":"${item.id}"}'. It may not exist.` - ); - } + expectNoAccess( + data, + errors, + deleteMutationName, + `You cannot delete that ${listKey} - it may not exist` + ); } else { expect(errors).toBe(undefined); expect(data![deleteMutationName]).not.toEqual(null); } if (!access.delete) { - await context - .sudo() - .query[nameFn[mode](access)].deleteOne({ where: { id: item.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item.id } }); } }); test(`multi denied: ${JSON.stringify(access)}`, async () => { - const item = await context.sudo().query[nameFn[mode](access)].createOne({ data: {} }); + const listKey = nameFn[mode](access); + const item = await context.sudo().query[listKey].createOne({ data: {} }); - const multiDeleteMutationName = `delete${nameFn[mode](access)}s`; + const multiDeleteMutationName = `delete${listKey}s`; const query = `mutation { ${multiDeleteMutationName}(where: [{ id: "${item.id}" }]) { id } }`; const { data, errors } = (await context.graphql.raw({ query })) as ExecutionResult; if (!access.delete) { - if (mode === 'filterBool' || mode === 'operation') { - expectNoAccessMany( - data, - errors, - multiDeleteMutationName, - `You cannot perform the 'delete' operation on the list '${nameFn[mode](access)}'.` - ); - } else { - expectNoAccessMany( - data, - errors, - multiDeleteMutationName, - `You cannot perform the 'delete' operation on the item '{"id":"${item.id}"}'. It may not exist.` - ); - } + expectNoAccessMany( + data, + errors, + multiDeleteMutationName, + `You cannot delete that ${listKey} - it may not exist` + ); } else { expect(errors).toBe(undefined); expect(data![multiDeleteMutationName]).not.toEqual(null); } if (!access.delete) { - await context - .sudo() - .query[nameFn[mode](access)].deleteOne({ where: { id: item.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item.id } }); } }); }); @@ -594,37 +498,24 @@ describe(`List access`, () => { describe(mode, () => { listAccessVariations.forEach(access => { test(`single denied: ${JSON.stringify(access)}`, async () => { - const item1 = await context - .sudo() - .query[nameFn[mode](access)].createOne({ data: { name: 'foo' } }); + const listKey = nameFn[mode](access); + const item1 = await context.sudo().query[listKey].createOne({ data: { name: 'foo' } }); const item2 = await context .sudo() - .query[nameFn[mode](access)].createOne({ data: { name: 'Hello' } }); + .query[listKey].createOne({ data: { name: 'Hello' } }); - const deleteMutationName = `delete${nameFn[mode](access)}`; + const deleteMutationName = `delete${listKey}`; const query = `mutation { ${deleteMutationName}(where: {id: "${item1.id}" }) { id } }`; const { data, errors } = await context.graphql.raw({ query }); - if (!access.delete) { - expectNoAccess( - data, - errors, - deleteMutationName, - `You cannot perform the 'delete' operation on the list '${nameFn[mode](access)}'.` - ); - } else { - // Filtered out - expectNoAccess( - data, - errors, - deleteMutationName, - `You cannot perform the 'delete' operation on the item '{"id":"${item1.id}"}'. It may not exist.` - ); - } + expectNoAccess( + data, + errors, + deleteMutationName, + `You cannot delete that ${listKey} - it may not exist` + ); if (!access.delete) { - await context - .sudo() - .query[nameFn[mode](access)].deleteOne({ where: { id: item1.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item1.id } }); } const _query = `mutation { ${deleteMutationName}(where: {id: "${item2.id}" }) { id } }`; @@ -634,7 +525,7 @@ describe(`List access`, () => { result.data, result.errors, deleteMutationName, - `You cannot perform the 'delete' operation on the list '${nameFn[mode](access)}'.` + `You cannot delete that ${listKey} - it may not exist` ); } else { // Filtered in @@ -643,43 +534,28 @@ describe(`List access`, () => { } if (!access.delete) { - await context - .sudo() - .query[nameFn[mode](access)].deleteOne({ where: { id: item2.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item2.id } }); } }); test(`multi denied: ${JSON.stringify(access)}`, async () => { - const item1 = await context - .sudo() - .query[nameFn[mode](access)].createOne({ data: { name: 'foo' } }); + const listKey = nameFn[mode](access); + const item1 = await context.sudo().query[listKey].createOne({ data: { name: 'foo' } }); const item2 = await context .sudo() - .query[nameFn[mode](access)].createOne({ data: { name: 'Hello' } }); + .query[listKey].createOne({ data: { name: 'Hello' } }); - const multiDeleteMutationName = `delete${nameFn[mode](access)}s`; + const multiDeleteMutationName = `delete${listKey}s`; const query = `mutation { ${multiDeleteMutationName}(where: [{ id: "${item1.id}" }]) { id } }`; const { data, errors } = await context.graphql.raw({ query }); + expectNoAccessMany( + data, + errors, + multiDeleteMutationName, + `You cannot delete that ${listKey} - it may not exist` + ); if (!access.delete) { - expectNoAccessMany( - data, - errors, - multiDeleteMutationName, - `You cannot perform the 'delete' operation on the list '${nameFn[mode](access)}'.` - ); - } else { - // Filtered out - expectNoAccessMany( - data, - errors, - multiDeleteMutationName, - `You cannot perform the 'delete' operation on the item '{"id":"${item1.id}"}'. It may not exist.` - ); - } - if (!access.delete) { - await context - .sudo() - .query[nameFn[mode](access)].deleteOne({ where: { id: item1.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item1.id } }); } const _query = `mutation { ${multiDeleteMutationName}(where: [{ id: "${item2.id}" }]) { id } }`; @@ -689,7 +565,7 @@ describe(`List access`, () => { result.data, result.errors, multiDeleteMutationName, - `You cannot perform the 'delete' operation on the list '${nameFn[mode](access)}'.` + `You cannot delete that ${listKey} - it may not exist` ); } else { // Filtered in @@ -697,9 +573,7 @@ describe(`List access`, () => { expect(result.data![multiDeleteMutationName]).not.toEqual(null); } if (!access.delete) { - await context - .sudo() - .query[nameFn[mode](access)].deleteOne({ where: { id: item2.id } }); + await context.sudo().query[listKey].deleteOne({ where: { id: item2.id } }); } }); }); diff --git a/tests/api-tests/access-control/mutations-field.test.ts b/tests/api-tests/access-control/mutations-field.test.ts index 944af59cbd1..38f89792558 100644 --- a/tests/api-tests/access-control/mutations-field.test.ts +++ b/tests/api-tests/access-control/mutations-field.test.ts @@ -86,7 +86,7 @@ describe('Access control', () => { expectAccessDenied(errors, [ { path: ['createUser'], - msg: `You cannot perform the 'create' operation on the item '{"other":"b","name":"bad"}'. You cannot create the fields ["name"].`, + msg: `You cannot create that User - you cannot create the fields ["name"]`, }, ]); @@ -146,7 +146,7 @@ describe('Access control', () => { expectAccessDenied(errors, [ { path: ['updateUser'], - msg: `You cannot perform the 'update' operation on the item '{"id":"${user.id}"}'. You cannot update the fields ["name"].`, + msg: `You cannot update that User - you cannot update the fields ["name"]`, }, ]); @@ -221,11 +221,11 @@ describe('Access control', () => { expectAccessDenied(errors, [ { path: ['createUsers', 1], - msg: `You cannot perform the 'create' operation on the item '{"other":"a","name":"bad"}'. You cannot create the fields [\"name\"].`, + msg: `You cannot create that User - you cannot create the fields ["name"]`, }, { path: ['createUsers', 3], - msg: `You cannot perform the 'create' operation on the item '{"other":"a","name":"bad"}'. You cannot create the fields [\"name\"].`, + msg: `You cannot create that User - you cannot create the fields ["name"]`, }, ]); @@ -283,11 +283,11 @@ describe('Access control', () => { expectAccessDenied(errors, [ { path: ['updateUsers', 1], - msg: `You cannot perform the 'update' operation on the item '{"id":"${users[1].id}"}'. You cannot update the fields [\"name\"].`, + msg: `You cannot update that User - you cannot update the fields ["name"]`, }, { path: ['updateUsers', 3], - msg: `You cannot perform the 'update' operation on the item '{"id":"${users[3].id}"}'. You cannot update the fields [\"name\"].`, + msg: `You cannot update that User - you cannot update the fields ["name"]`, }, ]); diff --git a/tests/api-tests/access-control/mutations-list-filter.test.ts b/tests/api-tests/access-control/mutations-list-filter.test.ts index 4cc7b4acadc..5b0e9506c28 100644 --- a/tests/api-tests/access-control/mutations-list-filter.test.ts +++ b/tests/api-tests/access-control/mutations-list-filter.test.ts @@ -45,7 +45,7 @@ describe('Access control - Filter', () => { expectAccessDenied(errors, [ { path: ['updateUser'], - msg: `You cannot perform the 'update' operation on the item '{"id":"${user.id}"}'. It may not exist.`, + msg: `You cannot update that User - it may not exist`, }, ]); @@ -74,7 +74,7 @@ describe('Access control - Filter', () => { expectAccessDenied(errors, [ { path: ['deleteUser'], - msg: `You cannot perform the 'delete' operation on the item '{"id":"${user2.id}"}'. It may not exist.`, + msg: `You cannot delete that User - it may not exist`, }, ]); @@ -133,11 +133,11 @@ describe('Access control - Filter', () => { expectAccessDenied(errors, [ { path: ['updateUsers', 1], - msg: `You cannot perform the 'update' operation on the item '{"id":"${users[1].id}"}'. It may not exist.`, + msg: `You cannot update that User - it may not exist`, }, { path: ['updateUsers', 3], - msg: `You cannot perform the 'update' operation on the item '{"id":"${users[3].id}"}'. It may not exist.`, + msg: `You cannot update that User - it may not exist`, }, ]); @@ -182,11 +182,11 @@ describe('Access control - Filter', () => { expectAccessDenied(errors, [ { path: ['deleteUsers', 1], - msg: `You cannot perform the 'delete' operation on the item '{"id":"${users[1].id}"}'. It may not exist.`, + msg: `You cannot delete that User - it may not exist`, }, { path: ['deleteUsers', 3], - msg: `You cannot perform the 'delete' operation on the item '{"id":"${users[3].id}"}'. It may not exist.`, + msg: `You cannot delete that User - it may not exist`, }, ]); diff --git a/tests/api-tests/access-control/mutations-list-item.test.ts b/tests/api-tests/access-control/mutations-list-item.test.ts index dada7993d38..d83749b0a48 100644 --- a/tests/api-tests/access-control/mutations-list-item.test.ts +++ b/tests/api-tests/access-control/mutations-list-item.test.ts @@ -8,10 +8,20 @@ import { apiTestConfig, expectAccessDenied, expectAccessReturnError } from '../u const runner = setupTestRunner({ config: apiTestConfig({ lists: { - // Item access control User: list({ access: { operation: allowAll, + filter: { + query: () => ({ + name: { not: { equals: 'hidden' } }, + }), + update: () => ({ + name: { not: { equals: 'hidden' } }, + }), + delete: () => ({ + name: { not: { equals: 'hidden' } }, + }), + }, item: { create: ({ inputData }) => { return inputData.name !== 'bad'; @@ -29,18 +39,17 @@ const runner = setupTestRunner({ }), BadAccess: list({ access: { + operation: allowAll, + // intentionally returns filters for testing purposes item: { - // @ts-ignore Intentionally return a filter for testing purposes create: () => { - return { name: { not: { equals: 'bad' } } }; + return { name: { not: { equals: 'bad' } } } as any; }, - // @ts-ignore Intentionally return a filter for testing purposes update: () => { - return { name: { not: { equals: 'bad' } } }; + return { name: { not: { equals: 'bad' } } } as any; }, - // @ts-ignore Intentionally return a filter for testing purposes delete: async () => { - return { name: { not: { startsWtih: 'no delete' } } }; + return { name: { not: { startsWtih: 'no delete' } } } as any; }, }, }, @@ -68,7 +77,7 @@ describe('Access control - Item', () => { expectAccessDenied(errors, [ { path: ['createUser'], - msg: `You cannot perform the 'create' operation on the item '{"name":"bad"}'.`, + msg: `You cannot create that User`, }, ]); @@ -120,7 +129,7 @@ describe('Access control - Item', () => { expectAccessDenied(errors, [ { path: ['updateUser'], - msg: `You cannot perform the 'update' operation on the item '{"id":"${user.id}"}'. It may not exist.`, + msg: `You cannot update that User - it may not exist`, }, ]); @@ -130,6 +139,31 @@ describe('Access control - Item', () => { }) ); + test( + 'updateOne - Missing item', + runner(async ({ context }) => { + const user = await context.query.User.createOne({ data: { name: 'hidden' } }); + const { data, errors } = await context.graphql.raw({ + query: `mutation ($id: ID! $data: UserUpdateInput!) { updateUser(where: { id: $id }, data: $data) { id } }`, + variables: { id: user.id, data: { name: 'something else' } }, + }); + + // Returns null and throws an error + expect(data).toEqual({ updateUser: null }); + expectAccessDenied(errors, [ + { + path: ['updateUser'], + msg: `You cannot update that User - it may not exist`, + }, + ]); + + // should be unchanged + const userAgain = await context.sudo().db.User.findOne({ where: { id: user.id } }); + expect(userAgain).not.toEqual(null); + expect(userAgain!.name).toEqual('hidden'); + }) + ); + test( 'updateOne - Bad function return value', runner(async ({ context }) => { @@ -175,7 +209,7 @@ describe('Access control - Item', () => { expectAccessDenied(errors, [ { path: ['deleteUser'], - msg: `You cannot perform the 'delete' operation on the item '{"id":"${user2.id}"}'. It may not exist.`, + msg: `You cannot delete that User - it may not exist`, }, ]); @@ -243,11 +277,11 @@ describe('Access control - Item', () => { expectAccessDenied(errors, [ { path: ['createUsers', 1], - msg: `You cannot perform the 'create' operation on the item '{"name":"bad"}'.`, + msg: `You cannot create that User`, }, { path: ['createUsers', 3], - msg: `You cannot perform the 'create' operation on the item '{"name":"bad"}'.`, + msg: `You cannot create that User`, }, ]); @@ -302,11 +336,11 @@ describe('Access control - Item', () => { expectAccessDenied(errors, [ { path: ['updateUsers', 1], - msg: `You cannot perform the 'update' operation on the item '{"id":"${users[1].id}"}'. It may not exist.`, + msg: `You cannot update that User - it may not exist`, }, { path: ['updateUsers', 3], - msg: `You cannot perform the 'update' operation on the item '{"id":"${users[3].id}"}'. It may not exist.`, + msg: `You cannot update that User - it may not exist`, }, ]); @@ -362,11 +396,11 @@ describe('Access control - Item', () => { expectAccessDenied(errors, [ { path: ['deleteUsers', 1], - msg: `You cannot perform the 'delete' operation on the item '{"id":"${users[1].id}"}'. It may not exist.`, + msg: `You cannot delete that User - it may not exist`, }, { path: ['deleteUsers', 3], - msg: `You cannot perform the 'delete' operation on the item '{"id":"${users[3].id}"}'. It may not exist.`, + msg: `You cannot delete that User - it may not exist`, }, ]); diff --git a/tests/api-tests/auth-header.test.ts b/tests/api-tests/auth-header.test.ts index 9bc93873ea0..8932a087dec 100644 --- a/tests/api-tests/auth-header.test.ts +++ b/tests/api-tests/auth-header.test.ts @@ -92,7 +92,7 @@ describe('Auth testing', () => { expectAccessDenied(result.errors, [ { path: ['updateUser'], - msg: "You cannot perform the 'update' operation on the list 'User'.", + msg: 'You cannot update that User - it may not exist', }, ]); }) diff --git a/tests/api-tests/queries/filters.test.ts b/tests/api-tests/queries/filters.test.ts index 1cda85c2275..2179c84aabf 100644 --- a/tests/api-tests/queries/filters.test.ts +++ b/tests/api-tests/queries/filters.test.ts @@ -28,10 +28,8 @@ const runner = setupTestRunner({ filterTrue: integer({ isFilterable: true }), filterFunctionFalse: integer({ isFilterable: () => false }), filterFunctionTrue: integer({ isFilterable: () => true }), - // @ts-ignore - filterFunctionOtherFalsey: integer({ isFilterable: () => null }), - // @ts-ignore - filterFunctionOtherTruthy: integer({ isFilterable: () => ({}) }), + filterFunctionOtherFalsey: integer({ isFilterable: () => null } as any), // as any for tests + filterFunctionOtherTruthy: integer({ isFilterable: () => ({}) } as any), // as any for tests }, }), SecondaryList: list({ @@ -53,9 +51,9 @@ const runner = setupTestRunner({ defaultIsFilterable: false, }), DefaultFilterTrue: list({ + access: allowAll, fields: { a: integer(), b: integer({ isFilterable: true }) }, - // @ts-ignore - defaultIsFilterable: true, + defaultIsFilterable: true as any, // not actually allowed }), DefaultFilterFunctionFalse: list({ access: allowAll, @@ -70,14 +68,12 @@ const runner = setupTestRunner({ DefaultFilterFunctionFalsey: list({ access: allowAll, fields: { a: integer(), b: integer({ isFilterable: true }) }, - // @ts-ignore - defaultIsFilterable: () => null, + defaultIsFilterable: (() => null) as any, // not actually allowed }), DefaultFilterFunctionTruthy: list({ access: allowAll, fields: { a: integer(), b: integer({ isFilterable: true }) }, - // @ts-ignore - defaultIsFilterable: () => ({}), + defaultIsFilterable: () => ({} as any), // not actually allowed }), }, }), diff --git a/tests/api-tests/relationships/nested-mutations/connect-many.test.ts b/tests/api-tests/relationships/nested-mutations/connect-many.test.ts index a91c763670c..1bfd12fa4dd 100644 --- a/tests/api-tests/relationships/nested-mutations/connect-many.test.ts +++ b/tests/api-tests/relationships/nested-mutations/connect-many.test.ts @@ -262,7 +262,7 @@ describe('non-matching filter', () => { }`, }); expect(data).toEqual({ createUser: null }); - const message = `Access denied: You cannot perform the 'connect' operation on the item '{\"id\":\"${FAKE_ID}\"}'. It may not exist.`; + const message = `Access denied: You cannot connect that Note - it may not exist`; expectSingleRelationshipError(errors, 'createUser', 'User.notes', message); }) ); @@ -293,7 +293,7 @@ describe('non-matching filter', () => { }); expect(data).toEqual({ updateUser: null }); - const message = `Access denied: You cannot perform the 'connect' operation on the item '{\"id\":\"${FAKE_ID}\"}'. It may not exist.`; + const message = `Access denied: You cannot connect that Note - it may not exist`; expectSingleRelationshipError(errors, 'updateUser', 'User.notes', message); }) ); @@ -350,7 +350,7 @@ describe('with access control', () => { }); expect(data).toEqual({ createUserToNotesNoRead: null }); - const message = `Access denied: You cannot perform the 'connect' operation on the item '{\"id\":\"${createNoteNoRead.id}\"}'. It may not exist.`; + const message = `Access denied: You cannot connect that NoteNoRead - it may not exist`; expectSingleRelationshipError( errors, 'createUserToNotesNoRead', @@ -391,7 +391,7 @@ describe('with access control', () => { }`, }); expect(data).toEqual({ updateUserToNotesNoRead: null }); - const message = `Access denied: You cannot perform the 'connect' operation on the item '{\"id\":\"${createNote.id}\"}'. It may not exist.`; + const message = `Access denied: You cannot connect that NoteNoRead - it may not exist`; expectSingleRelationshipError( errors, 'updateUserToNotesNoRead', diff --git a/tests/api-tests/relationships/nested-mutations/connect-singular.test.ts b/tests/api-tests/relationships/nested-mutations/connect-singular.test.ts index ee926bb5e98..cb1df4e09aa 100644 --- a/tests/api-tests/relationships/nested-mutations/connect-singular.test.ts +++ b/tests/api-tests/relationships/nested-mutations/connect-singular.test.ts @@ -167,7 +167,7 @@ describe('non-matching filter', () => { }); expect(data).toEqual({ createEvent: null }); - const message = `Access denied: You cannot perform the 'connect' operation on the item '{"id":"${FAKE_ID}"}'. It may not exist.`; + const message = `Access denied: You cannot connect that Group - it may not exist`; expectSingleRelationshipError(errors, 'createEvent', 'Event.group', message); }) ); @@ -197,7 +197,7 @@ describe('non-matching filter', () => { }`, }); expect(data).toEqual({ updateEvent: null }); - const message = `Access denied: You cannot perform the 'connect' operation on the item '{"id":"${FAKE_ID}"}'. It may not exist.`; + const message = `Access denied: You cannot connect that Group - it may not exist`; expectSingleRelationshipError(errors, 'updateEvent', 'Event.group', message); }) ); @@ -335,7 +335,7 @@ describe('with access control', () => { }`, }); expect(data).toEqual({ [`updateEventTo${group.name}`]: null }); - const message = `Access denied: You cannot perform the 'connect' operation on the item '{"id":"${groupModel.id}"}'. It may not exist.`; + const message = `Access denied: You cannot connect that ${group.name} - it may not exist`; expectSingleRelationshipError( errors, `updateEventTo${group.name}`, @@ -370,7 +370,7 @@ describe('with access control', () => { }); expect(data).toEqual({ [`createEventTo${group.name}`]: null }); - const message = `Access denied: You cannot perform the 'connect' operation on the item '{"id":"${id}"}'. It may not exist.`; + const message = `Access denied: You cannot connect that ${group.name} - it may not exist`; expectSingleRelationshipError( errors, `createEventTo${group.name}`, diff --git a/tests/api-tests/relationships/nested-mutations/create-and-connect-many.test.ts b/tests/api-tests/relationships/nested-mutations/create-and-connect-many.test.ts index fe141c9020b..050fdfd1e15 100644 --- a/tests/api-tests/relationships/nested-mutations/create-and-connect-many.test.ts +++ b/tests/api-tests/relationships/nested-mutations/create-and-connect-many.test.ts @@ -191,7 +191,7 @@ describe('with access control', () => { }); expect(data).toEqual({ createUserToNotesNoRead: null }); - const message = `Access denied: You cannot perform the 'connect' operation on the item '{\"id\":\"${createNoteNoRead.id}\"}'. It may not exist.`; + const message = `Access denied: You cannot connect that NoteNoRead - it may not exist`; expectSingleRelationshipError( errors, 'createUserToNotesNoRead', @@ -237,7 +237,7 @@ describe('with access control', () => { }); expect(data).toEqual({ updateUserToNotesNoRead: null }); - const message = `Access denied: You cannot perform the 'connect' operation on the item '{\"id\":\"${createNote.id}\"}'. It may not exist.`; + const message = `Access denied: You cannot connect that NoteNoRead - it may not exist`; expectSingleRelationshipError( errors, 'updateUserToNotesNoRead', diff --git a/tests/api-tests/relationships/nested-mutations/create-many.test.ts b/tests/api-tests/relationships/nested-mutations/create-many.test.ts index 860f50b1ea7..1d878f9ecb2 100644 --- a/tests/api-tests/relationships/nested-mutations/create-many.test.ts +++ b/tests/api-tests/relationships/nested-mutations/create-many.test.ts @@ -305,8 +305,7 @@ describe('with access control', () => { // Assert it throws an access denied error expect(data).toEqual({ createUserToNotesNoCreate: null }); - const message = - "Access denied: You cannot perform the 'create' operation on the list 'NoteNoCreate'."; + const message = 'Access denied: You cannot create that NoteNoCreate'; expectSingleRelationshipError( errors, 'createUserToNotesNoCreate', @@ -354,8 +353,7 @@ describe('with access control', () => { // Assert it throws an access denied error expect(data).toEqual({ updateUserToNotesNoCreate: null }); - const message = - "Access denied: You cannot perform the 'create' operation on the list 'NoteNoCreate'."; + const message = 'Access denied: You cannot create that NoteNoCreate'; expectSingleRelationshipError( errors, 'updateUserToNotesNoCreate', diff --git a/tests/api-tests/relationships/nested-mutations/create-singular.test.ts b/tests/api-tests/relationships/nested-mutations/create-singular.test.ts index 935c000ba4c..e4e95d3a6c3 100644 --- a/tests/api-tests/relationships/nested-mutations/create-singular.test.ts +++ b/tests/api-tests/relationships/nested-mutations/create-singular.test.ts @@ -275,7 +275,7 @@ describe('with access control', () => { // Assert it throws an access denied error expect(data).toEqual({ [`createEventTo${group.name}`]: null }); - const message = `Access denied: You cannot perform the 'create' operation on the list 'GroupNoCreate'.`; + const message = `Access denied: You cannot create that GroupNoCreate`; expectSingleRelationshipError( errors, `createEventTo${group.name}`, @@ -336,7 +336,7 @@ describe('with access control', () => { } else { const { data, errors } = await context.graphql.raw({ query }); expect(data).toEqual({ [`updateEventTo${group.name}`]: null }); - const message = `Access denied: You cannot perform the 'create' operation on the list 'GroupNoCreate'.`; + const message = `Access denied: You cannot create that GroupNoCreate`; expectSingleRelationshipError( errors, `updateEventTo${group.name}`, diff --git a/tests/api-tests/relationships/nested-mutations/disconnect-many.test.ts b/tests/api-tests/relationships/nested-mutations/disconnect-many.test.ts index f73dc6ebb9e..0908fd11bfe 100644 --- a/tests/api-tests/relationships/nested-mutations/disconnect-many.test.ts +++ b/tests/api-tests/relationships/nested-mutations/disconnect-many.test.ts @@ -140,8 +140,7 @@ describe('non-matching filter', () => { variables: { id: createUser.id }, }); expect(data).toEqual({ updateUser: null }); - const message = - 'Access denied: You cannot perform the \'disconnect\' operation on the item \'{"id":"c5b84f38256d3c2df59a0d9bf"}\'. It may not exist.'; + const message = 'Access denied: You cannot disconnect that Note - it may not exist'; expectSingleRelationshipError(errors, 'updateUser', 'User.notes', message); }) ); @@ -181,7 +180,7 @@ describe('with access control', () => { variables: { id: createUser.id, idToDisconnect: createNote.id }, }); expect(data).toEqual({ updateUserToNotesNoRead: null }); - const message = `Access denied: You cannot perform the 'disconnect' operation on the item '{\"id\":\"${createNote.id}\"}'. It may not exist.`; + const message = `Access denied: You cannot disconnect that NoteNoRead - it may not exist`; expectSingleRelationshipError( errors, 'updateUserToNotesNoRead',