Skip to content

Commit 0516803

Browse files
test(plugin-multi-tenant): verify that tenant selector respects access control (#14916)
### What? Adds an E2E test to verify that `getTenantOptions` properly respects access control when determining which tenants appear in the tenant selector. This is a test for functionality added in #14620. ### Why? The previous PR #14620 that simplified `getTenantOptions` had no tests and got merged before @JarrodMFlesch confirmed whether it should be tested. ### How? 1. **Added the `tenantRole` field** to the Users collection's tenants array with options: `admin` (default) / `member` 2. **Updated Tenants access control** to check for `tenantRole: 'admin'` - users can only read tenants where they have an admin role 3. **Added test user in seed data** with mixed tenant roles: - Steel Cat (admin role) → should appear in selector - Anchor Bar (admin role) → should appear in selector - Blue Dog (member role) → should NOT appear in selector 4. **Added an E2E test** that logs in as this user and verifies the tenant selector only shows tenants with read access The test confirms that `getTenantOptions` respects the tenant collection's access control configuration and doesn't blindly show all tenants from the user's array. Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
1 parent 74aa825 commit 0516803

6 files changed

Lines changed: 79 additions & 9 deletions

File tree

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { Access, CollectionConfig, Where } from 'payload'
22

3-
import { getUserTenantIDs } from '@payloadcms/plugin-multi-tenant/utilities'
4-
53
import { tenantsSlug } from '../shared.js'
64

75
const tenantAccess: Access = ({ req }) => {
@@ -11,18 +9,21 @@ const tenantAccess: Access = ({ req }) => {
119
}
1210

1311
if (req.user) {
14-
const assignedTenants = getUserTenantIDs(req.user, {
15-
tenantsArrayFieldName: 'tenants',
16-
tenantsArrayTenantFieldName: 'tenant',
17-
})
12+
// Filter tenants where user has 'admin' tenantRole
13+
const adminTenants = (
14+
(req.user.tenants as Array<{ tenant: { id: string } | string; tenantRole?: string }>) || []
15+
)
16+
.filter((t) => t.tenantRole === 'admin')
17+
.map((t) => (typeof t.tenant === 'string' ? t.tenant : t.tenant?.id))
18+
.filter(Boolean)
1819

19-
// if the user has assigned tenants, add id constraint
20-
if (assignedTenants.length > 0) {
20+
// User can access tenants where they have 'admin' tenantRole OR public tenants
21+
if (adminTenants.length > 0) {
2122
return {
2223
or: [
2324
{
2425
id: {
25-
in: assignedTenants,
26+
in: adminTenants,
2627
},
2728
},
2829
{

test/plugin-multi-tenant/config.base.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,26 @@ export const baseConfig: Partial<Config> = {
7474
tenantField: {
7575
access: {},
7676
},
77+
tenantsArrayField: {
78+
rowFields: [
79+
{
80+
name: 'tenantRole',
81+
type: 'select',
82+
defaultValue: 'admin',
83+
options: [
84+
{
85+
label: 'Admin',
86+
value: 'admin',
87+
},
88+
{
89+
label: 'Member',
90+
value: 'member',
91+
},
92+
],
93+
saveToJWT: true,
94+
},
95+
],
96+
},
7797
collections: {
7898
[menuItemsSlug]: {
7999
useTenantAccess: false,

test/plugin-multi-tenant/credentials.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@ export const credentials = {
1919
email: 'huel@steel-cat.com',
2020
password: 'test',
2121
},
22+
memberUser: {
23+
email: 'member@test.com',
24+
password: 'test',
25+
},
2226
} as const

test/plugin-multi-tenant/e2e.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,25 @@ test.describe('Multi Tenant', () => {
903903
.toBeFalsy()
904904
})
905905

906+
test('should only show tenants that user has read access to whether they are assigned to tenant or not', async () => {
907+
// Login as user with Steel Cat (admin), Anchor Bar (admin), and Blue Dog (member)
908+
await loginClientSide({
909+
data: credentials.memberUser,
910+
page,
911+
serverURL,
912+
})
913+
914+
await page.goto(tenantsURL.list)
915+
916+
// Should see: Steel Cat (admin role), Anchor Bar (admin role)
917+
// Should NOT see: Blue Dog (member role - no read access)
918+
await expect
919+
.poll(async () => {
920+
return (await getTenantOptions({ page })).sort()
921+
})
922+
.toEqual(['Anchor Bar', 'Steel Cat'].sort())
923+
})
924+
906925
test('should populate tenant selector after standard form login with tree-restructuring provider', async () => {
907926
// This test verifies the fix for a bug where TenantSelectionProviderClient
908927
// loses state on remount. The ConditionalWrapperProvider (registered in the

test/plugin-multi-tenant/payload-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export interface User {
161161
tenants?:
162162
| {
163163
tenant: string | Tenant;
164+
tenantRole?: ('admin' | 'member') | null;
164165
id?: string | null;
165166
}[]
166167
| null;
@@ -413,6 +414,7 @@ export interface UsersSelect<T extends boolean = true> {
413414
| T
414415
| {
415416
tenant?: T;
417+
tenantRole?: T;
416418
id?: T;
417419
};
418420
updatedAt?: T;

test/plugin-multi-tenant/seed/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,30 @@ export const seed = async (payload: Payload) => {
228228
},
229229
})
230230

231+
// User with mixed tenant roles: admin for Steel Cat, member for Blue Dog
232+
await payload.create({
233+
collection: usersSlug,
234+
data: {
235+
...credentials.memberUser,
236+
roles: ['user'],
237+
tenants: [
238+
{
239+
tenant: steelCatTenant.id,
240+
tenantRole: 'admin', // Has admin role - should see Steel Cat
241+
},
242+
{
243+
tenant: anchorBarTenant.id,
244+
tenantRole: 'admin',
245+
},
246+
{
247+
tenant: blueDogTenant.id,
248+
tenantRole: 'member', // Only member role - should NOT see Blue Dog
249+
},
250+
],
251+
},
252+
})
253+
254+
// Create a user with no tenant associations
231255
await payload.create({
232256
collection: usersSlug,
233257
data: {

0 commit comments

Comments
 (0)