### What?
The idea of this plugin is to only add constraints when a user is
present on a request. This change makes it so access control only
applies to admin panel users as they are the ones assigned to tenants.
This change allows you to more freely write access functions on tenant
enabled collections. Say you have 2 auth enabled collections, the plugin
would incorrectly assume since there is a user on the req that it needs
to apply tenant constraints. When really, you should be able to just add
in your own access check for `req.user.collection` and return true/false
if you want to prevent/allow other auth enabled collections for certain
operations.
```ts
import { Access } from 'payload'
const readByTenant: Access = ({ req }) => {
const { user } = req
if (!user || user.collection === 'auth2') return false
return true
}
```
When you have a function like this that returns `true` and the
collection is multi-tenant enabled - the plugin injects constraints
ensuring the user on the request is assigned to the tenant on the doc
being accessed.
Before this change, you would need to opt out of access control with
`useTenantAccess` and then wire up your own access function:
```ts
import type { Access } from 'payload'
import { getTenantAccess } from '@payloadcms/plugin-multi-tenant/utilities'
export const tenantAccess: Access = async ({ req: { user } }) => {
if (user) {
if (user.collection === 'auth2') {
return true
}
// Before, you would need to re-implement
// internal multi-tenant access constraints
if (user.roles?.includes('super-admin')) return true
return getTenantAccess({
fieldName: 'tenant',
user,
})
}
return false
}
```
After this change you would not need to opt out of `useTenantAccess` and
can just write:
```ts
import type { Access } from 'payload'
import { getTenantAccess } from '@payloadcms/plugin-multi-tenant/utilities'
export const tenantAccess: Access = async ({ req: { user } }) => {
return Boolean(user)
}
```
This is because internally the plugin will only add the tenant
constraint when the access function returns true/Where _AND_ the user
belongs to the admin panel users collection.
Multi Tenant Plugin
A plugin for Payload to easily manage multiple tenants from within your admin panel.
Installation
pnpm add @payloadcms/plugin-multi-tenant
Plugin Types
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
*
* @default true
*/
cleanupAfterTenantDelete?: boolean
/**
* Automatically
*/
collections: {
[key in CollectionSlug]?: {
/**
* Set to `true` if you want the collection to behave as a global
*
* @default false
*/
isGlobal?: boolean
/**
* Set to `false` if you want to manually apply the baseListFilter
*
* @default true
*/
useBaseListFilter?: boolean
/**
* Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied
*
* @default true
*/
useTenantAccess?: boolean
}
}
/**
* Enables debug mode
* - Makes the tenant field visible in the admin UI within applicable collections
*
* @default false
*/
debug?: boolean
/**
* Enables the multi-tenant plugin
*
* @default true
*/
enabled?: boolean
/**
* Field configuration for the field added to all tenant enabled collections
*/
tenantField?: {
access?: RelationshipField['access']
/**
* The name of the field added to all tenant enabled collections
*
* @default 'tenant'
*/
name?: string
}
/**
* Field configuration for the field added to the users collection
*
* If `includeDefaultField` is `false`, you must include the field on your users collection manually
* This is useful if you want to customize the field or place the field in a specific location
*/
tenantsArrayField?:
| {
/**
* Access configuration for the array field
*/
arrayFieldAccess?: ArrayField['access']
/**
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
*/
includeDefaultField?: true
/**
* Additional fields to include on the tenants array field
*/
rowFields?: Field[]
/**
* Access configuration for the tenant field
*/
tenantFieldAccess?: RelationshipField['access']
}
| {
arrayFieldAccess?: never
/**
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
*/
includeDefaultField?: false
rowFields?: never
tenantFieldAccess?: never
}
/**
* The slug for the tenant collection
*
* @default 'tenants'
*/
tenantsSlug?: string
/**
* Function that determines if a user has access to _all_ tenants
*
* Useful for super-admin type users
*/
userHasAccessToAllTenants?: (
user: ConfigTypes extends { user } ? ConfigTypes['user'] : User,
) => boolean
}
How to configure Collections as Globals for multi-tenant
When using multi-tenant, globals need to actually be configured as collections so the content can be specific per tenant.
To do that, you can mark a collection with isGlobal and it will behave like a global and users will not see the list view.
multiTenantPlugin({
collections: {
navigation: {
isGlobal: true,
},
},
})
Customizing access control
In some cases, the access control supplied out of the box may be too strict. For example, if you need some documents to be shared between tenants, you will need to opt out of the supplied access control functionality.
By default this plugin merges your access control result with a constraint based on tenants the user has access to within an AND condition. That would not work for the above scenario.
In the multi-tenant plugin config you can set useTenantAccess to false:
// File: payload.config.ts
import { buildConfig } from 'payload'
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
import { getTenantAccess } from '@payloadcms/plugin-multi-tenant/utilities'
import { Config as ConfigTypes } from './payload-types'
// Add the plugin to your payload config
export default buildConfig({
plugins: [
multiTenantPlugin({
collections: {
media: {
useTenantAccess: false,
},
},
}),
],
collections: [
{
slug: 'media',
fields: [
{
name: 'isShared',
type: 'checkbox',
defaultValue: false,
// you likely want to set access control on fields like this
// to prevent just any user from modifying it
},
],
access: {
read: ({ req, doc }) => {
if (!req.user) return false
const whereConstraint = {
or: [
{
isShared: {
equals: true,
},
},
],
}
const tenantAccessResult = getTenantAccess({ user: req.user })
if (tenantAccessResult) {
whereConstraint.or.push(tenantAccessResult)
}
return whereConstraint
},
},
},
],
})
Placing the tenants array field
In your users collection you may want to place the field in a tab or in the sidebar, or customize some of the properties on it.
You can use the tenantsArrayField.includeDefaultField: false setting in the plugin config. You will then need to manually add a tenants array field in your users collection.
This field cannot be nested inside a named field, ie a group, named-tab or array. It can be nested inside a row, unnamed-tab, collapsible.
To make it easier, this plugin exports the field for you to import and merge in your own properties.
import type { CollectionConfig } from 'payload'
import { tenantsArrayField } from '@payloadcms/plugin-multi-tenant/fields'
const customTenantsArrayField = tenantsArrayField({
arrayFieldAccess: {}, // access control for the array field
tenantFieldAccess: {}, // access control for the tenants field on the array row
rowFields: [], // additional row fields
})
export const UsersCollection: CollectionConfig = {
slug: 'users',
fields: [
{
...customTenantsArrayField,
label: 'Associated Tenants',
},
],
}