payload
Version:
Node, React, Headless CMS and Application Framework built on Next.js
193 lines (192 loc) • 7.5 kB
JavaScript
import { isDeepStrictEqual } from 'util';
import { entityDocExists } from './entityDocExists.js';
import { populateFieldPermissions } from './populateFieldPermissions.js';
const topLevelCollectionPermissions = [
'create',
'delete',
'read',
'readVersions',
'update',
'unlock'
];
const topLevelGlobalPermissions = [
'read',
'readVersions',
'update'
];
/**
* Build up permissions object for an entity (collection or global).
* This is not run during any update and reflects the current state of the entity data => doc and data is the same.
*
* When `fetchData` is false:
* - returned `Where` are not run and evaluated as "does not have permission".
* - If req.data is passed: `data` and `doc` is passed to access functions.
* - If req.data is not passed: `data` and `doc` is not passed to access functions.
*
* When `fetchData` is true:
* - `Where` are run and evaluated as "has permission" or "does not have permission".
* - `data` and `doc` are always passed to access functions.
* - Error is thrown if `entityType` is 'collection' and `id` is not passed.
*
* In both cases:
* We cannot include siblingData or blockData here, as we do not have siblingData available once we reach block or array
* rows, as we're calculating schema permissions, which do not include individual rows.
* For consistency, it's thus better to never include the siblingData and blockData
*
* @internal
*/ export async function getEntityPermissions(args) {
const { id, blockReferencesPermissions, data: _data, entity, entityType, fetchData, operations, req } = args;
const { locale: _locale, user } = req;
const locale = _locale ? _locale : undefined;
if (fetchData && entityType === 'collection' && !id) {
throw new Error('ID is required when fetching data for a collection');
}
const hasData = _data && Object.keys(_data).length > 0;
const data = hasData ? _data : fetchData ? await (async ()=>{
if (entityType === 'global') {
return req.payload.findGlobal({
slug: entity.slug,
depth: 0,
fallbackLocale: null,
locale,
overrideAccess: true,
req
});
}
if (entityType === 'collection') {
return req.payload.findByID({
id: id,
collection: entity.slug,
depth: 0,
fallbackLocale: null,
locale,
overrideAccess: true,
req,
trash: true
});
}
})() : undefined;
const isLoggedIn = !!user;
const fieldsPermissions = {};
const entityPermissions = {
fields: fieldsPermissions
};
const promises = [];
// Phase 1: Resolve all access functions to get where queries
const accessResults = [];
for (const _operation of operations){
const operation = _operation;
const accessFunction = entity.access[operation];
if (entityType === 'collection' && topLevelCollectionPermissions.includes(operation) || entityType === 'global' && topLevelGlobalPermissions.includes(operation)) {
if (typeof accessFunction === 'function') {
accessResults.push({
operation,
result: Promise.resolve(accessFunction({
id,
data,
req
}))
});
} else {
entityPermissions[operation] = {
permission: isLoggedIn
};
}
}
}
// Await all access functions in parallel
const resolvedAccessResults = await Promise.all(accessResults.map(async (item)=>({
operation: item.operation,
result: await item.result
})));
// Phase 2: Process where queries with cache and resolve in parallel
const whereQueryCache = [];
const wherePromises = [];
for (const { operation, result: accessResult } of resolvedAccessResults){
if (typeof accessResult === 'object') {
processWhereQuery({
id,
slug: entity.slug,
accessResult,
entityPermissions,
entityType,
fetchData,
locale,
operation,
req,
wherePromises,
whereQueryCache
});
} else if (entityPermissions[operation]?.permission !== false) {
entityPermissions[operation] = {
permission: !!accessResult
};
}
}
// Await all where query DB calls in parallel
await Promise.all(wherePromises);
populateFieldPermissions({
blockReferencesPermissions,
data,
fields: entity.fields,
operations,
parentPermissionsObject: entityPermissions,
permissionsObject: fieldsPermissions,
promises,
req
});
/**
* Await all promises in parallel.
* A promise can add more promises to the promises array (group of fields calls populateFieldPermissions again in their own promise), which will not be
* awaited in the first run.
* This is why we need to loop again to process the new promises, until there are no more promises left.
*/ let iterations = 0;
while(promises.length > 0){
const currentPromises = promises.splice(0, promises.length);
await Promise.all(currentPromises);
iterations++;
if (iterations >= 100) {
throw new Error('Infinite getEntityPermissions promise loop detected.');
}
}
return entityPermissions;
}
const processWhereQuery = ({ id, slug, accessResult, entityPermissions, entityType, fetchData, locale, operation, req, wherePromises, whereQueryCache })=>{
if (fetchData) {
// Check cache for identical where query using deep comparison
let cached = whereQueryCache.find((entry)=>isDeepStrictEqual(entry.where, accessResult));
if (!cached) {
// Cache miss - start DB query (don't await)
cached = {
result: entityDocExists({
id,
slug,
entityType,
locale,
operation,
req,
where: accessResult
}),
where: accessResult
};
whereQueryCache.push(cached);
}
// Defer resolution to Promise.all (cache hits reuse same promise)
wherePromises.push(cached.result.then((hasPermission)=>{
entityPermissions[operation] = {
permission: hasPermission,
where: accessResult
};
}));
} else {
// TODO: 4.0: Investigate defaulting to `false` here, if where query is returned but ignored as we don't
// have the document data available. This seems more secure.
// Alternatively, we could set permission to a third state, like 'unknown'.
// Even after calling sanitizePermissions, the permissions will still be true if the where query is returned but ignored as we don't have the document data available.
entityPermissions[operation] = {
permission: true,
where: accessResult
};
}
};
//# sourceMappingURL=getEntityPermissions.js.map