Browse Source

refactor: collect permissions dynamically using hasPermission, rename old model hasPermission properties/functions to differntiate

Kristian Vos 8 months ago
parent
commit
f8e33fa43c
52 changed files with 535 additions and 328 deletions
  1. 26 0
      backend/src/Job.ts
  2. 5 5
      backend/src/modules/DataModule/DataModuleEvent.ts
  3. 5 5
      backend/src/modules/DataModule/DataModuleJob.ts
  4. 3 0
      backend/src/modules/DataModule/models/minifiedUsers/events/MinifiedUserCreatedEvent.ts
  5. 5 2
      backend/src/modules/DataModule/models/minifiedUsers/events/MinifiedUserDeletedEvent.ts
  6. 5 2
      backend/src/modules/DataModule/models/minifiedUsers/events/MinifiedUserUpdatedEvent.ts
  7. 0 2
      backend/src/modules/DataModule/models/minifiedUsers/jobs/FindById.ts
  8. 0 2
      backend/src/modules/DataModule/models/minifiedUsers/jobs/FindManyById.ts
  9. 3 0
      backend/src/modules/DataModule/models/news/events/NewsCreatedEvent.ts
  10. 5 2
      backend/src/modules/DataModule/models/news/events/NewsDeletedEvent.ts
  11. 5 2
      backend/src/modules/DataModule/models/news/events/NewsUpdatedEvent.ts
  12. 3 0
      backend/src/modules/DataModule/models/news/jobs/Create.ts
  13. 3 0
      backend/src/modules/DataModule/models/news/jobs/DeleteById.ts
  14. 3 0
      backend/src/modules/DataModule/models/news/jobs/DeleteManyById.ts
  15. 2 1
      backend/src/modules/DataModule/models/news/jobs/FindById.ts
  16. 2 1
      backend/src/modules/DataModule/models/news/jobs/FindManyById.ts
  17. 3 0
      backend/src/modules/DataModule/models/news/jobs/GetData.ts
  18. 3 0
      backend/src/modules/DataModule/models/news/jobs/UpdateById.ts
  19. 3 0
      backend/src/modules/DataModule/models/stations/events/StationCreatedEvent.ts
  20. 8 5
      backend/src/modules/DataModule/models/stations/events/StationDeletedEvent.ts
  21. 8 5
      backend/src/modules/DataModule/models/stations/events/StationUpdatedEvent.ts
  22. 2 2
      backend/src/modules/DataModule/models/stations/jobs/Create.ts
  23. 2 2
      backend/src/modules/DataModule/models/stations/jobs/DeleteById.ts
  24. 2 2
      backend/src/modules/DataModule/models/stations/jobs/DeleteManyById.ts
  25. 10 5
      backend/src/modules/DataModule/models/stations/jobs/FindById.ts
  26. 10 5
      backend/src/modules/DataModule/models/stations/jobs/FindManyById.ts
  27. 4 6
      backend/src/modules/DataModule/models/stations/jobs/Index.ts
  28. 3 3
      backend/src/modules/DataModule/models/stations/jobs/UpdateById.ts
  29. 3 0
      backend/src/modules/DataModule/models/users/events/UserCreatedEvent.ts
  30. 3 0
      backend/src/modules/DataModule/models/users/events/UserDeletedEvent.ts
  31. 2 0
      backend/src/modules/DataModule/models/users/events/UserUpdatedEvent.ts
  32. 1 1
      backend/src/modules/DataModule/models/users/jobs/FindById.ts
  33. 3 0
      backend/src/modules/DataModule/models/users/jobs/GetData.ts
  34. 14 13
      backend/src/modules/DataModule/models/users/jobs/GetModelPermissions.ts
  35. 67 13
      backend/src/modules/DataModule/models/users/jobs/GetPermissions.ts
  36. 223 223
      backend/src/modules/DataModule/models/users/permissions.ts
  37. 6 0
      backend/src/modules/DataModule/permissions/isAdmin.ts
  38. 2 5
      backend/src/modules/DataModule/permissions/isLoggedIn.ts
  39. 6 0
      backend/src/modules/DataModule/permissions/isModerator.ts
  40. 0 0
      backend/src/modules/DataModule/permissions/modelPermissions/doesModelExist.ts
  41. 2 2
      backend/src/modules/DataModule/permissions/modelPermissions/isDj.ts
  42. 7 0
      backend/src/modules/DataModule/permissions/modelPermissions/isLoggedIn.ts
  43. 2 2
      backend/src/modules/DataModule/permissions/modelPermissions/isNewsPublished.ts
  44. 1 1
      backend/src/modules/DataModule/permissions/modelPermissions/isOwner.ts
  45. 0 0
      backend/src/modules/DataModule/permissions/modelPermissions/isPrivate.ts
  46. 1 1
      backend/src/modules/DataModule/permissions/modelPermissions/isPublic.ts
  47. 0 0
      backend/src/modules/DataModule/permissions/modelPermissions/isUnlisted.ts
  48. 27 0
      backend/src/modules/EventsModule/Event.ts
  49. 27 3
      backend/src/modules/EventsModule/jobs/Subscribe.spec.ts
  50. 2 2
      backend/src/modules/EventsModule/jobs/Unsubscribe.ts
  51. 1 1
      backend/src/modules/EventsModule/jobs/UnsubscribeAll.ts
  52. 2 2
      backend/src/modules/EventsModule/jobs/UnsubscribeMany.ts

+ 26 - 0
backend/src/Job.ts

@@ -1,12 +1,14 @@
 import { SessionSchema } from "@models/sessions/schema";
 import { getErrorMessage } from "@common/utils/getErrorMessage";
 import { generateUuid } from "@common/utils/generateUuid";
+import { HydratedDocument } from "mongoose";
 import JobContext from "@/JobContext";
 import JobStatistics, { JobStatisticsType } from "@/JobStatistics";
 import LogBook, { Log } from "@/LogBook";
 import BaseModule from "./BaseModule";
 import EventsModule from "./modules/EventsModule";
 import JobCompletedEvent from "./modules/EventsModule/events/JobCompletedEvent";
