Browse Source

feat: Add job payload validation

Owen Diffey 1 năm trước cách đây
mục cha
commit
479b003a59

+ 4 - 0
backend/src/Job.ts

@@ -168,6 +168,8 @@ export default abstract class Job {
 		return this.constructor._apiEnabled;
 	}
 
+	protected async _validate() {}
+
 	protected async _authorize() {
 		await this._context.assertPermission(this.getPath());
 	}
@@ -189,6 +191,8 @@ export default abstract class Job {
 		this._startedAt = performance.now();
 
 		try {
+			await this._validate();
+
 			await this._authorize();
 
 			const data = await this._execute(this._payload);

+ 9 - 3
backend/src/modules/DataModule/CreateJob.ts

@@ -2,12 +2,18 @@ import DataModule from "../DataModule";
 import DataModuleJob from "./DataModuleJob";
 
 export default abstract class CreateJob extends DataModuleJob {
-	protected async _execute({ query }: { query: Record<string, any[]> }) {
-		if (typeof query !== "object")
+	protected override async _validate() {
+		if (typeof this._payload !== "object")
+			throw new Error("Payload must be an object");
+
+		if (typeof this._payload.query !== "object")
 			throw new Error("Query is not an object");
-		if (Object.keys(query).length === 0)
+
+		if (Object.keys(this._payload.query).length === 0)
 			throw new Error("Empty query object provided");
+	}
 
+	protected async _execute({ query }: { query: Record<string, any[]> }) {
 		const model = await DataModule.getModel(this.getModelName());
 
 		if (model.schema.path("createdBy"))

+ 8 - 3
backend/src/modules/DataModule/DeleteByIdJob.ts

@@ -3,11 +3,16 @@ import DataModule from "../DataModule";
 import DataModuleJob from "./DataModuleJob";
 
 export default abstract class DeleteByIdJob extends DataModuleJob {
-	protected async _execute({ _id }: { _id: Types.ObjectId }) {
-		const model = await DataModule.getModel(this.getModelName());
+	protected override async _validate() {
+		if (typeof this._payload !== "object")
+			throw new Error("Payload must be an object");
 
-		if (!isObjectIdOrHexString(_id))
+		if (!isObjectIdOrHexString(this._payload._id))
 			throw new Error("_id is not an ObjectId");
+	}
+
+	protected async _execute({ _id }: { _id: Types.ObjectId }) {
+		const model = await DataModule.getModel(this.getModelName());
 
 		return model.deleteOne({ _id });
 	}

+ 9 - 1
backend/src/modules/DataModule/FindByIdJob.ts

@@ -1,8 +1,16 @@
-import { Types } from "mongoose";
+import { Types, isObjectIdOrHexString } from "mongoose";
 import DataModule from "../DataModule";
 import DataModuleJob from "./DataModuleJob";
 
 export default abstract class FindByIdJob extends DataModuleJob {
+	protected override async _validate() {
+		if (typeof this._payload !== "object")
+			throw new Error("Payload must be an object");
+
+		if (!isObjectIdOrHexString(this._payload._id))
+			throw new Error("_id is not an ObjectId");
+	}
+
 	protected async _execute({ _id }: { _id: Types.ObjectId }) {
 		const model = await DataModule.getModel(this.getModelName());
 

+ 59 - 1
backend/src/modules/DataModule/GetDataJob.ts

@@ -1,8 +1,66 @@
 import DataModule from "../DataModule";
 import DataModuleJob from "./DataModuleJob";
-import { GetData } from "./plugins/getData";
+import { FilterType, GetData } from "./plugins/getData";
 
 export default abstract class GetDataJob extends DataModuleJob {
+	protected override async _validate() {
+		if (typeof this._payload !== "object")
+			throw new Error("Payload must be an object");
+
+		if (typeof this._payload.page !== "number")
+			throw new Error("Page must be a number");
+
+		if (typeof this._payload.pageSize !== "number")
+			throw new Error("Page size must be a number");
+
+		if (!Array.isArray(this._payload.properties))
+			throw new Error("Properties must be an array");
+
+		this._payload.properties.forEach(property => {
+			if (typeof property !== "string")
+				throw new Error("Property must be a string");
+		});
+
+		if (
+			typeof this._payload.sort !== "object" ||
+			Array.isArray(this._payload.sort)
+		)
+			throw new Error("Sort must be an object");
+
+		Object.values(this._payload.sort).forEach(sort => {
+			if (sort !== "ascending" && sort !== "descending")
+				throw new Error("Sort must be ascending or descending");
+		});
+
+		if (!Array.isArray(this._payload.queries))
+			throw new Error("Queries must be an array");
+
+		Object.values(this._payload.queries).forEach(query => {
+			if (typeof query !== "object" || Array.isArray(query))
+				throw new Error("Query must be an object");
+
+			if (typeof query.filter !== "object" || Array.isArray(query.filter))
+				throw new Error("Query filter must be an object");
+
+			if (typeof query.filter?.property !== "string")
+				throw new Error("Query filter property must be a string");
+
+			if (
+				!Object.values(FilterType).find(
+					value => value === query.filterType
+				)
+			)
+				throw new Error("Invalid Query filter type");
+		});
+
+		if (
+			!["and", "or", "nor"].find(
+				value => value === this._payload.operator
+			)
+		)
+			throw new Error("Operator must be one of; and, or, nor");
+	}
+
 	protected async _execute(payload: Parameters<GetData["getData"]>[0]) {
 		const model = await DataModule.getModel(this.getModelName());
 

+ 14 - 8
backend/src/modules/DataModule/UpdateByIdJob.ts

@@ -3,6 +3,20 @@ import DataModule from "../DataModule";
 import DataModuleJob from "./DataModuleJob";
 
 export default abstract class UpdateByIdJob extends DataModuleJob {
+	protected override async _validate() {
+		if (typeof this._payload !== "object")
+			throw new Error("Payload must be an object");
+
+		if (!isObjectIdOrHexString(this._payload._id))
+			throw new Error("_id is not an ObjectId");
+
+		if (typeof this._payload.query !== "object")
+			throw new Error("Query is not an object");
+
+		if (Object.keys(this._payload.query).length === 0)
+			throw new Error("Empty query object provided");
+	}
+
 	protected async _execute({
 		_id,
 		query
@@ -12,14 +26,6 @@ export default abstract class UpdateByIdJob extends DataModuleJob {
 	}) {
 		const model = await DataModule.getModel(this.getModelName());
 
-		if (!isObjectIdOrHexString(_id))
-			throw new Error("_id is not an ObjectId");
-
-		if (typeof query !== "object")
-			throw new Error("Query is not an object");
-		if (Object.keys(query).length === 0)
-			throw new Error("Empty query object provided");
-
 		return model.updateOne({ _id }, { $set: query });
 	}
 }

+ 23 - 0
backend/src/modules/DataModule/models/news/jobs/Newest.ts

@@ -7,6 +7,29 @@ export default class Newest extends DataModuleJob {
 
 	protected static _hasPermission = true;
 
+	protected override async _validate() {
+		if (
+			typeof this._payload !== "object" &&
+			typeof this._payload !== "undefined" &&
+			this._payload !== null
+		)
+			throw new Error("Payload must be an object or undefined");
+
+		if (
+			typeof this._payload?.showToNewUsers !== "boolean" &&
+			typeof this._payload?.showToNewUsers !== "undefined" &&
+			this._payload?.showToNewUsers !== null
+		)
+			throw new Error("Show to new users must be a boolean or undefined");
+
+		if (
+			typeof this._payload?.limit !== "number" &&
+			typeof this._payload?.limit !== "undefined" &&
+			this._payload?.limit !== null
+		)
+			throw new Error("Limit must be a number or undefined");
+	}
+
 	protected async _execute(payload?: {
 		showToNewUsers?: boolean;
 		limit?: number;

+ 17 - 2
backend/src/modules/DataModule/models/users/jobs/GetModelPermissions.ts

@@ -1,7 +1,7 @@
-import { Types } from "mongoose";
+import { Types, isObjectIdOrHexString } from "mongoose";
 import CacheModule from "@/modules/CacheModule";
 import DataModule from "@/modules/DataModule";
-import { Models, Models } from "@/types/Models";
+import { Models } from "@/types/Models";
 import ModuleManager from "@/ModuleManager";
 import GetPermissions from "./GetPermissions";
 import DataModuleJob from "@/modules/DataModule/DataModuleJob";
@@ -9,6 +9,21 @@ import DataModuleJob from "@/modules/DataModule/DataModuleJob";
 export default class GetModelPermissions extends DataModuleJob {
 	protected static _modelName: keyof Models = "users";
 
+	protected override async _validate() {
+		if (typeof this._payload !== "object")
+			throw new Error("Payload must be an object");
+
+		if (typeof this._payload.modelName !== "string")
+			throw new Error("Model name must be a string");
+
+		if (
+			!isObjectIdOrHexString(this._payload.modelId) &&
+			typeof this._payload.modelId !== "undefined" &&
+			this._payload.modelId !== null
+		)
+			throw new Error("Model Id must be an ObjectId or undefined");
+	}
+
 	protected override async _authorize() {}
 
 	protected async _execute({

+ 10 - 2
backend/src/modules/EventsModule/jobs/Subscribe.ts

@@ -10,13 +10,21 @@ export default class Subscribe extends Job {
 		super(EventsModule, payload, options);
 	}
 
+	protected override async _validate() {
+		if (typeof this._payload !== "object")
+			throw new Error("Payload must be an object");
+
+		if (typeof this._payload.channel !== "string")
+			throw new Error("Channel must be a string");
+	}
+
 	protected override async _authorize() {
 		const [, moduleName, modelName, event, modelId] =
 			/^([a-z]+)\.([a-z]+)\.([A-z]+)\.?([A-z0-9]+)?$/.exec(
-				this._payload?.channel
+				this._payload.channel
 			) ?? [];
 
-		let permission = `event.${this._payload?.channel}`;
+		let permission = `event.${this._payload.channel}`;
 
 		if (
 			moduleName === "model" &&

+ 13 - 0
backend/src/modules/EventsModule/jobs/SubscribeMany.ts

@@ -10,6 +10,19 @@ export default class SubscribeMany extends Job {
 		super(EventsModule, payload, options);
 	}
 
+	protected override async _validate() {
+		if (typeof this._payload !== "object")
+			throw new Error("Payload must be an object");
+
+		if (!Array.isArray(this._payload.channels))
+			throw new Error("Channels must be an array");
+
+		this._payload.channels.forEach(channel => {
+			if (typeof channel !== "string")
+				throw new Error("Channel must be a string");
+		});
+	}
+
 	protected override async _authorize() {
 		await Promise.all(
 			this._payload.channels.map(async channel => {

+ 8 - 0
backend/src/modules/EventsModule/jobs/Unsubscribe.ts

@@ -10,6 +10,14 @@ export default class Unsubscribe extends Job {
 		super(EventsModule, payload, options);
 	}
 
+	protected override async _validate() {
+		if (typeof this._payload !== "object")
+			throw new Error("Payload must be an object");
+
+		if (typeof this._payload.channel !== "string")
+			throw new Error("Channel must be a string");
+	}
+
 	protected override async _authorize() {}
 
 	protected async _execute({ channel }: { channel: string }) {

+ 13 - 0
backend/src/modules/EventsModule/jobs/UnsubscribeMany.ts

@@ -10,6 +10,19 @@ export default class UnsubscribeMany extends Job {
 		super(EventsModule, payload, options);
 	}
 
+	protected override async _validate() {
+		if (typeof this._payload !== "object")
+			throw new Error("Payload must be an object");
+
+		if (!Array.isArray(this._payload.channels))
+			throw new Error("Channels must be an array");
+
+		this._payload.channels.forEach(channel => {
+			if (typeof channel !== "string")
+				throw new Error("Channel must be a string");
+		});
+	}
+
 	protected override async _authorize() {}
 
 	protected async _execute({ channels }: { channels: string[] }) {