|
@@ -2,9 +2,10 @@ import { isObjectIdOrHexString } from "mongoose";
|
|
|
import { forEachIn } from "@common/utils/forEachIn";
|
|
|
import CacheModule from "@/modules/CacheModule";
|
|
|
import DataModule from "@/modules/DataModule";
|
|
|
-import ModuleManager from "@/ModuleManager";
|
|
|
import GetPermissions, { GetPermissionsResult } from "./GetPermissions";
|
|
|
import DataModuleJob from "@/modules/DataModule/DataModuleJob";
|
|
|
+import DataModuleEvent from "@/modules/DataModule/DataModuleEvent";
|
|
|
+import { EventClass } from "@/modules/EventsModule/Event";
|
|
|
|
|
|
export type GetSingleModelPermissionsResult = Record<string, boolean>; // Returned when getting permissions for a single modelId
|
|
|
export type GetMultipleModelPermissionsResult = Record<
|
|
@@ -13,8 +14,16 @@ export type GetMultipleModelPermissionsResult = Record<
|
|
|
>; // Returned when getting permissions for several modelIds
|
|
|
export type GetModelPermissionsResult =
|
|
|
| GetSingleModelPermissionsResult
|
|
|
- | GetMultipleModelPermissionsResult;
|
|
|
-
|
|
|
+ | GetMultipleModelPermissionsResult; // TODO We should probably combine this into a single type of response to make it simpler
|
|
|
+
|
|
|
+/**
|
|
|
+ * Returns permissions for zero, one or more modelIds, for a single modelName, for a specific user
|
|
|
+ * For each modelId, it will return an object with permissions, where object keys are the DataModule job names in camelCase,
|
|
|
+ * prefixed by "data.", and the value is whether the user has permission or not
|
|
|
+ * For events, it will be DataModule event names in camelCase, prefixed by "event.data."
|
|
|
+ *
|
|
|
+ * If no modelId is provided, it will not include jobs that apply specifically to a single modelId (those ending in ById)
|
|
|
+ */
|
|
|
export default class GetModelPermissions extends DataModuleJob {
|
|
|
protected static _modelName = "users";
|
|
|
|
|
@@ -41,6 +50,7 @@ export default class GetModelPermissions extends DataModuleJob {
|
|
|
const { modelName, modelId, modelIds } = this._payload;
|
|
|
|
|
|
const user = await this._context.getUser().catch(() => null);
|
|
|
+ // Gets the generic permissions for the current user, these are not model-specific
|
|
|
const permissions = (await this._context.executeJob(
|
|
|
GetPermissions
|
|
|
)) as GetPermissionsResult;
|
|
@@ -48,6 +58,7 @@ export default class GetModelPermissions extends DataModuleJob {
|
|
|
const Model = await DataModule.getModel(modelName);
|
|
|
if (!Model) throw new Error("Model not found");
|
|
|
|
|
|
+ // If no modelId is supplied, we want to return generic permissions for the provided model for the current user
|
|
|
if (!modelId && (!modelIds || modelIds.length === 0)) {
|
|
|
const cacheKey = this._getCacheKey(user, modelName);
|
|
|
const cached = await CacheModule.get(cacheKey);
|
|
@@ -65,6 +76,7 @@ export default class GetModelPermissions extends DataModuleJob {
|
|
|
return modelPermissions;
|
|
|
}
|
|
|
|
|
|
+ // For a single modelId, we want to return the permissions for that model for the current user
|
|
|
if (modelId) {
|
|
|
const cacheKey = this._getCacheKey(user, modelName, modelId);
|
|
|
const cached = await CacheModule.get(cacheKey);
|
|
@@ -89,6 +101,7 @@ export default class GetModelPermissions extends DataModuleJob {
|
|
|
const result: any = {};
|
|
|
const uncachedModelIds: any = [];
|
|
|
|
|
|
+ // Go through the modelIds, check if any of them already have cached permissions. If they do, use those. For the rest, collect the id's
|
|
|
await forEachIn(modelIds, async modelId => {
|
|
|
const cacheKey = this._getCacheKey(user, modelName, modelId);
|
|
|
const cached = await CacheModule.get(cacheKey);
|
|
@@ -99,8 +112,10 @@ export default class GetModelPermissions extends DataModuleJob {
|
|
|
uncachedModelIds.push(modelId);
|
|
|
});
|
|
|
|
|
|
+ // For the modelIds that were not cached, get the documents from MongoDB
|
|
|
const uncachedModels = await Model.find({ _id: uncachedModelIds });
|
|
|
|
|
|
+ // Loop through the modelIds that were not cached, and get the permissions for each one individually
|
|
|
await forEachIn(uncachedModelIds, async modelId => {
|
|
|
const model = uncachedModels.find(
|
|
|
model => model._id.toString() === modelId.toString()
|
|
@@ -124,6 +139,15 @@ export default class GetModelPermissions extends DataModuleJob {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Returns the permissions for the provided user, generic permissions, model and, if provided, the modelId
|
|
|
+ * It will first take the generic permissions, only including the data/event data permisisons for the provided modelName
|
|
|
+ * After that, it loops through all DataModule jobs for the provided modelName, and checks if the user has permission for that job
|
|
|
+ * If it does, it includes these job names in the result, along with the filtered generic permissions.
|
|
|
+ * One example: with modelName being news, it would get the news FindById job, which always results in "data.news.findById" being true
|
|
|
+ * due to _hasPermission being true in that class
|
|
|
+ * It will also loop through DataModule events in the same manner, except without the extra logic for findById
|
|
|
+ */
|
|
|
protected async _getPermissionsForModel(
|
|
|
user: any,
|
|
|
permissions: GetPermissionsResult,
|
|
@@ -131,6 +155,7 @@ export default class GetModelPermissions extends DataModuleJob {
|
|
|
modelId: string,
|
|
|
model?: any
|
|
|
) {
|
|
|
+ // Filters the generic permissions, only returning the data or event data permissions for the provided model
|
|
|
const modelPermissions = Object.fromEntries(
|
|
|
Object.entries(permissions).filter(
|
|
|
([permission]) =>
|
|
@@ -139,37 +164,95 @@ export default class GetModelPermissions extends DataModuleJob {
|
|
|
)
|
|
|
);
|
|
|
|
|
|
- await forEachIn(
|
|
|
- Object.entries(
|
|
|
- ModuleManager.getModule("data")?.getJobs() ?? {}
|
|
|
- ).filter(
|
|
|
- ([jobName]) =>
|
|
|
- jobName.startsWith(modelName.toString()) &&
|
|
|
- (modelId ? true : !jobName.endsWith("ById"))
|
|
|
- ) as [string, typeof DataModuleJob][],
|
|
|
- async ([jobName, Job]) => {
|
|
|
- jobName = `data.${jobName}`;
|
|
|
+ /**
|
|
|
+ * Get all DataModule jobs for the provided model.
|
|
|
+ * If a modelId is specified, it will return any ById jobs, like DeleteById.
|
|
|
+ * If not specified, it will not return any *ById jobs.
|
|
|
+ */
|
|
|
+ const dataModuleJobs = Object.entries(
|
|
|
+ // ModuleManager.getModule("data")?.getJobs() ?? {}
|
|
|
+ DataModule.getJobs() ?? {}
|
|
|
+ ).filter(
|
|
|
+ ([jobName]) =>
|
|
|
+ jobName.startsWith(modelName.toString()) &&
|
|
|
+ (modelId ? true : !jobName.endsWith("ById"))
|
|
|
+ ) as [string, typeof DataModuleJob][];
|
|
|
+
|
|
|
+ // Loops through all data jobs
|
|
|
+ await forEachIn(dataModuleJobs, async ([jobName, Job]) => {
|
|
|
+ jobName = `data.${jobName}`; // For example, data.news.getData
|
|
|
+
|
|
|
+ // If the generic permissions contains the current job, we don't need to continue further, just say the user has permission for this job
|
|
|
+ if (permissions[jobName]) {
|
|
|
+ modelPermissions[jobName] = true;
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- let hasPermission = permissions[jobName];
|
|
|
+ // If the generic permissions has access to for example "data.news.findManyById.*", the user will have permission to the job "data.news.findManyById"
|
|
|
+ if (permissions[`${jobName}.*`]) {
|
|
|
+ modelPermissions[jobName] = true;
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (!hasPermission && modelId)
|
|
|
- hasPermission =
|
|
|
- permissions[`${jobName}.*`] ||
|
|
|
- permissions[`${jobName}.${modelId}`];
|
|
|
+ /**
|
|
|
+ * If we haven't found a generic permission, but the current job has a hasPermission function, call that function to see if the current user
|
|
|
+ * should have permission for the provided model (document? TODO) (if any). The job, for example data.news.findManyById, will already be aware of the model name
|
|
|
+ * hasPermission can be overwritten, but by default it will check _hasPermission. This is false by default, but can be true, or a function or array of functions
|
|
|
+ */
|
|
|
+ if (
|
|
|
+ typeof Job.hasPermission === "function" &&
|
|
|
+ (await Job.hasPermission(model, user))
|
|
|
+ ) {
|
|
|
+ modelPermissions[jobName] = true;
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (hasPermission) {
|
|
|
- modelPermissions[jobName] = true;
|
|
|
+ // We haven't found permission so far, so we can assume we don't have permission
|
|
|
+ modelPermissions[jobName] = false;
|
|
|
+ });
|
|
|
|
|
|
- return;
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Get all DataModule events for the provided model.
|
|
|
+ */
|
|
|
+ const dataModuleEvents = Object.entries(
|
|
|
+ DataModule.getEvents() ?? {}
|
|
|
+ ).filter(([eventName]) =>
|
|
|
+ eventName.startsWith(modelName.toString())
|
|
|
+ ) as [string, typeof DataModuleEvent & EventClass][];
|
|
|
+
|
|
|
+ // Loops through all data events
|
|
|
+ await forEachIn(dataModuleEvents, async ([eventName, Event]) => {
|
|
|
+ eventName = `event.data.${eventName}`; // For example, event.data.news.created
|
|
|
+
|
|
|
+ // If the generic permissions contains the current event, we don't need to continue further, just say the user has permission for this event
|
|
|
+ if (permissions[eventName]) {
|
|
|
+ modelPermissions[eventName] = true;
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (typeof Job.hasPermission === "function") {
|
|
|
- hasPermission = await Job.hasPermission(model, user);
|
|
|
- }
|
|
|
+ // If the generic permissions has access to for example "event.data.news.updated.*", the user will have permission to the event "event.data.news.updated" regardless of the model id
|
|
|
+ if (permissions[`${eventName}.*`]) {
|
|
|
+ modelPermissions[eventName] = true;
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- modelPermissions[jobName] = !!hasPermission;
|
|
|
+ /**
|
|
|
+ * If we haven't found a generic permission, but the current event has a hasPermission function, call that function to see if the current user
|
|
|
+ * should have permission for the provided model (if any). The event, for example event.data.news.updated, will already be aware of the model name
|
|
|
+ * hasPermission can be overwritten, but by default it will check _hasPermission. This is false by default, but can be changed to true, or a function,
|
|
|
+ * or an array of functions
|
|
|
+ */
|
|
|
+ if (
|
|
|
+ typeof Event.hasPermission === "function" &&
|
|
|
+ (await Event.hasPermission(model, user))
|
|
|
+ ) {
|
|
|
+ modelPermissions[eventName] = true;
|
|
|
+ return;
|
|
|
}
|
|
|
- );
|
|
|
+
|
|
|
+ // We haven't found permission so far, so we can assume we don't have permission
|
|
|
+ modelPermissions[eventName] = false;
|
|
|
+ });
|
|
|
|
|
|
return modelPermissions;
|
|
|
}
|