Skip to content

Commit 20b4de9

Browse files
fix(plugin-multi-tenant): constrain results to assigned tenants when present (#13365)
Extension of #13213 This PR correctly filters tenants, users and documents based on the users assigned tenants if any are set. If a user is assigned tenants then list results should only show documents with those tenants (when selector is not set). Previously you could construct access results that allows them to see them, but in the confines of the admin panel they should not see them. If you wanted a user to be able to see a "public" tenant while inside the admin panel they either need to be added to the tenant or have no tenants at all. Note that this is for filtering only, access control still controls what documents a user has _access_ to a document. The filters are and always have been a way to filter out results in the list view.
1 parent 43b4b22 commit 20b4de9

File tree

13 files changed

+496
-209
lines changed

13 files changed

+496
-209
lines changed

packages/plugin-multi-tenant/src/exports/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { filterDocumentsBySelectedTenant as getTenantListFilter } from '../list-filters/filterDocumentsBySelectedTenant.js'
1+
export { filterDocumentsByTenants as getTenantListFilter } from '../filters/filterDocumentsByTenants.js'
22
export { getGlobalViewRedirect } from '../utilities/getGlobalViewRedirect.js'
33
export { getTenantAccess } from '../utilities/getTenantAccess.js'
44
export { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { PayloadRequest, Where } from 'payload'
2+
3+
import { defaults } from '../defaults.js'
4+
import { getCollectionIDType } from '../utilities/getCollectionIDType.js'
5+
import { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
6+
import { getUserTenantIDs } from '../utilities/getUserTenantIDs.js'
7+
8+
type Args = {
9+
filterFieldName: string
10+
req: PayloadRequest
11+
tenantsArrayFieldName?: string
12+
tenantsArrayTenantFieldName?: string
13+
tenantsCollectionSlug: string
14+
}
15+
export const filterDocumentsByTenants = ({
16+
filterFieldName,
17+
req,
18+
tenantsArrayFieldName = defaults.tenantsArrayFieldName,
19+
tenantsArrayTenantFieldName = defaults.tenantsArrayTenantFieldName,
20+
tenantsCollectionSlug,
21+
}: Args): null | Where => {
22+
const idType = getCollectionIDType({
23+
collectionSlug: tenantsCollectionSlug,
24+
payload: req.payload,
25+
})
26+
27+
// scope results to selected tenant
28+
const selectedTenant = getTenantFromCookie(req.headers, idType)
29+
if (selectedTenant) {
30+
return {
31+
[filterFieldName]: {
32+
in: [selectedTenant],
33+
},
34+
}
35+
}
36+
37+
// scope to user assigned tenants
38+
const userAssignedTenants = getUserTenantIDs(req.user, {
39+
tenantsArrayFieldName,
40+
tenantsArrayTenantFieldName,
41+
})
42+
if (userAssignedTenants.length > 0) {
43+
return {
44+
[filterFieldName]: {
45+
in: userAssignedTenants,
46+
},
47+
}
48+
}
49+
50+
// no tenant selected and no user tenants, return null to allow access control to handle it
51+
return null
52+
}

packages/plugin-multi-tenant/src/index.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ import { defaults } from './defaults.js'
1010
import { getTenantOptionsEndpoint } from './endpoints/getTenantOptionsEndpoint.js'
1111
import { tenantField } from './fields/tenantField/index.js'
1212
import { tenantsArrayField } from './fields/tenantsArrayField/index.js'
13+
import { filterDocumentsByTenants } from './filters/filterDocumentsByTenants.js'
1314
import { addTenantCleanup } from './hooks/afterTenantDelete.js'
14-
import { filterDocumentsBySelectedTenant } from './list-filters/filterDocumentsBySelectedTenant.js'
15-
import { filterTenantsBySelectedTenant } from './list-filters/filterTenantsBySelectedTenant.js'
16-
import { filterUsersBySelectedTenant } from './list-filters/filterUsersBySelectedTenant.js'
1715
import { translations } from './translations/index.js'
1816
import { addCollectionAccess } from './utilities/addCollectionAccess.js'
1917
import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js'
@@ -148,7 +146,8 @@ export const multiTenantPlugin =
148146
adminUsersCollection.admin.baseListFilter = combineListFilters({
149147
baseListFilter: adminUsersCollection.admin?.baseListFilter,
150148
customFilter: (args) =>
151-
filterUsersBySelectedTenant({
149+
filterDocumentsByTenants({
150+
filterFieldName: `${tenantsArrayFieldName}.${tenantsArrayTenantFieldName}`,
152151
req: args.req,
153152
tenantsArrayFieldName,
154153
tenantsArrayTenantFieldName,
@@ -211,8 +210,11 @@ export const multiTenantPlugin =
211210
collection.admin.baseListFilter = combineListFilters({
212211
baseListFilter: collection.admin?.baseListFilter,
213212
customFilter: (args) =>
214-
filterTenantsBySelectedTenant({
213+
filterDocumentsByTenants({
214+
filterFieldName: 'id',
215215
req: args.req,
216+
tenantsArrayFieldName,
217+
tenantsArrayTenantFieldName,
216218
tenantsCollectionSlug,
217219
}),
218220
})
@@ -306,9 +308,11 @@ export const multiTenantPlugin =
306308
collection.admin.baseListFilter = combineListFilters({
307309
baseListFilter: collection.admin?.baseListFilter,
308310
customFilter: (args) =>
309-
filterDocumentsBySelectedTenant({
311+
filterDocumentsByTenants({
312+
filterFieldName: tenantFieldName,
310313
req: args.req,
311-
tenantFieldName,
314+
tenantsArrayFieldName,
315+
tenantsArrayTenantFieldName,
312316
tenantsCollectionSlug,
313317
}),
314318
})

packages/plugin-multi-tenant/src/list-filters/filterDocumentsBySelectedTenant.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

packages/plugin-multi-tenant/src/list-filters/filterTenantsBySelectedTenant.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

packages/plugin-multi-tenant/src/list-filters/filterUsersBySelectedTenant.ts

Lines changed: 0 additions & 36 deletions
This file was deleted.

packages/plugin-multi-tenant/src/utilities/getUserTenantIDs.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ export const getUserTenantIDs = <IDType extends number | string>(
1919
return []
2020
}
2121

22-
const {
23-
tenantsArrayFieldName = defaults.tenantsArrayFieldName,
24-
tenantsArrayTenantFieldName = defaults.tenantsArrayTenantFieldName,
25-
} = options || {}
22+
const tenantsArrayFieldName = options?.tenantsArrayFieldName || defaults.tenantsArrayFieldName
23+
const tenantsArrayTenantFieldName =
24+
options?.tenantsArrayTenantFieldName || defaults.tenantsArrayTenantFieldName
2625

2726
return (
2827
(Array.isArray(user[tenantsArrayFieldName]) ? user[tenantsArrayFieldName] : [])?.reduce<

test/plugin-multi-tenant/collections/MenuItems.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,85 @@
1-
import type { CollectionConfig } from 'payload'
1+
import type { Access, CollectionConfig, Where } from 'payload'
2+
3+
import { getUserTenantIDs } from '@payloadcms/plugin-multi-tenant/utilities'
24

35
import { menuItemsSlug } from '../shared.js'
46

7+
const collectionTenantReadAccess: Access = ({ req }) => {
8+
// admins can access all tenants
9+
if (req?.user?.roles?.includes('admin')) {
10+
return true
11+
}
12+
13+
if (req.user) {
14+
const assignedTenants = getUserTenantIDs(req.user, {
15+
tenantsArrayFieldName: 'tenants',
16+
tenantsArrayTenantFieldName: 'tenant',
17+
})
18+
19+
// if the user has assigned tenants, add id constraint
20+
if (assignedTenants.length > 0) {
21+
return {
22+
or: [
23+
{
24+
tenant: {
25+
in: assignedTenants,
26+
},
27+
},
28+
{
29+
'tenant.isPublic': {
30+
equals: true,
31+
},
32+
},
33+
],
34+
}
35+
}
36+
}
37+
38+
// if the user has no assigned tenants, return a filter that allows access to public tenants
39+
return {
40+
'tenant.isPublic': {
41+
equals: true,
42+
},
43+
} as Where
44+
}
45+
46+
const collectionTenantUpdateAccess: Access = ({ req }) => {
47+
// admins can update all tenants
48+
if (req?.user?.roles?.includes('admin')) {
49+
return true
50+
}
51+
52+
if (req.user) {
53+
const assignedTenants = getUserTenantIDs(req.user, {
54+
tenantsArrayFieldName: 'tenants',
55+
tenantsArrayTenantFieldName: 'tenant',
56+
})
57+
58+
// if the user has assigned tenants, add id constraint
59+
if (assignedTenants.length > 0) {
60+
return {
61+
tenant: {
62+
in: assignedTenants,
63+
},
64+
}
65+
}
66+
}
67+
68+
return false
69+
}
70+
571
export const MenuItems: CollectionConfig = {
672
slug: menuItemsSlug,
73+
access: {
74+
read: collectionTenantReadAccess,
75+
create: ({ req }) => {
76+
return Boolean(req?.user?.roles?.includes('admin'))
77+
},
78+
update: collectionTenantUpdateAccess,
79+
delete: ({ req }) => {
80+
return Boolean(req?.user?.roles?.includes('admin'))
81+
},
82+
},
783
admin: {
884
useAsTitle: 'name',
985
group: 'Tenant Collections',

test/plugin-multi-tenant/collections/Tenants.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,56 @@
1-
import type { CollectionConfig } from 'payload'
1+
import type { Access, CollectionConfig, Where } from 'payload'
2+
3+
import { getUserTenantIDs } from '@payloadcms/plugin-multi-tenant/utilities'
24

35
import { tenantsSlug } from '../shared.js'
46

7+
const tenantAccess: Access = ({ req }) => {
8+
// admins can access all tenants
9+
if (req?.user?.roles?.includes('admin')) {
10+
return true
11+
}
12+
13+
if (req.user) {
14+
const assignedTenants = getUserTenantIDs(req.user, {
15+
tenantsArrayFieldName: 'tenants',
16+
tenantsArrayTenantFieldName: 'tenant',
17+
})
18+
19+
// if the user has assigned tenants, add id constraint
20+
if (assignedTenants.length > 0) {
21+
return {
22+
or: [
23+
{
24+
id: {
25+
in: assignedTenants,
26+
},
27+
},
28+
{
29+
isPublic: {
30+
equals: true,
31+
},
32+
},
33+
],
34+
}
35+
}
36+
}
37+
38+
// if the user has no assigned tenants, return a filter that allows access to public tenants
39+
return {
40+
isPublic: {
41+
equals: true,
42+
},
43+
} as Where
44+
}
45+
546
export const Tenants: CollectionConfig = {
647
slug: tenantsSlug,
48+
access: {
49+
read: tenantAccess,
50+
create: tenantAccess,
51+
update: tenantAccess,
52+
delete: tenantAccess,
53+
},
754
labels: {
855
singular: 'Tenant',
956
plural: 'Tenants',
@@ -30,5 +77,10 @@ export const Tenants: CollectionConfig = {
3077
collection: 'users',
3178
on: 'tenants.tenant',
3279
},
80+
{
81+
name: 'isPublic',
82+
type: 'checkbox',
83+
label: 'Public Tenant',
84+
},
3385
],
3486
}

test/plugin-multi-tenant/config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@ export default buildConfigWithDefaults({
3232
plugins: [
3333
multiTenantPlugin<ConfigType>({
3434
userHasAccessToAllTenants: (user) => Boolean(user.roles?.includes('admin')),
35+
useTenantsCollectionAccess: false,
3536
tenantField: {
3637
access: {},
3738
},
3839
collections: {
39-
[menuItemsSlug]: {},
40+
[menuItemsSlug]: {
41+
useTenantAccess: false,
42+
},
4043
[menuSlug]: {
4144
isGlobal: true,
4245
},

0 commit comments

Comments
 (0)