diff --git a/packages/graphql/src/schema/buildObjectType.ts b/packages/graphql/src/schema/buildObjectType.ts index 37cc866e44..76eacfc5ea 100644 --- a/packages/graphql/src/schema/buildObjectType.ts +++ b/packages/graphql/src/schema/buildObjectType.ts @@ -255,18 +255,17 @@ export function buildObjectType({ [field.on]: { equals: parent._id ?? parent.id }, }) - const results = await req.payload.find({ + return await req.payload.find({ collection, depth: 0, fallbackLocale: req.fallbackLocale, limit, locale: req.locale, + overrideAccess: false, req, sort, where: fullWhere, }) - - return results }, } diff --git a/packages/ui/src/utilities/buildTableState.ts b/packages/ui/src/utilities/buildTableState.ts index 1c041cc822..d908fc6b00 100644 --- a/packages/ui/src/utilities/buildTableState.ts +++ b/packages/ui/src/utilities/buildTableState.ts @@ -208,6 +208,7 @@ export const buildTableState = async ( collection: collectionSlug, depth: 0, limit: query?.limit ? parseInt(query.limit, 10) : undefined, + overrideAccess: false, page: query?.page ? parseInt(query.page, 10) : undefined, sort: query?.sort, where: query?.where, diff --git a/test/joins/config.ts b/test/joins/config.ts index e55ec49f0f..e37474b275 100644 --- a/test/joins/config.ts +++ b/test/joins/config.ts @@ -11,6 +11,8 @@ import { Uploads } from './collections/Uploads.js' import { Versions } from './collections/Versions.js' import { seed } from './seed.js' import { + categoriesJoinRestrictedSlug, + collectionRestrictedSlug, localizedCategoriesSlug, localizedPostsSlug, postsSlug, @@ -95,6 +97,25 @@ export default buildConfigWithDefaults({ }, ], }, + { + slug: categoriesJoinRestrictedSlug, + admin: { + useAsTitle: 'name', + }, + fields: [ + { + name: 'name', + type: 'text', + }, + { + // join collection with access.read: () => false which should not populate + name: 'collectionRestrictedJoin', + type: 'join', + collection: collectionRestrictedSlug, + on: 'category', + }, + ], + }, { slug: restrictedPostsSlug, admin: { @@ -120,6 +141,31 @@ export default buildConfigWithDefaults({ }, ], }, + { + slug: collectionRestrictedSlug, + admin: { + useAsTitle: 'title', + }, + access: { + read: () => ({ canRead: { equals: true } }), + }, + fields: [ + { + name: 'title', + type: 'text', + }, + { + name: 'canRead', + type: 'checkbox', + defaultValue: false, + }, + { + name: 'category', + type: 'relationship', + relationTo: restrictedCategoriesSlug, + }, + ], + }, ], localization: { locales: ['en', 'es'], diff --git a/test/joins/e2e.spec.ts b/test/joins/e2e.spec.ts index 2ae3dd351e..cb876df924 100644 --- a/test/joins/e2e.spec.ts +++ b/test/joins/e2e.spec.ts @@ -18,7 +18,7 @@ import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' import { navigateToDoc } from '../helpers/e2e/navigateToDoc.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { TEST_TIMEOUT_LONG } from '../playwright.config.js' -import { categoriesSlug, postsSlug, uploadsSlug } from './shared.js' +import { categoriesJoinRestrictedSlug, categoriesSlug, postsSlug, uploadsSlug } from './shared.js' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) @@ -30,16 +30,16 @@ test.describe('Admin Panel', () => { let page: Page let categoriesURL: AdminUrlUtil let uploadsURL: AdminUrlUtil - let postsURL: AdminUrlUtil + let categoriesJoinRestrictedURL: AdminUrlUtil test.beforeAll(async ({ browser }, testInfo) => { testInfo.setTimeout(TEST_TIMEOUT_LONG) ;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname, })) - postsURL = new AdminUrlUtil(serverURL, postsSlug) categoriesURL = new AdminUrlUtil(serverURL, categoriesSlug) uploadsURL = new AdminUrlUtil(serverURL, uploadsSlug) + categoriesJoinRestrictedURL = new AdminUrlUtil(serverURL, categoriesJoinRestrictedSlug) const context = await browser.newContext() page = await context.newPage() @@ -310,4 +310,14 @@ test.describe('Admin Panel', () => { }), ).toBeVisible() }) + + test('should render initial rows within relationship table respecting access control', async () => { + await navigateToDoc(page, categoriesJoinRestrictedURL) + const joinField = page.locator('#field-collectionRestrictedJoin.field-type.join') + await expect(joinField).toBeVisible() + await expect(joinField.locator('.relationship-table table')).toBeVisible() + const rows = joinField.locator('.relationship-table tbody tr') + await expect(rows).toHaveCount(1) + await expect(joinField.locator('.cell-canRead')).not.toContainText('false') + }) }) diff --git a/test/joins/int.spec.ts b/test/joins/int.spec.ts index 33ea1cf737..606f40dacf 100644 --- a/test/joins/int.spec.ts +++ b/test/joins/int.spec.ts @@ -11,6 +11,7 @@ import { devUser } from '../credentials.js' import { idToString } from '../helpers/idToString.js' import { initPayloadInt } from '../helpers/initPayloadInt.js' import { + categoriesJoinRestrictedSlug, categoriesSlug, postsSlug, restrictedCategoriesSlug, @@ -554,6 +555,22 @@ describe('Joins Field', () => { expect(unlimited.docs[0].relatedPosts.hasNextPage).toStrictEqual(false) }) + it('should respect access control for join collections', async () => { + const { docs } = await payload.find({ + collection: categoriesJoinRestrictedSlug, + where: { + name: { equals: 'categoryJoinRestricted' }, + }, + overrideAccess: false, + user, + }) + const [categoryWithRestrictedPosts] = docs + expect(categoryWithRestrictedPosts.collectionRestrictedJoin.docs).toHaveLength(1) + expect(categoryWithRestrictedPosts.collectionRestrictedJoin.docs[0].title).toStrictEqual( + 'should allow read', + ) + }) + it('should respect access control for join request `where` queries', async () => { await expect(async () => { await payload.findByID({ @@ -579,7 +596,7 @@ describe('Joins Field', () => { name: 'restricted category', }, }) - const post = await createPost({ + await createPost({ collection: restrictedPostsSlug, data: { title: 'restricted post', @@ -776,6 +793,31 @@ describe('Joins Field', () => { .then((res) => res.json()) expect(response.data.Category.relatedPosts.docs[0].title).toStrictEqual('test 3') }) + + it('should respect access control for join collections', async () => { + const query = `query { + CategoriesJoinRestricteds { + docs { + name + collectionRestrictedJoin { + docs { + title + canRead + } + } + } + } + }` + + const response = await restClient + .GRAPHQL_POST({ body: JSON.stringify({ query }) }) + .then((res) => res.json()) + const [categoryWithRestrictedPosts] = response.data.CategoriesJoinRestricteds.docs + expect(categoryWithRestrictedPosts.collectionRestrictedJoin.docs).toHaveLength(1) + expect(categoryWithRestrictedPosts.collectionRestrictedJoin.docs[0].title).toStrictEqual( + 'should allow read', + ) + }) }) it('should work id.in command delimited querying with joins', async () => { diff --git a/test/joins/payload-types.ts b/test/joins/payload-types.ts index 37f02f32ee..65550b5b86 100644 --- a/test/joins/payload-types.ts +++ b/test/joins/payload-types.ts @@ -21,7 +21,9 @@ export interface Config { 'localized-posts': LocalizedPost; 'localized-categories': LocalizedCategory; 'restricted-categories': RestrictedCategory; + 'categories-join-restricted': CategoriesJoinRestricted; 'restricted-posts': RestrictedPost; + 'collection-restricted': CollectionRestricted; users: User; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; @@ -51,6 +53,9 @@ export interface Config { 'restricted-categories': { restrictedPosts: 'posts'; }; + 'categories-join-restricted': { + collectionRestrictedJoin: 'collection-restricted'; + }; }; collectionsSelect: { posts: PostsSelect | PostsSelect; @@ -63,7 +68,9 @@ export interface Config { 'localized-posts': LocalizedPostsSelect | LocalizedPostsSelect; 'localized-categories': LocalizedCategoriesSelect | LocalizedCategoriesSelect; 'restricted-categories': RestrictedCategoriesSelect | RestrictedCategoriesSelect; + 'categories-join-restricted': CategoriesJoinRestrictedSelect | CategoriesJoinRestrictedSelect; 'restricted-posts': RestrictedPostsSelect | RestrictedPostsSelect; + 'collection-restricted': CollectionRestrictedSelect | CollectionRestrictedSelect; users: UsersSelect | UsersSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; @@ -278,6 +285,32 @@ export interface RestrictedCategory { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "categories-join-restricted". + */ +export interface CategoriesJoinRestricted { + id: string; + name?: string | null; + collectionRestrictedJoin?: { + docs?: (string | CollectionRestricted)[] | null; + hasNextPage?: boolean | null; + } | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "collection-restricted". + */ +export interface CollectionRestricted { + id: string; + title?: string | null; + canRead?: boolean | null; + category?: (string | null) | RestrictedCategory; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "restricted-posts". @@ -354,10 +387,18 @@ export interface PayloadLockedDocument { relationTo: 'restricted-categories'; value: string | RestrictedCategory; } | null) + | ({ + relationTo: 'categories-join-restricted'; + value: string | CategoriesJoinRestricted; + } | null) | ({ relationTo: 'restricted-posts'; value: string | RestrictedPost; } | null) + | ({ + relationTo: 'collection-restricted'; + value: string | CollectionRestricted; + } | null) | ({ relationTo: 'users'; value: string | User; @@ -536,6 +577,16 @@ export interface RestrictedCategoriesSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "categories-join-restricted_select". + */ +export interface CategoriesJoinRestrictedSelect { + name?: T; + collectionRestrictedJoin?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "restricted-posts_select". @@ -547,6 +598,17 @@ export interface RestrictedPostsSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "collection-restricted_select". + */ +export interface CollectionRestrictedSelect { + title?: T; + canRead?: T; + category?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "users_select". diff --git a/test/joins/seed.ts b/test/joins/seed.ts index 757cebf31f..72c84dc08d 100644 --- a/test/joins/seed.ts +++ b/test/joins/seed.ts @@ -7,7 +7,9 @@ import { fileURLToPath } from 'url' import { devUser } from '../credentials.js' import { seedDB } from '../helpers/seed.js' import { + categoriesJoinRestrictedSlug, categoriesSlug, + collectionRestrictedSlug, collectionSlugs, hiddenPostsSlug, postsSlug, @@ -91,6 +93,29 @@ export const seed = async (_payload) => { upload: uploadedImage.id, }, }) + + const restrictedCategory = await _payload.create({ + collection: categoriesJoinRestrictedSlug, + data: { + name: 'categoryJoinRestricted', + }, + }) + await _payload.create({ + collection: collectionRestrictedSlug, + data: { + title: 'should not allow read', + canRead: false, + category: restrictedCategory.id, + }, + }) + await _payload.create({ + collection: collectionRestrictedSlug, + data: { + title: 'should allow read', + canRead: true, + category: restrictedCategory.id, + }, + }) } export async function clearAndSeedEverything(_payload: Payload) { diff --git a/test/joins/shared.ts b/test/joins/shared.ts index 3be3a76814..bd936b5ce4 100644 --- a/test/joins/shared.ts +++ b/test/joins/shared.ts @@ -14,6 +14,10 @@ export const localizedCategoriesSlug = 'localized-categories' export const restrictedPostsSlug = 'restricted-posts' +export const categoriesJoinRestrictedSlug = 'categories-join-restricted' + +export const collectionRestrictedSlug = 'collection-restricted' + export const restrictedCategoriesSlug = 'restricted-categories' export const collectionSlugs = [