fix: join collection read access (#9930)
Respect read access control through the join field collections for GraphQL and admin UI fixes #9922 and #9865
This commit is contained in:
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user