payload-gatekeeper
Version:
The ultimate access control gatekeeper for Payload CMS v3 - Advanced RBAC with wildcard support, auto role assignment, and flexible configuration
202 lines • 10.5 kB
JavaScript
import { createRolesCollection } from './collections/Roles';
import { enhanceAdminCollection } from './utils/enhanceAdminCollection';
import { syncSystemRoles } from './utils/syncRoles';
import { SUPER_ADMIN_ROLE, PUBLIC_ROLE } from './defaultRoles';
import { createAfterChangeHook } from './hooks';
import { setRolesSlug, getRolesSlug } from './utils/getRolesSlug';
import { createCollectionAccess, createUIVisibilityCheck } from './access';
/**
* Payload Gatekeeper - The ultimate access control plugin for Payload CMS
*
* Features:
* - Automatic permission generation for all collections
* - Role-based access control
* - Wildcard permissions support
* - Super admin management
* - Audit logging (optional)
*
* Your collections' trusted guardian 🚪
*/
export const gatekeeperPlugin = (options = {}) => {
return async (config) => {
const { collections: collectionConfigs = {}, defaultConfig = {}, excludeCollections = [], rolesSlug = 'roles', } = options;
// Set the roles slug globally
setRolesSlug(rolesSlug);
// Get admin user collection slug
const adminCollectionSlug = config.admin?.user;
if (!adminCollectionSlug) {
console.warn('⚠️ No admin user collection configured. Permissions plugin may not work correctly.');
}
// Build collection configs
const finalCollectionConfigs = { ...collectionConfigs };
// Process collections
let enhancedCollections = [...(config.collections || [])];
enhancedCollections = enhancedCollections.map(collection => {
let processedCollection = collection;
// Determine configuration for this collection once
let collectionConfig = finalCollectionConfigs[collection.slug];
const isExcluded = excludeCollections.includes(collection.slug);
// If not explicitly configured, check if we should use default
if (!collectionConfig && collection.auth === true && defaultConfig.enhance) {
collectionConfig = defaultConfig;
}
// Phase 1: Enhancement (if configured)
if (collectionConfig?.enhance) {
// Prepare options for enhancement
const enhanceOptions = {
...options,
roleFieldPlacement: collectionConfig.roleFieldPlacement,
roleFieldConfig: collectionConfig.roleFieldConfig,
};
// Enhance with role field
processedCollection = enhanceAdminCollection(processedCollection, enhanceOptions);
// Add hooks based on collection config
if (collectionConfig.autoAssignFirstUser || collectionConfig.defaultRole) {
const existingAfterChange = processedCollection.hooks?.afterChange || [];
const afterChangeHooks = Array.isArray(existingAfterChange)
? existingAfterChange
: [existingAfterChange].filter(Boolean);
processedCollection = {
...processedCollection,
hooks: {
...processedCollection.hooks,
afterChange: [
...afterChangeHooks,
createAfterChangeHook(collection.slug, collectionConfig),
]
}
};
}
}
// Phase 2: Access Control (unless excluded) - moved here for single pass
if (!isExcluded) {
// Will be applied after roles collection is added
// Mark for access control application
processedCollection._needsAccessControl = true;
}
return processedCollection;
});
// Identify the admin collection for first user setup
// Only check the admin collection configured in config.admin.user
let adminCollectionForFirstUser;
if (adminCollectionSlug) {
const adminConfig = finalCollectionConfigs[adminCollectionSlug];
// Only pass it if it's enhanced and has autoAssignFirstUser
if (adminConfig?.enhance && adminConfig?.autoAssignFirstUser) {
adminCollectionForFirstUser = adminCollectionSlug;
}
}
// Create Roles collection with all available collections
const rolesCollection = createRolesCollection(enhancedCollections, options, adminCollectionForFirstUser);
// Add Roles collection if it doesn't exist
const hasRolesCollection = enhancedCollections.some(c => c.slug === rolesSlug);
if (!hasRolesCollection) {
// Check if Roles collection should be excluded from access control
const isRolesExcluded = excludeCollections.includes(rolesSlug);
// Mark the Roles collection for access control if not excluded
const rolesWithMarker = {
...rolesCollection,
_needsAccessControl: !isRolesExcluded
};
enhancedCollections.push(rolesWithMarker);
}
// Apply access control to marked collections and clean up
const finalCollections = enhancedCollections.map(collection => {
// Skip if not marked for access control
if (!collection._needsAccessControl) {
// Return without marker
const { _needsAccessControl, ...cleanCollection } = collection;
return cleanCollection;
}
// Remove marker for access control processing
const { _needsAccessControl, ...cleanCollection } = collection;
// Add UI visibility check based on 'manage' permission as wrapper
const originalHidden = collection.admin?.hidden;
const permissionHidden = createUIVisibilityCheck(collection.slug);
const enhancedAdmin = {
...collection.admin,
// Wrapper: Check original hidden function first, then permission-based visibility
hidden: typeof originalHidden === 'function'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
? (args) => {
// First check original hidden function
const isOriginallyHidden = originalHidden(args);
if (isOriginallyHidden)
return true;
// Then check permission-based visibility
return permissionHidden(args);
}
: originalHidden === true
? true // If originally always hidden, keep it hidden
: permissionHidden // Otherwise use permission check
};
// Add permission-based access control as wrapper around existing access control
return {
...cleanCollection,
admin: enhancedAdmin,
access: createCollectionAccess(cleanCollection, options),
};
});
// Return enhanced config
return {
...config,
collections: finalCollections,
onInit: async (payload) => {
// Check if roles exist
const rolesCount = await payload.count({ collection: getRolesSlug() });
// Sync system roles if:
// - No roles exist (first start)
// - In development mode
// - Explicitly requested via config
const shouldSyncRoles = rolesCount.totalDocs === 0 || // Automatically sync on first start
process.env.NODE_ENV === 'development' ||
options.syncRolesOnInit === true;
if (shouldSyncRoles) {
console.info('🔄 Syncing system roles...');
try {
// Identify admin collections (those with autoAssignFirstUser)
const adminCollections = Object.entries(finalCollectionConfigs)
.filter(([_, config]) => config.enhance && config.autoAssignFirstUser)
.map(([slug]) => slug);
// Configure Super Admin role visibility
const superAdminRole = {
...SUPER_ADMIN_ROLE,
// Super Admin only visible for admin collections
visibleFor: adminCollections.length > 0 ? adminCollections : undefined
};
// Prepare public role if not disabled
const publicRole = !options.disablePublicRole ? {
...PUBLIC_ROLE,
permissions: options.publicRolePermissions || PUBLIC_ROLE.permissions
} : null;
// Always include super_admin, public (if enabled), plus any configured roles
const rolesToSync = [
superAdminRole,
...(publicRole ? [publicRole] : []),
...(options.systemRoles || [])
];
const results = await syncSystemRoles(payload, rolesToSync);
// Only log if there were changes
if (results.created.length > 0 || results.updated.length > 0) {
console.info('✅ Role sync completed');
}
}
catch (error) {
console.error('❌ Error syncing roles:', error);
// Don't fail initialization if role sync fails
// The system can still work with existing roles
}
}
// Run original onInit if exists
if (config.onInit) {
await config.onInit(payload);
}
},
};
};
};
// Export utilities for use outside the plugin
export { checkPermission, hasPermission, canAssignRole } from './utils/checkPermission';
export { PERMISSIONS } from './constants';
export { EXAMPLE_ROLES } from './defaultRoles';
//# sourceMappingURL=index.js.map