Skip to content

Commit

Permalink
fix: join collection read access (#9930)
Browse files Browse the repository at this point in the history
Respect read access control through the join field collections for GraphQL and admin UI

fixes #9922 and #9865
  • Loading branch information
DanRibbens authored Dec 12, 2024
1 parent d4d79c1 commit 5af71fb
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 7 deletions.
5 changes: 2 additions & 3 deletions packages/graphql/src/schema/buildObjectType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
}

Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/utilities/buildTableState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
46 changes: 46 additions & 0 deletions test/joins/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand All @@ -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'],
Expand Down
16 changes: 13 additions & 3 deletions test/joins/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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')
})
})
44 changes: 43 additions & 1 deletion test/joins/int.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand All @@ -579,7 +596,7 @@ describe('Joins Field', () => {
name: 'restricted category',
},
})
const post = await createPost({
await createPost({
collection: restrictedPostsSlug,
data: {
title: 'restricted post',
Expand Down Expand Up @@ -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 () => {
Expand Down
62 changes: 62 additions & 0 deletions test/joins/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,6 +53,9 @@ export interface Config {
'restricted-categories': {
restrictedPosts: 'posts';
};
'categories-join-restricted': {
collectionRestrictedJoin: 'collection-restricted';
};
};
collectionsSelect: {
posts: PostsSelect<false> | PostsSelect<true>;
Expand All @@ -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>;
Expand Down Expand Up @@ -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".
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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".
Expand All @@ -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".
Expand Down
25 changes: 25 additions & 0 deletions test/joins/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions test/joins/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down

0 comments on commit 5af71fb

Please sign in to comment.