Repository: payloadcms/payload (language: python)

Issue:
GraphQL Join Field Access ignored
### Describe the Bug

When querying a join field in GraphQL no matter what the access property says it's shown in a GraphQL Request.

### Link to the code that reproduces this issue

https://github.com/1MisterT/payload_issue_graphql_access

### Reproduction Steps

- Clone Repo
- 
- cd test
- `pnpm install`
- `pnpm dev`
- 
- open admin Panel and add item to both collections and link them together
- test API Requests with and without AUTH
- open private window (definitely no auth) and open graphql-playground
- new Query: 

`query {
  Joins {
    docs {
      title
      noAccess
      join {
        docs {
          name
        }
      }
    }
  }
}
`

Expected output
![Image](https://github.com/user-attachments/assets/323fe39d-3ceb-4fdd-a4f9-8996ff32765a)


### Which area(s) are affected? (Select all that apply)

area: core

### Environment Info

```text
Binaries:
  Node: 20.15.0
  npm: 10.9.0
  Yarn: 1.22.22
  pnpm: 9.14.3
Relevant Packages:
  payload: 3.2.2
  next: 15.0.0
  @payloadcms/email-nodemailer: 3.2.2
  @payloadcms/graphql: 3.2.2
  @payloadcms/next/utilities: 3.2.2
  @payloadcms/payload-cloud: 3.2.2
  @payloadcms/richtext-lexical: 3.2.2
  @payloadcms/translations: 3.2.2
  @payloadcms/ui/shared: 3.2.2
  react: 19.0.0-rc-65a56d0e-20241020
  react-dom: 19.0.0-rc-65a56d0e-20241020
Operating System:
  Platform: linux
  Arch: x64
  Version: #9-Ubuntu SMP PREEMPT_DYNAMIC Mon Oct 14 13:19:59 UTC 2024
  Available memory (MB): 15167
  Available CPU cores: 12
```


Discussion / hints:

Fix patch:
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,


Test patch:
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<Config>({
       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<false> | PostsSelect<true>;
@@ -63,7 +68,9 @@ export interface Config {
     'localized-posts': LocalizedPostsSelect<false> | LocalizedPostsSelect<true>;
     'localized-categories': LocalizedCategoriesSelect<false> | LocalizedCategoriesSelect<true>;
     'restricted-categories': RestrictedCategoriesSelect<false> | RestrictedCategoriesSelect<true>;
+    'categories-join-restricted': CategoriesJoinRestrictedSelect<false> | CategoriesJoinRestrictedSelect<true>;
     'restricted-posts': RestrictedPostsSelect<false> | RestrictedPostsSelect<true>;
+    'collection-restricted': CollectionRestrictedSelect<false> | CollectionRestrictedSelect<true>;
     users: UsersSelect<false> | UsersSelect<true>;
     'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
     'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -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<T extends boolean = true> {
   updatedAt?: T;
   createdAt?: T;
 }
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "categories-join-restricted_select".
+ */
+export interface CategoriesJoinRestrictedSelect<T extends boolean = true> {
+  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<T extends boolean = true> {
   updatedAt?: T;
   createdAt?: T;
 }
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "collection-restricted_select".
+ */
+export interface CollectionRestrictedSelect<T extends boolean = true> {
+  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 = [