diff --git a/src/command/rolePermissionCommand.ts b/src/command/rolePermissionCommand.ts new file mode 100644 index 0000000..e86dfbd --- /dev/null +++ b/src/command/rolePermissionCommand.ts @@ -0,0 +1,10 @@ +import { PermissionString } from "../type/permissionTypes"; + +export interface CreateRolePermissionCommand { + permission: PermissionString; + roleId: number; +} + +export interface DeleteRolePermissionCommand { + id: number; +} diff --git a/src/command/rolePermissionCommandHandler.ts b/src/command/rolePermissionCommandHandler.ts new file mode 100644 index 0000000..e5726dd --- /dev/null +++ b/src/command/rolePermissionCommandHandler.ts @@ -0,0 +1,48 @@ +import { dataSource } from "../data-source"; +import { rolePermission } from "../entity/role_permission"; +import InternalException from "../exceptions/internalException"; +import RoleService from "../service/roleService"; +import { CreateRolePermissionCommand, DeleteRolePermissionCommand } from "./rolePermissionCommand"; + +export default abstract class UserPermissionCommandHandler { + /** + * @description grant permission to user + * @param CreateRolePermissionCommand + * @returns {Promise} + */ + static async create(createPermission: CreateRolePermissionCommand): Promise { + return await dataSource + .createQueryBuilder() + .insert() + .into(rolePermission) + .values({ + permission: createPermission.permission, + role: await RoleService.getById(createPermission.roleId), + }) + .execute() + .then((result) => { + return result.identifiers[0].id; + }) + .catch((err) => { + throw new InternalException("Failed saving role permission"); + }); + } + + /** + * @description remove permission from role + * @param DeleteRolePermissionCommand + * @returns {Promise} + */ + static async deleteByToken(deletePermission: DeleteRolePermissionCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(rolePermission) + .where("permission.id = :id", { id: deletePermission.id }) + .execute() + .then((res) => {}) + .catch((err) => { + throw new InternalException("failed role permission removal"); + }); + } +} diff --git a/src/command/permissionCommand.ts b/src/command/userPermissionCommand.ts similarity index 58% rename from src/command/permissionCommand.ts rename to src/command/userPermissionCommand.ts index abc7e30..8bd6e9d 100644 --- a/src/command/permissionCommand.ts +++ b/src/command/userPermissionCommand.ts @@ -1,10 +1,10 @@ import { PermissionString } from "../type/permissionTypes"; -export interface CreatePermissionCommand { +export interface CreateUserPermissionCommand { permission: PermissionString; userId: number; } -export interface DeletePermissionCommand { +export interface DeleteUserPermissionCommand { id: number; } diff --git a/src/command/permissionCommandHandler.ts b/src/command/userPermissionCommandHandler.ts similarity index 56% rename from src/command/permissionCommandHandler.ts rename to src/command/userPermissionCommandHandler.ts index 7e9b5a0..76d944e 100644 --- a/src/command/permissionCommandHandler.ts +++ b/src/command/userPermissionCommandHandler.ts @@ -1,20 +1,20 @@ import { dataSource } from "../data-source"; -import { permission } from "../entity/permission"; +import { userPermission } from "../entity/user_permission"; import InternalException from "../exceptions/internalException"; import UserService from "../service/userService"; -import { CreatePermissionCommand, DeletePermissionCommand } from "./permissionCommand"; +import { CreateUserPermissionCommand, DeleteUserPermissionCommand } from "./userPermissionCommand"; -export default abstract class PermissionCommandHandler { +export default abstract class UserPermissionCommandHandler { /** * @description grant permission to user - * @param CreatePermissionCommand + * @param CreateUserPermissionCommand * @returns {Promise} */ - static async create(createPermission: CreatePermissionCommand): Promise { + static async create(createPermission: CreateUserPermissionCommand): Promise { return await dataSource .createQueryBuilder() .insert() - .into(permission) + .into(userPermission) .values({ permission: createPermission.permission, user: await UserService.getById(createPermission.userId), @@ -24,25 +24,25 @@ export default abstract class PermissionCommandHandler { return result.identifiers[0].id; }) .catch((err) => { - throw new InternalException("Failed saving permission"); + throw new InternalException("Failed saving user permission"); }); } /** * @description remove permission to user - * @param DeletePermissionCommand + * @param DeleteUserPermissionCommand * @returns {Promise} */ - static async deleteByToken(deletePermission: DeletePermissionCommand): Promise { + static async deleteByToken(deletePermission: DeleteUserPermissionCommand): Promise { return await dataSource .createQueryBuilder() .delete() - .from(permission) + .from(userPermission) .where("permission.id = :id", { id: deletePermission.id }) .execute() .then((res) => {}) .catch((err) => { - throw new InternalException("failed permission removal"); + throw new InternalException("failed user permission removal"); }); } } diff --git a/src/controller/authController.ts b/src/controller/authController.ts index f6d15b9..a8a4045 100644 --- a/src/controller/authController.ts +++ b/src/controller/authController.ts @@ -8,8 +8,9 @@ import UserService from "../service/userService"; import speakeasy from "speakeasy"; import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; import RefreshService from "../service/refreshService"; -import PermissionService from "../service/permissionService"; +import UserPermissionService from "../service/userPermissionService"; import PermissionHelper from "../helpers/permissionHelper"; +import RolePermissionService from "../service/rolePermissionService"; /** * @description Check authentication status by token @@ -34,9 +35,12 @@ export async function login(req: Request, res: Response): Promise { throw new UnauthorizedRequestException("Token not valid or expired"); } - let permissions = await PermissionService.getByUser(id); - let permissionStrings = permissions.map((e) => e.permission); - let permissionObject = PermissionHelper.convertToObject(permissionStrings); + let userPermissions = await UserPermissionService.getByUser(id); + let userPermissionStrings = userPermissions.map((e) => e.permission); + let userRoles = await UserService.getAssignedRolesByUserId(id); + let rolePermissions = await RolePermissionService.getByRoles(userRoles.map((e) => e.id)); + let rolePermissionStrings = rolePermissions.map((e) => e.permission); + let permissionObject = PermissionHelper.convertToObject([...userPermissionStrings, ...rolePermissionStrings]); let jwtData: JWTToken = { userId: id, @@ -103,7 +107,7 @@ export async function refresh(req: Request, res: Response): Promise { let { id, username, mail, firstname, lastname } = await UserService.getById(tokenUserId); - let permissions = await PermissionService.getByUser(id); + let permissions = await UserPermissionService.getByUser(id); let permissionStrings = permissions.map((e) => e.permission); let permissionObject = PermissionHelper.convertToObject(permissionStrings); diff --git a/src/controller/inviteController.ts b/src/controller/inviteController.ts index 23c1993..0a301ca 100644 --- a/src/controller/inviteController.ts +++ b/src/controller/inviteController.ts @@ -16,8 +16,8 @@ import InviteService from "../service/inviteService"; import UserService from "../service/userService"; import CustomRequestException from "../exceptions/customRequestException"; import { CLUB_NAME } from "../env.defaults"; -import { CreatePermissionCommand } from "../command/permissionCommand"; -import PermissionCommandHandler from "../command/permissionCommandHandler"; +import { CreateUserPermissionCommand } from "../command/userPermissionCommand"; +import UserPermissionCommandHandler from "../command/userPermissionCommandHandler"; /** * @description start first user @@ -130,11 +130,11 @@ export async function finishInvite(req: Request, res: Response, grantAdmin: bool let id = await UserCommandHandler.create(createUser); if (grantAdmin) { - let createPermission: CreatePermissionCommand = { + let createPermission: CreateUserPermissionCommand = { permission: "*", userId: id, }; - await PermissionCommandHandler.create(createPermission); + await UserPermissionCommandHandler.create(createPermission); } let jwtData: JWTToken = { diff --git a/src/controller/permissionController.ts b/src/controller/permissionController.ts deleted file mode 100644 index 08a141c..0000000 --- a/src/controller/permissionController.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Request, Response } from "express"; -import { permissionModules, permissionSections, permissionTypes } from "../type/permissionTypes"; - -/** - * @description sections of permissions - * @param req {Request} Express req object - * @param res {Response} Express res object - * @returns {Promise<*>} - */ -export async function getSections(req: Request, res: Response): Promise { - res.json(permissionSections); -} diff --git a/src/data-source.ts b/src/data-source.ts index 8cab226..15aaf25 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -6,12 +6,15 @@ import { DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME, DB_TYPE } from "./env.defau import { user } from "./entity/user"; import { refresh } from "./entity/refresh"; import { invite } from "./entity/invite"; -import { permission } from "./entity/permission"; +import { userPermission } from "./entity/user_permission"; import { Initial1724317398939 } from "./migrations/1724317398939-initial"; import { RefreshPrimaryChange1724573307851 } from "./migrations/1724573307851-refreshPrimaryChange"; import { Invite1724579024939 } from "./migrations/1724579024939-invite"; import { Permissions1724661484664 } from "./migrations/1724661484664-permissions"; +import { role } from "./entity/role"; +import { rolePermission } from "./entity/role_permission"; +import { RolePermission1724771491085 } from "./migrations/1724771491085-role_permission"; const dataSource = new DataSource({ type: DB_TYPE as any, @@ -23,8 +26,14 @@ const dataSource = new DataSource({ synchronize: false, logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"], bigNumberStrings: false, - entities: [user, refresh, invite, permission], - migrations: [Initial1724317398939, RefreshPrimaryChange1724573307851, Invite1724579024939, Permissions1724661484664], + entities: [user, refresh, invite, userPermission, role, rolePermission], + migrations: [ + Initial1724317398939, + RefreshPrimaryChange1724573307851, + Invite1724579024939, + Permissions1724661484664, + RolePermission1724771491085, + ], migrationsRun: true, migrationsTransactionMode: "each", subscribers: [], diff --git a/src/entity/role.ts b/src/entity/role.ts new file mode 100644 index 0000000..964860b --- /dev/null +++ b/src/entity/role.ts @@ -0,0 +1,14 @@ +import { Column, Entity, ManyToMany, PrimaryColumn } from "typeorm"; +import { user } from "./user"; + +@Entity() +export class role { + @PrimaryColumn({ generated: "increment", type: "int" }) + id: number; + + @Column({ type: "varchar", length: 255 }) + role: string; + + @ManyToMany(() => user, (user) => user.roles) + users: user[]; +} diff --git a/src/entity/role_permission.ts b/src/entity/role_permission.ts new file mode 100644 index 0000000..a723ccc --- /dev/null +++ b/src/entity/role_permission.ts @@ -0,0 +1,15 @@ +import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; +import { PermissionString } from "../type/permissionTypes"; +import { role } from "./role"; + +@Entity() +export class rolePermission { + @PrimaryColumn({ type: "int" }) + roleId: number; + + @PrimaryColumn({ type: "varchar", length: 255 }) + permission: PermissionString; + + @ManyToOne(() => role) + role: role; +} diff --git a/src/entity/user.ts b/src/entity/user.ts index 5541468..3d4c05c 100644 --- a/src/entity/user.ts +++ b/src/entity/user.ts @@ -1,5 +1,5 @@ -import { Column, Entity, PrimaryColumn } from "typeorm"; -import { refresh } from "./refresh"; +import { Column, Entity, JoinTable, ManyToMany, PrimaryColumn } from "typeorm"; +import { role } from "./role"; @Entity() export class user { @@ -20,4 +20,10 @@ export class user { @Column({ type: "varchar", length: 255 }) secret: string; + + @ManyToMany(() => role, (role) => role.users) + @JoinTable({ + name: "user_roles", + }) + roles: role[]; } diff --git a/src/entity/permission.ts b/src/entity/user_permission.ts similarity index 92% rename from src/entity/permission.ts rename to src/entity/user_permission.ts index 4b642e5..01cb8a6 100644 --- a/src/entity/permission.ts +++ b/src/entity/user_permission.ts @@ -3,7 +3,7 @@ import { user } from "./user"; import { PermissionObject, PermissionString } from "../type/permissionTypes"; @Entity() -export class permission { +export class userPermission { @PrimaryColumn({ type: "int" }) userId: number; diff --git a/src/helpers/permissionHelper.ts b/src/helpers/permissionHelper.ts index 9b7f6a3..2730cbe 100644 --- a/src/helpers/permissionHelper.ts +++ b/src/helpers/permissionHelper.ts @@ -32,6 +32,22 @@ export default class PermissionHelper { return false; } + static canSection( + permissions: PermissionObject, + type: PermissionType | "admin", + section: PermissionSection + ): boolean { + if (type == "admin") return permissions.admin ?? false; + if (permissions.admin) return true; + if ( + permissions[section]?.all == "*" || + permissions[section]?.all?.includes(type) || + permissions[section] != undefined + ) + return true; + return false; + } + static passCheckMiddleware( requiredPermissions: PermissionType | "admin", section: PermissionSection, @@ -52,6 +68,25 @@ export default class PermissionHelper { }; } + static sectionPassCheckMiddleware( + requiredPermissions: PermissionType | "admin", + section: PermissionSection + ): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + const permissions = req.permissions; + + if (this.canSection(permissions, requiredPermissions, section)) { + next(); + } else { + throw new ForbiddenRequestException( + `missing permission for ${section}.${module}.${ + Array.isArray(requiredPermissions) ? requiredPermissions.join("|") : requiredPermissions + }` + ); + } + }; + } + static convertToObject(permissions: Array): PermissionObject { if (permissions.includes("*")) { return { diff --git a/src/migrations/1724771491085-role_permission.ts b/src/migrations/1724771491085-role_permission.ts new file mode 100644 index 0000000..e658462 --- /dev/null +++ b/src/migrations/1724771491085-role_permission.ts @@ -0,0 +1,123 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm"; + +export class RolePermission1724771491085 implements MigrationInterface { + name = "RolePermission1724771491085"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "role", + columns: [ + { + name: "id", + type: "int", + isPrimary: true, + isNullable: false, + isGenerated: true, + generationStrategy: "increment", + }, + { + name: "role", + type: "varchar", + length: "255", + isNullable: false, + }, + ], + }), + true + ); + + await queryRunner.createTable( + new Table({ + name: "role_permission", + columns: [ + { + name: "permission", + type: "varchar", + length: "255", + isPrimary: true, + isNullable: false, + }, + { + name: "roleId", + type: "int", + isPrimary: true, + isNullable: false, + }, + ], + }), + true + ); + + await queryRunner.renameTable("permission", "user_permission"); + + await queryRunner.createTable( + new Table({ + name: "user_roles", + columns: [ + { + name: "userId", + type: "int", + isPrimary: true, + isNullable: false, + }, + { + name: "roleId", + type: "int", + isPrimary: true, + isNullable: false, + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + "role_permission", + new TableForeignKey({ + columnNames: ["roleId"], + referencedColumnNames: ["id"], + referencedTableName: "role", + onDelete: "No Action", + }) + ); + + await queryRunner.createForeignKey( + "user_roles", + new TableForeignKey({ + columnNames: ["userId"], + referencedColumnNames: ["id"], + referencedTableName: "user", + onDelete: "No Action", + }) + ); + + await queryRunner.createForeignKey( + "user_roles", + new TableForeignKey({ + columnNames: ["roleId"], + referencedColumnNames: ["id"], + referencedTableName: "role", + onDelete: "No Action", + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + const user_roles = await queryRunner.getTable("user_roles"); + const roles_foreignKey = user_roles.foreignKeys.find((fk) => fk.columnNames.indexOf("roleId") !== -1); + const user_foreignKey = user_roles.foreignKeys.find((fk) => fk.columnNames.indexOf("userId") !== -1); + await queryRunner.dropForeignKey("user_roles", roles_foreignKey); + await queryRunner.dropForeignKey("user_roles", user_foreignKey); + await queryRunner.dropTable("user_roles"); + + const role_permission = await queryRunner.getTable("role_permission"); + const permission_foreignKey = role_permission.foreignKeys.find((fk) => fk.columnNames.indexOf("roleId") !== -1); + await queryRunner.dropForeignKey("role_permission", permission_foreignKey); + await queryRunner.dropTable("role_permission"); + + await queryRunner.dropTable("role"); + + await queryRunner.renameTable("user_permission", "permission"); + } +} diff --git a/src/routes/index.ts b/src/routes/index.ts index f5e7226..77129c9 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -8,7 +8,6 @@ import errorHandler from "../middleware/errorHandler"; import setup from "./setup"; import auth from "./auth"; -import permission from "./permission"; import PermissionHelper from "../helpers/permissionHelper"; export default (app: Express) => { @@ -25,6 +24,6 @@ export default (app: Express) => { app.use("/setup", allowSetup, setup); app.use("/auth", auth); app.use(authenticate); - app.use("/permission", PermissionHelper.passCheckMiddleware("admin", "user"), permission); + app.use("/secured", PermissionHelper.passCheckMiddleware("admin", "user"), (req, res) => {}); app.use(errorHandler); }; diff --git a/src/routes/permission.ts b/src/routes/permission.ts deleted file mode 100644 index cb085e9..0000000 --- a/src/routes/permission.ts +++ /dev/null @@ -1,10 +0,0 @@ -import express from "express"; -import { getSections } from "../controller/permissionController"; - -var router = express.Router({ mergeParams: true }); - -router.get("/sections", async (req, res) => { - await getSections(req, res); -}); - -export default router; diff --git a/src/service/rolePermissionService.ts b/src/service/rolePermissionService.ts new file mode 100644 index 0000000..e345765 --- /dev/null +++ b/src/service/rolePermissionService.ts @@ -0,0 +1,44 @@ +import { dataSource } from "../data-source"; +import { rolePermission } from "../entity/role_permission"; +import { userPermission } from "../entity/user_permission"; +import InternalException from "../exceptions/internalException"; + +export default abstract class RolePermissionService { + /** + * @description get permission by role + * @param roleId number + * @returns {Promise>} + */ + static async getByRole(roleId: number): Promise> { + return await dataSource + .getRepository(rolePermission) + .createQueryBuilder("permission") + .where("permission.roleId = :roleId", { roleId: roleId }) + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("permissions not found by role"); + }); + } + + /** + * @description get permission by roles + * @param roleIds Array + * @returns {Promise>} + */ + static async getByRoles(roleIds: Array): Promise> { + return await dataSource + .getRepository(rolePermission) + .createQueryBuilder("permission") + .where("permission.roleId IN (:...roleIds)", { roleIds: roleIds }) + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("permissions not found by roles"); + }); + } +} diff --git a/src/service/roleService.ts b/src/service/roleService.ts new file mode 100644 index 0000000..c952802 --- /dev/null +++ b/src/service/roleService.ts @@ -0,0 +1,24 @@ +import { dataSource } from "../data-source"; +import { role } from "../entity/role"; +import InternalException from "../exceptions/internalException"; + +export default abstract class RoleService { + /** + * @description get role by id + * @param id number + * @returns {Promise} + */ + static async getById(id: number): Promise { + return await dataSource + .getRepository(role) + .createQueryBuilder("role") + .where("role.id = :id", { id: id }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("role not found by id"); + }); + } +} diff --git a/src/service/permissionService.ts b/src/service/userPermissionService.ts similarity index 61% rename from src/service/permissionService.ts rename to src/service/userPermissionService.ts index ad5a50a..1c0f778 100644 --- a/src/service/permissionService.ts +++ b/src/service/userPermissionService.ts @@ -1,16 +1,16 @@ import { dataSource } from "../data-source"; -import { permission } from "../entity/permission"; +import { userPermission } from "../entity/user_permission"; import InternalException from "../exceptions/internalException"; -export default abstract class PermissionService { +export default abstract class UserPermissionService { /** * @description get permission by user - * @param user number - * @returns {Promise>} + * @param userId number + * @returns {Promise>} */ - static async getByUser(userId: number): Promise> { + static async getByUser(userId: number): Promise> { return await dataSource - .getRepository(permission) + .getRepository(userPermission) .createQueryBuilder("permission") .where("permission.userId = :userId", { userId: userId }) .getMany() diff --git a/src/service/userService.ts b/src/service/userService.ts index 82060c2..095d1ea 100644 --- a/src/service/userService.ts +++ b/src/service/userService.ts @@ -1,4 +1,5 @@ import { dataSource } from "../data-source"; +import { role } from "../entity/role"; import { user } from "../entity/user"; import InternalException from "../exceptions/internalException"; @@ -81,4 +82,24 @@ export default abstract class UserService { throw new InternalException("could not count users"); }); } + + /** + * @description get roles assigned to user + * @param userId number + * @returns {Promise>} + */ + static async getAssignedRolesByUserId(userId: number): Promise> { + return await dataSource + .getRepository(user) + .createQueryBuilder("user") + .leftJoinAndSelect("user.roles", "roles") + .where("user.id = :id", { id: userId }) + .getOneOrFail() + .then((res) => { + return res.roles; + }) + .catch((err) => { + throw new InternalException("could not get roles for user"); + }); + } } diff --git a/src/type/permissionTypes.ts b/src/type/permissionTypes.ts index f042390..a73000b 100644 --- a/src/type/permissionTypes.ts +++ b/src/type/permissionTypes.ts @@ -12,7 +12,7 @@ export type PermissionModule = | "user" | "role"; -export type PermissionType = "create" | "read" | "update" | "delete"; +export type PermissionType = "read" | "create" | "update" | "delete"; export type PermissionString = | `${PermissionSection}.${PermissionModule}.${PermissionType}` // für spezifische Berechtigungen