+import { UserSchema } from "./modules/DataModule/models/users/schema";
 
 export enum JobStatus {
 	QUEUED = "QUEUED",
@@ -176,6 +178,30 @@ export default abstract class Job {
 		return (this.constructor as typeof Job)._apiEnabled;
 	}
 
+	protected static _hasPermission:
+		| boolean
+		| CallableFunction
+		| (boolean | CallableFunction)[] = false;
+
+	// Check if a given user has generic permission to execute a job, using _hasPermission
+	public static async hasPermission(
+		user: HydratedDocument<UserSchema> | null
+	) {
+		const options = Array.isArray(this._hasPermission)
+			? this._hasPermission
+			: [this._hasPermission];
+
+		return options.reduce(async (previous, option) => {
+			if (await previous) return true;
+
+			if (typeof option === "boolean") return option;
+
+			if (typeof option === "function") return option(user);
+
+			return false;
+		}, Promise.resolve(false));
+	}
+
 	protected async _validate() {}
 
 	protected async _authorize() {

+ 5 - 5
backend/src/modules/DataModule/DataModuleEvent.ts

@@ -8,7 +8,7 @@ export default abstract class DataModuleEvent extends ModuleEvent {
 
 	protected static _modelName: string;
 
-	protected static _hasPermission:
+	protected static _hasModelPermission:
 		| boolean
 		| CallableFunction
 		| (boolean | CallableFunction)[] = false;
@@ -21,13 +21,13 @@ export default abstract class DataModuleEvent extends ModuleEvent {
 		return this._modelName;
 	}
 
-	public static async hasPermission(
+	public static async hasModelPermission(
 		model: HydratedDocument<Model<any>>, // TODO model can be null too, as GetModelPermissions is currently written
 		user: HydratedDocument<UserSchema> | null
 	) {
-		const options = Array.isArray(this._hasPermission)
-			? this._hasPermission
-			: [this._hasPermission];
+		const options = Array.isArray(this._hasModelPermission)
+			? this._hasModelPermission
+			: [this._hasModelPermission];
 
 		return options.reduce(async (previous, option) => {
 			if (await previous) return true;

+ 5 - 5
backend/src/modules/DataModule/DataModuleJob.ts

@@ -8,7 +8,7 @@ export default abstract class DataModuleJob extends Job {
 
 	protected static _isBulk = false;
 
-	protected static _hasPermission:
+	protected static _hasModelPermission:
 		| boolean
 		| CallableFunction
 		| (boolean | CallableFunction)[] = false;
@@ -43,13 +43,13 @@ export default abstract class DataModuleJob extends Job {
 		return (this.constructor as typeof DataModuleJob)._isBulk;
 	}
 
-	public static async hasPermission(
+	public static async hasModelPermission(
 		model: HydratedDocument<Model<any>>, // TODO model can be null too, as GetModelPermissions is currently written
 		user: HydratedDocument<UserSchema> | null
 	) {
-		const options = Array.isArray(this._hasPermission)
-			? this._hasPermission
-			: [this._hasPermission];
+		const options = Array.isArray(this._hasModelPermission)
+			? this._hasModelPermission
+			: [this._hasModelPermission];
 
 		return options.reduce(async (previous, option) => {
 			if (await previous) return true;

+ 3 - 0
backend/src/modules/DataModule/models/minifiedUsers/events/MinifiedUserCreatedEvent.ts

@@ -1,5 +1,8 @@
 import ModelCreatedEvent from "@/modules/DataModule/ModelCreatedEvent";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default abstract class MinifiedUserCreatedEvent extends ModelCreatedEvent {
 	protected static _modelName = "minifiedUsers";
+
+	protected static _hasPermission = isAdmin;
 }

+ 5 - 2
backend/src/modules/DataModule/models/minifiedUsers/events/MinifiedUserDeletedEvent.ts

@@ -1,12 +1,15 @@
 import ModelDeletedEvent from "@/modules/DataModule/ModelDeletedEvent";
-import doesModelExist from "@/modules/DataModule/permissions/doesModelExist";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
+import doesModelExist from "@/modules/DataModule/permissions/modelPermissions/doesModelExist";
 
 export default abstract class MinifiedUserDeletedEvent extends ModelDeletedEvent {
 	protected static _modelName = "minifiedUsers";
 
+	protected static _hasPermission = isAdmin;
+
 	/**
 	 * If a modelId was specified, any user can subscribe.
 	 * If not, only admins can subscribe.
 	 */
-	protected static _hasPermission = doesModelExist;
+	protected static _hasModelPermission = doesModelExist;
 }

+ 5 - 2
backend/src/modules/DataModule/models/minifiedUsers/events/MinifiedUserUpdatedEvent.ts

@@ -1,12 +1,15 @@
 import ModelUpdatedEvent from "@/modules/DataModule/ModelUpdatedEvent";
-import doesModelExist from "@/modules/DataModule/permissions/doesModelExist";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
+import doesModelExist from "@/modules/DataModule/permissions/modelPermissions/doesModelExist";
 
 export default abstract class MinifiedUserUpdatedEvent extends ModelUpdatedEvent {
 	protected static _modelName = "minifiedUsers";
 
+	protected static _hasPermission = isAdmin;
+
 	/**
 	 * If a modelId was specified, any user can subscribe.
 	 * If not, only admins can subscribe.
 	 */
-	protected static _hasPermission = doesModelExist;
+	protected static _hasModelPermission = doesModelExist;
 }

+ 0 - 2
backend/src/modules/DataModule/models/minifiedUsers/jobs/FindById.ts

@@ -4,6 +4,4 @@ export default class FindById extends FindByIdJob {
 	protected static _modelName = "minifiedUsers";
 
 	protected static _hasPermission = true;
-
-	protected async _authorize() {}
 }

+ 0 - 2
backend/src/modules/DataModule/models/minifiedUsers/jobs/FindManyById.ts

@@ -4,6 +4,4 @@ export default class FindManyById extends FindManyByIdJob {
 	protected static _modelName = "minifiedUsers";
 
 	protected static _hasPermission = true;
-
-	protected async _authorize() {}
 }

+ 3 - 0
backend/src/modules/DataModule/models/news/events/NewsCreatedEvent.ts

@@ -1,5 +1,8 @@
 import ModelCreatedEvent from "@/modules/DataModule/ModelCreatedEvent";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default abstract class NewsCreatedEvent extends ModelCreatedEvent {
 	protected static _modelName = "news";
+
+	protected static _hasPermission = isAdmin;
 }

+ 5 - 2
backend/src/modules/DataModule/models/news/events/NewsDeletedEvent.ts

@@ -1,8 +1,11 @@
 import ModelDeletedEvent from "@/modules/DataModule/ModelDeletedEvent";
-import isNewsPublished from "@/modules/DataModule/permissions/isNewsPublished";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
+import isNewsPublished from "@/modules/DataModule/permissions/modelPermissions/isNewsPublished";
 
 export default abstract class NewsDeletedEvent extends ModelDeletedEvent {
 	protected static _modelName = "news";
 
-	protected static _hasPermission = isNewsPublished;
+	protected static _hasPermission = isAdmin;
+
+	protected static _hasModelPermission = isNewsPublished;
 }

+ 5 - 2
backend/src/modules/DataModule/models/news/events/NewsUpdatedEvent.ts

@@ -1,8 +1,11 @@
 import ModelUpdatedEvent from "@/modules/DataModule/ModelUpdatedEvent";
-import isNewsPublished from "@/modules/DataModule/permissions/isNewsPublished";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
+import isNewsPublished from "@/modules/DataModule/permissions/modelPermissions/isNewsPublished";
 
 export default abstract class NewsUpdatedEvent extends ModelUpdatedEvent {
 	protected static _modelName = "news";
 
-	protected static _hasPermission = isNewsPublished;
+	protected static _hasPermission = isAdmin;
+
+	protected static _hasModelPermission = isNewsPublished;
 }

+ 3 - 0
backend/src/modules/DataModule/models/news/jobs/Create.ts

@@ -1,5 +1,8 @@
 import CreateJob from "@/modules/DataModule/CreateJob";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default class Create extends CreateJob {
 	protected static _modelName = "news";
+
+	protected static _hasPermission = isAdmin;
 }

+ 3 - 0
backend/src/modules/DataModule/models/news/jobs/DeleteById.ts

@@ -1,5 +1,8 @@
 import DeleteByIdJob from "@/modules/DataModule/DeleteByIdJob";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default class DeleteById extends DeleteByIdJob {
 	protected static _modelName = "news";
+
+	protected static _hasPermission = isAdmin;
 }

+ 3 - 0
backend/src/modules/DataModule/models/news/jobs/DeleteManyById.ts

@@ -1,5 +1,8 @@
 import DeleteManyByIdJob from "@/modules/DataModule/DeleteManyByIdJob";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default class DeleteManyById extends DeleteManyByIdJob {
 	protected static _modelName = "news";
+
+	protected static _hasPermission = isAdmin;
 }

+ 2 - 1
backend/src/modules/DataModule/models/news/jobs/FindById.ts

@@ -1,7 +1,8 @@
 import FindByIdJob from "@/modules/DataModule/FindByIdJob";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default class FindById extends FindByIdJob {
 	protected static _modelName = "news";
 
-	protected static _hasPermission = true;
+	protected static _hasPermission = isAdmin;
 }

+ 2 - 1
backend/src/modules/DataModule/models/news/jobs/FindManyById.ts

@@ -1,7 +1,8 @@
 import FindManyByIdJob from "@/modules/DataModule/FindManyByIdJob";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default class FindManyById extends FindManyByIdJob {
 	protected static _modelName = "news";
 
-	protected static _hasPermission = true;
+	protected static _hasPermission = isAdmin;
 }

+ 3 - 0
backend/src/modules/DataModule/models/news/jobs/GetData.ts

@@ -1,5 +1,8 @@
 import GetDataJob from "@/modules/DataModule/GetDataJob";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default class GetData extends GetDataJob {
 	protected static _modelName = "news";
+
+	protected static _hasPermission = isAdmin;
 }

+ 3 - 0
backend/src/modules/DataModule/models/news/jobs/UpdateById.ts

@@ -1,5 +1,8 @@
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 import UpdateByIdJob from "@/modules/DataModule/UpdateByIdJob";
 
 export default class UpdateById extends UpdateByIdJob {
 	protected static _modelName = "news";
+
+	protected static _hasPermission = isAdmin;
 }

+ 3 - 0
backend/src/modules/DataModule/models/stations/events/StationCreatedEvent.ts

@@ -1,5 +1,8 @@
 import ModelCreatedEvent from "@/modules/DataModule/ModelCreatedEvent";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default abstract class StationCreatedEvent extends ModelCreatedEvent {
 	protected static _modelName = "stations";
+
+	protected static _hasPermission = isAdmin;
 }

+ 8 - 5
backend/src/modules/DataModule/models/stations/events/StationDeletedEvent.ts

@@ -1,13 +1,16 @@
 import ModelDeletedEvent from "@/modules/DataModule/ModelDeletedEvent";
-import isUnlisted from "@/modules/DataModule/permissions/isUnlisted";
-import isPublic from "@/modules/DataModule/permissions/isPublic";
-import isOwner from "@/modules/DataModule/permissions/isOwner";
-import isDj from "@/modules/DataModule/permissions/isDj";
+import isUnlisted from "@/modules/DataModule/permissions/modelPermissions/isUnlisted";
+import isPublic from "@/modules/DataModule/permissions/modelPermissions/isPublic";
+import isOwner from "@/modules/DataModule/permissions/modelPermissions/isOwner";
+import isDj from "@/modules/DataModule/permissions/modelPermissions/isDj";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default abstract class StationDeletedEvent extends ModelDeletedEvent {
 	protected static _modelName = "stations";
 
-	protected static _hasPermission = [
+	protected static _hasPermission = isAdmin;
+
+	protected static _hasModelPermission = [
 		isPublic,
 		isUnlisted,
 		isDj,

+ 8 - 5
backend/src/modules/DataModule/models/stations/events/StationUpdatedEvent.ts

@@ -1,13 +1,16 @@
 import ModelUpdatedEvent from "@/modules/DataModule/ModelUpdatedEvent";
-import isPublic from "@/modules/DataModule/permissions/isPublic";
-import isUnlisted from "@/modules/DataModule/permissions/isUnlisted";
-import isOwner from "@/modules/DataModule/permissions/isOwner";
-import isDj from "@/modules/DataModule/permissions/isDj";
+import isPublic from "@/modules/DataModule/permissions/modelPermissions/isPublic";
+import isUnlisted from "@/modules/DataModule/permissions/modelPermissions/isUnlisted";
+import isOwner from "@/modules/DataModule/permissions/modelPermissions/isOwner";
+import isDj from "@/modules/DataModule/permissions/modelPermissions/isDj";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default abstract class StationUpdatedEvent extends ModelUpdatedEvent {
 	protected static _modelName = "stations";
 
-	protected static _hasPermission = [
+	protected static _hasPermission = isAdmin;
+
+	protected static _hasModelPermission = [
 		isPublic,
 		isUnlisted,
 		isDj,

+ 2 - 2
backend/src/modules/DataModule/models/stations/jobs/Create.ts

@@ -1,8 +1,8 @@
 import CreateJob from "@/modules/DataModule/CreateJob";
-import isLoggedIn from "@/modules/DataModule/permissions/isLoggedIn";
+import isLoggedIn from "@/modules/DataModule/permissions/modelPermissions/isLoggedIn";
 
 export default class Create extends CreateJob {
 	protected static _modelName = "stations";
 
-	protected static _hasPermission = isLoggedIn;
+	protected static _hasModelPermission = isLoggedIn;
 }

+ 2 - 2
backend/src/modules/DataModule/models/stations/jobs/DeleteById.ts

@@ -1,8 +1,8 @@
 import DeleteByIdJob from "@/modules/DataModule/DeleteByIdJob";
-import isOwner from "@/modules/DataModule/permissions/isOwner";
+import isOwner from "@/modules/DataModule/permissions/modelPermissions/isOwner";
 
 export default class DeleteById extends DeleteByIdJob {
 	protected static _modelName = "stations";
 
-	protected static _hasPermission = isOwner;
+	protected static _hasModelPermission = isOwner;
 }

+ 2 - 2
backend/src/modules/DataModule/models/stations/jobs/DeleteManyById.ts

@@ -1,8 +1,8 @@
 import DeleteManyByIdJob from "@/modules/DataModule/DeleteManyByIdJob";
-import isOwner from "@/modules/DataModule/permissions/isOwner";
+import isOwner from "@/modules/DataModule/permissions/modelPermissions/isOwner";
 
 export default class DeleteManyById extends DeleteManyByIdJob {
 	protected static _modelName = "stations";
 
-	protected static _hasPermission = isOwner;
+	protected static _hasModelPermission = isOwner;
 }

+ 10 - 5
backend/src/modules/DataModule/models/stations/jobs/FindById.ts

@@ -1,11 +1,16 @@
 import FindByIdJob from "@/modules/DataModule/FindByIdJob";
-import isDj from "@/modules/DataModule/permissions/isDj";
-import isPublic from "@/modules/DataModule/permissions/isPublic";
-import isUnlisted from "@/modules/DataModule/permissions/isUnlisted";
-import isOwner from "@/modules/DataModule/permissions/isOwner";
+import isDj from "@/modules/DataModule/permissions/modelPermissions/isDj";
+import isPublic from "@/modules/DataModule/permissions/modelPermissions/isPublic";
+import isUnlisted from "@/modules/DataModule/permissions/modelPermissions/isUnlisted";
+import isOwner from "@/modules/DataModule/permissions/modelPermissions/isOwner";
 
 export default class FindById extends FindByIdJob {
 	protected static _modelName = "stations";
 
-	protected static _hasPermission = [isOwner, isDj, isPublic, isUnlisted];
+	protected static _hasModelPermission = [
+		isOwner,
+		isDj,
+		isPublic,
+		isUnlisted
+	];
 }

+ 10 - 5
backend/src/modules/DataModule/models/stations/jobs/FindManyById.ts

@@ -1,11 +1,16 @@
 import FindManyByIdJob from "@/modules/DataModule/FindManyByIdJob";
-import isDj from "@/modules/DataModule/permissions/isDj";
-import isPublic from "@/modules/DataModule/permissions/isPublic";
-import isUnlisted from "@/modules/DataModule/permissions/isUnlisted";
-import isOwner from "@/modules/DataModule/permissions/isOwner";
+import isDj from "@/modules/DataModule/permissions/modelPermissions/isDj";
+import isPublic from "@/modules/DataModule/permissions/modelPermissions/isPublic";
+import isUnlisted from "@/modules/DataModule/permissions/modelPermissions/isUnlisted";
+import isOwner from "@/modules/DataModule/permissions/modelPermissions/isOwner";
 
 export default class FindManyById extends FindManyByIdJob {
 	protected static _modelName = "stations";
 
-	protected static _hasPermission = [isOwner, isDj, isPublic, isUnlisted];
+	protected static _hasModelPermission = [
+		isOwner,
+		isDj,
+		isPublic,
+		isUnlisted
+	];
 }

+ 4 - 6
backend/src/modules/DataModule/models/stations/jobs/Index.ts

@@ -2,9 +2,9 @@ import { HydratedDocument } from "mongoose";
 import { forEachIn } from "@common/utils/forEachIn";
 import DataModule from "@/modules/DataModule";
 import DataModuleJob from "@/modules/DataModule/DataModuleJob";
-import isDj from "@/modules/DataModule/permissions/isDj";
-import isOwner from "@/modules/DataModule/permissions/isOwner";
-import isPublic from "@/modules/DataModule/permissions/isPublic";
+import isDj from "@/modules/DataModule/permissions/modelPermissions/isDj";
+import isOwner from "@/modules/DataModule/permissions/modelPermissions/isOwner";
+import isPublic from "@/modules/DataModule/permissions/modelPermissions/isPublic";
 import { StationModel, StationSchema } from "../schema";
 
 export default class Index extends DataModuleJob {
@@ -28,8 +28,6 @@ export default class Index extends DataModuleJob {
 			throw new Error("Admin filter must be a boolean or undefined");
 	}
 
-	protected override async _authorize() {}
-
 	protected async _execute() {
 		const model = await DataModule.getModel<StationModel>(
 			this.getModelName()
@@ -47,7 +45,7 @@ export default class Index extends DataModuleJob {
 				(user && (isOwner(station, user) || isDj(station, user))) ||
 				(this._payload?.adminFilter &&
 					(await this._context
-						.assertPermission("data.stations.index:adminFilter")
+						.assertPermission("data.stations.index:adminFilter") // TODO fix in new permission system
 						.then(() => true)
 						.catch(() => false)))
 			)

+ 3 - 3
backend/src/modules/DataModule/models/stations/jobs/UpdateById.ts

@@ -1,9 +1,9 @@
 import UpdateByIdJob from "@/modules/DataModule/UpdateByIdJob";
-import isDj from "@/modules/DataModule/permissions/isDj";
-import isOwner from "@/modules/DataModule/permissions/isOwner";
+import isDj from "@/modules/DataModule/permissions/modelPermissions/isDj";
+import isOwner from "@/modules/DataModule/permissions/modelPermissions/isOwner";
 
 export default class UpdateById extends UpdateByIdJob {
 	protected static _modelName = "stations";
 
-	protected static _hasPermission = [isOwner, isDj];
+	protected static _hasModelPermission = [isOwner, isDj];
 }

+ 3 - 0
backend/src/modules/DataModule/models/users/events/UserCreatedEvent.ts

@@ -1,5 +1,8 @@
 import ModelCreatedEvent from "@/modules/DataModule/ModelCreatedEvent";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default abstract class UserCreatedEvent extends ModelCreatedEvent {
 	protected static _modelName = "users";
+
+	protected static _hasPermission = isAdmin;
 }

+ 3 - 0
backend/src/modules/DataModule/models/users/events/UserDeletedEvent.ts

@@ -1,5 +1,8 @@
 import ModelDeletedEvent from "@/modules/DataModule/ModelDeletedEvent";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default abstract class UserDeletedEvent extends ModelDeletedEvent {
 	protected static _modelName = "users";
+
+	protected static _hasPermission = isAdmin;
 }

+ 2 - 0
backend/src/modules/DataModule/models/users/events/UserUpdatedEvent.ts

@@ -1,7 +1,9 @@
 import ModelUpdatedEvent from "@/modules/DataModule/ModelUpdatedEvent";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default abstract class UserUpdatedEvent extends ModelUpdatedEvent {
 	protected static _modelName = "users";
 
+	protected static _hasPermission = isAdmin;
 	// TODO maybe allow this for the current logged in user?
 }

+ 1 - 1
backend/src/modules/DataModule/models/users/jobs/FindById.ts

@@ -5,7 +5,7 @@ import { UserModel } from "../schema";
 export default class FindById extends FindByIdJob {
 	protected static _modelName = "users";
 
-	protected static _hasPermission = this._isSelf;
+	protected static _hasModelPermission = this._isSelf;
 
 	protected static _isSelf(
 		model: HydratedDocument<UserModel>,

+ 3 - 0
backend/src/modules/DataModule/models/users/jobs/GetData.ts

@@ -1,5 +1,8 @@
 import GetDataJob from "@/modules/DataModule/GetDataJob";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default class GetData extends GetDataJob {
 	protected static _modelName = "users";
+
+	protected static _hasPermission = isAdmin;
 }

+ 14 - 13
backend/src/modules/DataModule/models/users/jobs/GetModelPermissions.ts

@@ -44,6 +44,7 @@ export default class GetModelPermissions extends DataModuleJob {
 			throw new Error("Model Id must be an ObjectId or undefined");
 	}
 
+	// _authorize calls GetPermissions and GetModelPermissions, so to avoid ending up in an infinite loop, just override it
 	protected override async _authorize() {}
 
 	protected async _execute(): Promise<GetModelPermissionsResult> {
@@ -145,7 +146,7 @@ export default class GetModelPermissions extends DataModuleJob {
 	 * 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
+	 * due to _hasModelPermission 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(
@@ -188,20 +189,20 @@ export default class GetModelPermissions extends DataModuleJob {
 				return;
 			}
 
-			// 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}.*`]) {
+			// 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 we haven't found a generic permission, but the current job has a hasPermission function, call that function to see if the current user
+			 * If we haven't found a generic permission, but the current job has a hasModelPermission 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
+			 * hasModelPermission can be overwritten, but by default it will check _hasModelPermission. 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))
+				typeof Job.hasModelPermission === "function" &&
+				(await Job.hasModelPermission(model, user))
 			) {
 				modelPermissions[jobName] = true;
 				return;
@@ -230,21 +231,21 @@ export default class GetModelPermissions extends DataModuleJob {
 				return;
 			}
 
-			// 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}.*`]) {
+			// 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;
 			}
 
 			/**
-			 * 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
+			 * If we haven't found a generic permission, but the current event has a hasModelPermission 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,
+			 * hasModelPermission can be overwritten, but by default it will check _hasModelPermission. 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))
+				typeof Event.hasModelPermission === "function" &&
+				(await Event.hasModelPermission(model, user))
 			) {
 				modelPermissions[eventName] = true;
 				return;

+ 67 - 13
backend/src/modules/DataModule/models/users/jobs/GetPermissions.ts

@@ -1,7 +1,11 @@
+import { HydratedDocument } from "mongoose";
+import { forEachIn } from "@common/utils/forEachIn";
 import CacheModule from "@/modules/CacheModule";
-import permissions from "@/modules/DataModule/models/users/permissions";
-import { UserRole } from "../UserRole";
 import DataModuleJob from "@/modules/DataModule/DataModuleJob";
+import { UserSchema } from "../schema";
+import ModuleManager from "@/ModuleManager";
+import Job from "@/Job";
+import Event from "@/modules/EventsModule/Event";
 
 export type GetPermissionsResult = Record<string, boolean>;
 
@@ -14,29 +18,79 @@ export default class GetPermissions extends DataModuleJob {
 
 	protected static _hasPermission = true;
 
+	// _authorize calls GetPermissions and GetModelPermissions, so to avoid ending up in an infinite loop, just override it
 	protected override async _authorize() {}
 
 	protected async _execute(): Promise<GetPermissionsResult> {
 		const user = await this._context.getUser().catch(() => null);
 
-		if (!user) return permissions.guest;
+		const cacheKey = user
+			? `user-permissions.${user._id}`
+			: `user-permissions.guest`;
+		const cachedPermissions = await CacheModule.get(cacheKey);
+		if (cachedPermissions) return cachedPermissions;
 
-		const cacheKey = `user-permissions.${user._id}`;
+		const permissions = await this._getPermissions(user);
 
-		const cached = await CacheModule.get(cacheKey);
+		await CacheModule.set(cacheKey, permissions, 360);
 
-		if (cached) return cached;
+		return permissions;
+	}
+
+	protected async _getPermissions(user: HydratedDocument<UserSchema> | null) {
+		const jobs = this._getAllJobs();
+		const events = this._getAllEvents();
+
+		const jobNames = Object.keys(jobs);
+		const eventNames = Object.keys(events);
+
+		const permissions: GetPermissionsResult = {};
+
+		await forEachIn(jobNames, async jobName => {
+			const job = jobs[jobName];
+			const hasPermission = await job.hasPermission(user);
+			if (hasPermission) {
+				permissions[jobName] = true;
+			}
+		});
 
-		const roles: UserRole[] = [user.role];
+		await forEachIn(eventNames, async eventName => {
+			const event = events[eventName];
+			const hasPermission = await event.hasPermission(user);
+			if (hasPermission) {
+				permissions[eventName] = true;
+			}
+		});
+
+		return permissions;
+	}
 
-		let rolePermissions: Record<string, boolean> = {};
-		roles.forEach(role => {
-			if (permissions[role])
-				rolePermissions = { ...rolePermissions, ...permissions[role] };
+	protected _getAllJobs(): Record<string, typeof Job> {
+		const modules = Object.entries(ModuleManager.getModules() ?? {});
+		let jobs: (string | typeof Job)[][] = [];
+		modules.forEach(([moduleName, module]) => {
+			const moduleJobs = Object.entries(module.getJobs()).map(
+				([jobName, job]) => [`${moduleName}.${jobName}`, job]
+			);
+			jobs = [...jobs, ...moduleJobs];
 		});
 
-		await CacheModule.set(cacheKey, rolePermissions, 360);
+		return Object.fromEntries(jobs);
+	}
+
+	protected _getAllEvents(): Record<string, typeof Event> {
+		const modules = Object.entries(ModuleManager.getModules() ?? {});
+		let events: (string | typeof Event)[][] = [];
+		modules.forEach(([moduleName, module]) => {
+			const moduleEvents = Object.entries(module.getEvents()).map(
+				([eventName, event]) => [
+					`event.${moduleName}.${eventName}`,
+					event
+				]
+			);
+			events = [...events, ...moduleEvents];
+		});
 
-		return rolePermissions;
+		return Object.fromEntries(events);
 	}
 }

+ 223 - 223
backend/src/modules/DataModule/models/users/permissions.ts

@@ -1,223 +1,223 @@
-import config from "config";
-import { UserRole } from "@/modules/DataModule/models/users/UserRole";
-
-const guest = {};
-
-const user = { ...guest };
-
-const dj = { ...user };
-
-const owner = { ...dj };
-
-const moderator = {
-	...owner,
-
-	// DataModule importJobs model
-	"data.importJobs.create": true,
-	"data.importJobs.findById.*": true,
-	"data.importJobs.getData": true,
-	"data.importJobs.deleteById.*": true,
-	"data.importJobs.updateById.*": true,
-
-	// DataModule news model
-	"data.news.create": true,
-	"data.news.getData": true,
-	"data.news.updateById.*": true,
-	"data.news.findManyById.*": true,
-
-	// DataModule playlists model
-	"data.playlists.addSongById.*": true,
-	"data.playlists.create.admin": true,
-	"data.playlists.findById.*": true,
-	"data.playlists.getData": true,
-	"data.playlists.removeSongById.*": true,
-	"data.playlists.repositionSongById.*": true,
-	"data.playlists.updateDisplayNameById.*": true,
-	"data.playlists.updatePrivacyById.*": true,
-
-	// DataModule punishments model
-	"data.punishments.banIP": true,
-	"data.punishments.findById.*": true,
-	"data.punishments.getData": true,
-
-	// DataModule reports model
-	"data.reports.findById.*": true,
-	"data.reports.getData": true,
-	"data.reports.updateById.*": true,
-
-	// DataModule songs model
-	"data.songs.create": true,
-	"data.songs.findById.*": true,
-	"data.songs.getData": true,
-	"data.songs.updateById.*": true,
-	"data.songs.verifyById.*": true,
-
-	// DataModule stations model
-	"data.stations.create.official": true,
-	"data.stations.findById.*": true,
-	"data.stations.getData": true,
-	"data.stations.index.adminFilter": true,
-	"data.stations.updateById.*": true,
-	"data.stations.findManyById.*": true,
-
-	// DataModule users model
-	"data.users.banById.*": true,
-	"data.users.findById.*": true,
-	"data.users.getData": true,
-	"data.users.requestPasswordResetById.*": !!config.get("mail.enabled"),
-	"data.users.resendVerifyEmailById.*": !!config.get("mail.enabled"),
-	"data.users.updateById.*": true,
-	// "data.users.findManyById.*": true,
-
-	// DataModule youtubeVideos model
-	"data.youtubeVideos.getData": true,
-	"data.youtubeVideos.requestSet": true,
-
-	// DiscogsModule
-	"discogs.search": !!config.get("apis.discogs.enabled"),
-
-	// Frontend admin views
-	"admin.view": true,
-	"admin.view.import": true,
-	"admin.view.news": true,
-	"admin.view.playlists": true,
-	"admin.view.punishments": true,
-	"admin.view.reports": true,
-	"admin.view.songs": true,
-	"admin.view.stations": true,
-	"admin.view.users": true,
-	"admin.view.youtubeVideos": true
-
-	// // Experimental SoundCloud
-	// ...(config.get("experimental.soundcloud")
-	// 	? {
-	// 			"admin.view.soundcloudTracks": true,
-	// 			"admin.view.soundcloud": true,
-	// 			"soundcloud.getArtist": true
-	// 	  }
-	// 	: {}),
-
-	// // Experimental Spotify
-	// ...(config.get("experimental.spotify")
-	// 	? {
-	// 			"admin.view.spotify": true,
-	// 			"spotify.getTracksFromMediaSources": true,
-	// 			"spotify.getAlbumsFromIds": true,
-	// 			"spotify.getArtistsFromIds": true,
-	// 			"spotify.getAlternativeArtistSourcesForArtists": true,
-	// 			"spotify.getAlternativeAlbumSourcesForAlbums": true,
-	// 			"spotify.getAlternativeMediaSourcesForTracks": true,
-	// 			"admin.view.youtubeChannels": true,
-	// 			"youtube.getChannel": true
-	// 	  }
-	// 	: {})
-};
-
-const admin = {
-	...moderator,
-
-	// DataModule dataRequests model
-	"data.dataRequests.findById.*": true,
-	"data.dataRequests.getData": true,
-	"data.dataRequests.resolveById.*": true,
-
-	// DataModule importJobs model
-	"data.importJobs.deleteById.*": true,
-
-	// DataModule news model
-	"data.news.deleteById.*": true,
-	"data.news.deleteManyById.*": true,
-
-	// DataModule playlists model
-	"data.playlists.clearAndRefillById.*": true,
-	"data.playlists.clearAndRefillAll": true,
-	"data.playlists.createMissing": true,
-	"data.playlists.deleteOrphaned": true,
-	"data.playlists.deleteById.*": true,
-	"data.playlists.requestOrphanedPlaylistSongs": true,
-
-	// DataModule punishments model
-	"data.punishments.deactivateById.*": true,
-
-	// DataModule ratings model
-	"data.ratings.recalculateAll": true,
-
-	// DataModule reports model
-	"data.reports.deleteById.*": true,
-
-	// DataModule songs model
-	"data.songs.deleteById.*": true,
-	"data.songs.updateAll": true,
-
-	// DataModule stations model
-	"data.stations.clearEveryStationQueue": true,
-	"data.stations.deleteById.*": true,
-	"data.stations.deleteManyById.*": true,
-
-	// DataModule users model
-	"data.users.deleteById.*": true,
-	"data.users.deleteSessionsById.*": true,
-	"data.users.updateById.*": true,
-	"data.users.deleteManyById.*": true,
-
-	// DataModule youtubeApiRequests model
-	"data.youtubeApiRequests.findById.*": true,
-	"data.youtubeApiRequests.getData": true,
-	"data.youtubeApiRequests.deleteAll": true,
-	"data.youtubeApiRequests.deleteById.*": true,
-
-	// DataModule youtubeVideos model
-	"data.youtubeVideos.getMissing": true,
-	"data.youtubeVideos.deleteById.*": true,
-	"data.youtubeVideos.migrateV1ToV2.*": true,
-
-	// Frontend admin views
-	"admin.view.dataRequests": true,
-	"admin.view.statistics": true,
-	"admin.view.youtube": true,
-
-	// // Experimental SoundCloud
-	// ...(config.get("experimental.soundcloud")
-	// 	? {
-	// 			"soundcloud.fetchNewApiKey": true,
-	// 			"soundcloud.testApiKey": true
-	// 	  }
-	// 	: {}),
-
-	// // Experimental Spotify
-	// ...(config.get("experimental.spotify")
-	// 	? {
-	// 			"youtube.getMissingChannels": true
-	// 	  }
-	// 	: {})
-
-	"event.data.minifiedUsers.created": true,
-	"event.data.minifiedUsers.updated.*": true,
-	"event.data.minifiedUsers.deleted.*": true,
-
-	"event.data.news.created": true,
-	"event.data.news.updated.*": true,
-	"event.data.news.deleted.*": true,
-
-	"event.data.stations.created": true,
-	"event.data.stations.updated.*": true,
-	"event.data.stations.deleted.*": true,
-
-	"event.data.users.created": true,
-	"event.data.users.updated.*": true,
-	"event.data.users.deleted.*": true
-};
-
-const permissions: Record<
-	UserRole | "owner" | "dj" | "guest",
-	Record<string, boolean>
-> = {
-	guest,
-	user,
-	dj,
-	owner,
-	moderator,
-	admin
-};
-
-export default permissions;
+// import config from "config";
+// import { UserRole } from "@/modules/DataModule/models/users/UserRole";
+
+// const guest = {};
+
+// const user = { ...guest };
+
+// const dj = { ...user };
+
+// const owner = { ...dj };
+
+// const moderator = {
+// 	...owner,
+
+// 	// DataModule importJobs model
+// 	"data.importJobs.create": true,
+// 	"data.importJobs.findById.*": true,
+// 	"data.importJobs.getData": true,
+// 	"data.importJobs.deleteById.*": true,
+// 	"data.importJobs.updateById.*": true,
+
+// 	// DataModule news model
+// 	"data.news.create": true,
+// 	"data.news.getData": true,
+// 	"data.news.updateById.*": true,
+// 	"data.news.findManyById.*": true,
+
+// 	// DataModule playlists model
+// 	"data.playlists.addSongById.*": true,
+// 	"data.playlists.create.admin": true,
+// 	"data.playlists.findById.*": true,
+// 	"data.playlists.getData": true,
+// 	"data.playlists.removeSongById.*": true,
+// 	"data.playlists.repositionSongById.*": true,
+// 	"data.playlists.updateDisplayNameById.*": true,
+// 	"data.playlists.updatePrivacyById.*": true,
+
+// 	// DataModule punishments model
+// 	"data.punishments.banIP": true,
+// 	"data.punishments.findById.*": true,
+// 	"data.punishments.getData": true,
+
+// 	// DataModule reports model
+// 	"data.reports.findById.*": true,
+// 	"data.reports.getData": true,
+// 	"data.reports.updateById.*": true,
+
+// 	// DataModule songs model
+// 	"data.songs.create": true,
+// 	"data.songs.findById.*": true,
+// 	"data.songs.getData": true,
+// 	"data.songs.updateById.*": true,
+// 	"data.songs.verifyById.*": true,
+
+// 	// DataModule stations model
+// 	"data.stations.create.official": true,
+// 	"data.stations.findById.*": true,
+// 	"data.stations.getData": true,
+// 	"data.stations.index.adminFilter": true,
+// 	"data.stations.updateById.*": true,
+// 	"data.stations.findManyById.*": true,
+
+// 	// DataModule users model
+// 	"data.users.banById.*": true,
+// 	"data.users.findById.*": true,
+// 	"data.users.getData": true,
+// 	"data.users.requestPasswordResetById.*": !!config.get("mail.enabled"),
+// 	"data.users.resendVerifyEmailById.*": !!config.get("mail.enabled"),
+// 	"data.users.updateById.*": true,
+// 	// "data.users.findManyById.*": true,
+
+// 	// DataModule youtubeVideos model
+// 	"data.youtubeVideos.getData": true,
+// 	"data.youtubeVideos.requestSet": true,
+
+// 	// DiscogsModule
+// 	"discogs.search": !!config.get("apis.discogs.enabled"),
+
+// 	// Frontend admin views
+// 	"admin.view": true,
+// 	"admin.view.import": true,
+// 	"admin.view.news": true,
+// 	"admin.view.playlists": true,
+// 	"admin.view.punishments": true,
+// 	"admin.view.reports": true,
+// 	"admin.view.songs": true,
+// 	"admin.view.stations": true,
+// 	"admin.view.users": true,
+// 	"admin.view.youtubeVideos": true
+
+// 	// // Experimental SoundCloud
+// 	// ...(config.get("experimental.soundcloud")
+// 	// 	? {
+// 	// 			"admin.view.soundcloudTracks": true,
+// 	// 			"admin.view.soundcloud": true,
+// 	// 			"soundcloud.getArtist": true
+// 	// 	  }
+// 	// 	: {}),
+
+// 	// // Experimental Spotify
+// 	// ...(config.get("experimental.spotify")
+// 	// 	? {
+// 	// 			"admin.view.spotify": true,
+// 	// 			"spotify.getTracksFromMediaSources": true,
+// 	// 			"spotify.getAlbumsFromIds": true,
+// 	// 			"spotify.getArtistsFromIds": true,
+// 	// 			"spotify.getAlternativeArtistSourcesForArtists": true,
+// 	// 			"spotify.getAlternativeAlbumSourcesForAlbums": true,
+// 	// 			"spotify.getAlternativeMediaSourcesForTracks": true,
+// 	// 			"admin.view.youtubeChannels": true,
+// 	// 			"youtube.getChannel": true
+// 	// 	  }
+// 	// 	: {})
+// };
+
+// const admin = {
+// 	...moderator,
+
+// 	// DataModule dataRequests model
+// 	"data.dataRequests.findById.*": true,
+// 	"data.dataRequests.getData": true,
+// 	"data.dataRequests.resolveById.*": true,
+
+// 	// DataModule importJobs model
+// 	"data.importJobs.deleteById.*": true,
+
+// 	// DataModule news model
+// 	"data.news.deleteById.*": true,
+// 	"data.news.deleteManyById.*": true,
+
+// 	// DataModule playlists model
+// 	"data.playlists.clearAndRefillById.*": true,
+// 	"data.playlists.clearAndRefillAll": true,
+// 	"data.playlists.createMissing": true,
+// 	"data.playlists.deleteOrphaned": true,
+// 	"data.playlists.deleteById.*": true,
+// 	"data.playlists.requestOrphanedPlaylistSongs": true,
+
+// 	// DataModule punishments model
+// 	"data.punishments.deactivateById.*": true,
+
+// 	// DataModule ratings model
+// 	"data.ratings.recalculateAll": true,
+
+// 	// DataModule reports model
+// 	"data.reports.deleteById.*": true,
+
+// 	// DataModule songs model
+// 	"data.songs.deleteById.*": true,
+// 	"data.songs.updateAll": true,
+
+// 	// DataModule stations model
+// 	"data.stations.clearEveryStationQueue": true,
+// 	"data.stations.deleteById.*": true,
+// 	"data.stations.deleteManyById.*": true,
+
+// 	// DataModule users model
+// 	"data.users.deleteById.*": true,
+// 	"data.users.deleteSessionsById.*": true,
+// 	"data.users.updateById.*": true,
+// 	"data.users.deleteManyById.*": true,
+
+// 	// DataModule youtubeApiRequests model
+// 	"data.youtubeApiRequests.findById.*": true,
+// 	"data.youtubeApiRequests.getData": true,
+// 	"data.youtubeApiRequests.deleteAll": true,
+// 	"data.youtubeApiRequests.deleteById.*": true,
+
+// 	// DataModule youtubeVideos model
+// 	"data.youtubeVideos.getMissing": true,
+// 	"data.youtubeVideos.deleteById.*": true,
+// 	"data.youtubeVideos.migrateV1ToV2.*": true,
+
+// 	// Frontend admin views
+// 	"admin.view.dataRequests": true,
+// 	"admin.view.statistics": true,
+// 	"admin.view.youtube": true,
+
+// 	// // Experimental SoundCloud
+// 	// ...(config.get("experimental.soundcloud")
+// 	// 	? {
+// 	// 			"soundcloud.fetchNewApiKey": true,
+// 	// 			"soundcloud.testApiKey": true
+// 	// 	  }
+// 	// 	: {}),
+
+// 	// // Experimental Spotify
+// 	// ...(config.get("experimental.spotify")
+// 	// 	? {
+// 	// 			"youtube.getMissingChannels": true
+// 	// 	  }
+// 	// 	: {})
+
+// 	"event.data.minifiedUsers.created": true,
+// 	"event.data.minifiedUsers.updated.*": true,
+// 	"event.data.minifiedUsers.deleted.*": true,
+
+// 	"event.data.news.created": true,
+// 	"event.data.news.updated.*": true,
+// 	"event.data.news.deleted.*": true,
+
+// 	"event.data.stations.created": true,
+// 	"event.data.stations.updated.*": true,
+// 	"event.data.stations.deleted.*": true,
+
+// 	"event.data.users.created": true,
+// 	"event.data.users.updated.*": true,
+// 	"event.data.users.deleted.*": true
+// };
+
+// const permissions: Record<
+// 	UserRole | "owner" | "dj" | "guest",
+// 	Record<string, boolean>
+// > = {
+// 	guest,
+// 	user,
+// 	dj,
+// 	owner,
+// 	moderator,
+// 	admin
+// };
+
+// export default permissions;

+ 6 - 0
backend/src/modules/DataModule/permissions/isAdmin.ts

@@ -0,0 +1,6 @@
+import { HydratedDocument } from "mongoose";
+import { UserSchema } from "../models/users/schema";
+import { UserRole } from "../models/users/UserRole";
+
+export default (user: HydratedDocument<UserSchema>) =>
+	user && user.role === UserRole.ADMIN;

+ 2 - 5
backend/src/modules/DataModule/permissions/isLoggedIn.ts

@@ -1,7 +1,4 @@
-import { HydratedDocument, Schema } from "mongoose";
+import { HydratedDocument } from "mongoose";
 import { UserSchema } from "../models/users/schema";
 
-export default <ModelSchemaType extends Schema>(
-	model: HydratedDocument<ModelSchemaType>,
-	user?: HydratedDocument<UserSchema>
-) => !!user;
+export default (user: HydratedDocument<UserSchema>) => user;

+ 6 - 0
backend/src/modules/DataModule/permissions/isModerator.ts

@@ -0,0 +1,6 @@
+import { HydratedDocument } from "mongoose";
+import { UserSchema } from "../models/users/schema";
+import { UserRole } from "../models/users/UserRole";
+
+export default (user: HydratedDocument<UserSchema>) =>
+	user && user.role === UserRole.ADMIN;

+ 0 - 0
backend/src/modules/DataModule/permissions/doesModelExist.ts → backend/src/modules/DataModule/permissions/modelPermissions/doesModelExist.ts


+ 2 - 2
backend/src/modules/DataModule/permissions/isDj.ts → backend/src/modules/DataModule/permissions/modelPermissions/isDj.ts

@@ -1,6 +1,6 @@
 import { HydratedDocument } from "mongoose";
-import { StationSchema } from "../models/stations/schema";
-import { UserSchema } from "../models/users/schema";
+import { StationSchema } from "../../models/stations/schema";
+import { UserSchema } from "../../models/users/schema";
 
 export default (
 	model: HydratedDocument<StationSchema>,

+ 7 - 0
backend/src/modules/DataModule/permissions/modelPermissions/isLoggedIn.ts

@@ -0,0 +1,7 @@
+import { HydratedDocument, Schema } from "mongoose";
+import { UserSchema } from "../../models/users/schema";
+
+export default <ModelSchemaType extends Schema>(
+	model: HydratedDocument<ModelSchemaType>,
+	user?: HydratedDocument<UserSchema>
+) => !!user;

+ 2 - 2
backend/src/modules/DataModule/permissions/isNewsPublished.ts → backend/src/modules/DataModule/permissions/modelPermissions/isNewsPublished.ts

@@ -1,6 +1,6 @@
 import { HydratedDocument } from "mongoose";
-import { NewsStatus } from "../models/news/NewsStatus";
-import { NewsSchema } from "../models/news/schema";
+import { NewsStatus } from "../../models/news/NewsStatus";
+import { NewsSchema } from "../../models/news/schema";
 
 /**
  * Simply used to check if a news model exists and is published

+ 1 - 1
backend/src/modules/DataModule/permissions/isOwner.ts → backend/src/modules/DataModule/permissions/modelPermissions/isOwner.ts

@@ -1,5 +1,5 @@
 import { HydratedDocument } from "mongoose";
-import { UserSchema } from "../models/users/schema";
+import { UserSchema } from "../../models/users/schema";
 
 export default (
 	model:

+ 0 - 0
backend/src/modules/DataModule/permissions/isPrivate.ts → backend/src/modules/DataModule/permissions/modelPermissions/isPrivate.ts


+ 1 - 1
backend/src/modules/DataModule/permissions/isPublic.ts → backend/src/modules/DataModule/permissions/modelPermissions/isPublic.ts

@@ -1,6 +1,6 @@
 import { HydratedDocument } from "mongoose";
 import { StationPrivacy } from "@/modules/DataModule/models/stations/StationPrivacy";
-import { StationSchema } from "../models/stations/schema";
+import { StationSchema } from "../../models/stations/schema";
 
 export default (model: HydratedDocument<StationSchema>) =>
 	model && model?.privacy === StationPrivacy.PUBLIC;

+ 0 - 0
backend/src/modules/DataModule/permissions/isUnlisted.ts → backend/src/modules/DataModule/permissions/modelPermissions/isUnlisted.ts


+ 27 - 0
backend/src/modules/EventsModule/Event.ts

@@ -1,3 +1,6 @@
+import { HydratedDocument } from "mongoose";
+import { UserSchema } from "../DataModule/models/users/schema";
+
 export default abstract class Event {
 	protected static _namespace: string;
 
@@ -94,6 +97,30 @@ export default abstract class Event {
 	public makeMessage() {
 		return (this.constructor as typeof Event).makeMessage(this._data);
 	}
+
+	protected static _hasPermission:
+		| boolean
+		| CallableFunction
+		| (boolean | CallableFunction)[] = false;
+
+	// Check if a given user has generic permission to subscribe to an event, using _hasPermission
+	public static async hasPermission(
+		user: HydratedDocument<UserSchema> | null
+	) {
+		const options = Array.isArray(this._hasPermission)
+			? this._hasPermission
+			: [this._hasPermission];
+
+		return options.reduce(async (previous, option) => {
+			if (await previous) return true;
+
+			if (typeof option === "boolean") return option;
+
+			if (typeof option === "function") return option(user);
+
+			return false;
+		}, Promise.resolve(false));
+	}
 }
 
 export type EventClass = {

+ 27 - 3
backend/src/modules/EventsModule/jobs/Subscribe.spec.ts

@@ -52,6 +52,11 @@ describe("Subscribe job", async function () {
 				"subscribeSocket"
 			);
 
+			getPermissionsExecute = sinon.stub(
+				GetPermissions.prototype,
+				"execute"
+			);
+
 			modelFindByIdStub;
 
 			Model;
@@ -67,6 +72,7 @@ describe("Subscribe job", async function () {
 				this.dataModuleGetModelStub.restore();
 				this.eventsModuleGetEventStub.restore();
 				this.eventsModuleSubscribeSocketStub.restore();
+				this.getPermissionsExecute.restore();
 			}
 
 			constructor(Model: mongoose.Model<any>, modelFindByIdStub: any) {
@@ -174,7 +180,10 @@ describe("Subscribe job", async function () {
 				channel
 			});
 			// @ts-ignore
-			th.jobContextGetUserStub.resolves(userAdmin); // Or we set _authorize to return true?
+			th.jobContextGetUserStub.resolves(userAdmin);
+			th.getPermissionsExecute.resolves({
+				"event.data.news.created": true
+			});
 			th.jobContextGetSocketIdStub.returns(undefined);
 
 			await job
@@ -189,7 +198,9 @@ describe("Subscribe job", async function () {
 			});
 			// @ts-ignore
 			th.jobContextGetUserStub.resolves(userAdmin);
-
+			th.getPermissionsExecute.resolves({
+				"event.data.news.created": true
+			});
 			th.jobContextGetSocketIdStub.returns("SomeSocketId");
 
 			await job.execute().should.eventually.be.undefined;
@@ -206,6 +217,7 @@ describe("Subscribe job", async function () {
 				});
 				// @ts-ignore
 				th.jobContextGetUserStub.resolves(userNormal);
+				th.getPermissionsExecute.resolves({});
 
 				await job
 					.execute()
@@ -222,7 +234,9 @@ describe("Subscribe job", async function () {
 				});
 				// @ts-ignore
 				th.jobContextGetUserStub.resolves(userAdmin);
-
+				th.getPermissionsExecute.resolves({
+					"event.data.news.created": true
+				});
 				th.jobContextGetSocketIdStub.returns("SomeSocketId");
 
 				await job.execute().should.eventually.be.undefined;
@@ -243,6 +257,9 @@ describe("Subscribe job", async function () {
 				});
 				// @ts-ignore
 				th.jobContextGetUserStub.resolves(userAdmin);
+				th.getPermissionsExecute.resolves({
+					"event.data.news.updated": true
+				});
 
 				createDocument(modelId, NewsStatus.DRAFT);
 
@@ -259,6 +276,7 @@ describe("Subscribe job", async function () {
 				});
 				// @ts-ignore
 				th.jobContextGetUserStub.resolves(userNormal);
+				th.getPermissionsExecute.resolves({});
 
 				createDocument(modelId, NewsStatus.PUBLISHED);
 
@@ -275,6 +293,7 @@ describe("Subscribe job", async function () {
 				});
 				// @ts-ignore
 				th.jobContextGetUserStub.resolves(userNormal);
+				th.getPermissionsExecute.resolves({});
 
 				createDocument(modelId, NewsStatus.DRAFT);
 
@@ -302,6 +321,9 @@ describe("Subscribe job", async function () {
 				});
 				// @ts-ignore
 				th.jobContextGetUserStub.resolves(userAdmin);
+				th.getPermissionsExecute.resolves({
+					"event.data.news.deleted": true
+				});
 
 				createDocument(modelId, NewsStatus.DRAFT);
 
@@ -318,6 +340,7 @@ describe("Subscribe job", async function () {
 				});
 				// @ts-ignore
 				th.jobContextGetUserStub.resolves(userNormal);
+				th.getPermissionsExecute.resolves({});
 
 				createDocument(modelId, NewsStatus.PUBLISHED);
 
@@ -334,6 +357,7 @@ describe("Subscribe job", async function () {
 				});
 				// @ts-ignore
 				th.jobContextGetUserStub.resolves(userNormal);
+				th.getPermissionsExecute.resolves({});
 
 				createDocument(modelId, NewsStatus.DRAFT);
 

+ 2 - 2
backend/src/modules/EventsModule/jobs/Unsubscribe.ts

@@ -6,6 +6,8 @@ export default class Unsubscribe extends Job {
 		super(EventsModule, payload, options);
 	}
 
+	protected static _hasPermission = true;
+
 	protected override async _validate() {
 		if (typeof this._payload !== "object" || this._payload === null)
 			throw new Error("Payload must be an object");
@@ -14,8 +16,6 @@ export default class Unsubscribe extends Job {
 			throw new Error("Channel must be a string");
 	}
 
-	protected override async _authorize() {}
-
 	protected async _execute() {
 		const socketId = this._context.getSocketId();
 

+ 1 - 1
backend/src/modules/EventsModule/jobs/UnsubscribeAll.ts

@@ -6,7 +6,7 @@ export default class UnsubscribeAll extends Job {
 		super(EventsModule, payload, options);
 	}
 
-	protected override async _authorize() {}
+	protected static _hasPermission = true;
 
 	protected async _execute() {
 		const socketId = this._context.getSocketId();

+ 2 - 2
backend/src/modules/EventsModule/jobs/UnsubscribeMany.ts

@@ -6,6 +6,8 @@ export default class UnsubscribeMany extends Job {
 		super(EventsModule, payload, options);
 	}
 
+	protected static _hasPermission = true;
+
 	protected override async _validate() {
 		if (typeof this._payload !== "object" || this._payload === null)
 			throw new Error("Payload must be an object");
@@ -19,8 +21,6 @@ export default class UnsubscribeMany extends Job {
 		});
 	}
 
-	protected override async _authorize() {}
-
 	protected async _execute() {
 		const socketId = this._context.getSocketId();