roles and permissions

This commit is contained in:
Julian Krauser 2024-08-27 17:54:59 +02:00
parent d77c3ca1a5
commit 9808100d81
21 changed files with 389 additions and 59 deletions

View file

@ -0,0 +1,10 @@
import { PermissionString } from "../type/permissionTypes";
export interface CreateRolePermissionCommand {
permission: PermissionString;
roleId: number;
}
export interface DeleteRolePermissionCommand {
id: number;
}

View file

@ -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<number>}
*/
static async create(createPermission: CreateRolePermissionCommand): Promise<number> {
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<any>}
*/
static async deleteByToken(deletePermission: DeleteRolePermissionCommand): Promise<any> {
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");
});
}
}

View file

@ -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;
}

View file

@ -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<number>}
*/
static async create(createPermission: CreatePermissionCommand): Promise<number> {
static async create(createPermission: CreateUserPermissionCommand): Promise<number> {
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<any>}
*/
static async deleteByToken(deletePermission: DeletePermissionCommand): Promise<any> {
static async deleteByToken(deletePermission: DeleteUserPermissionCommand): Promise<any> {
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");
});
}
}

View file

@ -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<any> {
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<any> {
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);

View file

@ -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 = {

View file

@ -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<any> {
res.json(permissionSections);
}

View file

@ -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: [],

14
src/entity/role.ts Normal file
View file

@ -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[];
}

View file

@ -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;
}

View file

@ -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[];
}

View file

@ -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;

View file

@ -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<PermissionString>): PermissionObject {
if (permissions.includes("*")) {
return {

View file

@ -0,0 +1,123 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";
export class RolePermission1724771491085 implements MigrationInterface {
name = "RolePermission1724771491085";
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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");
}
}

View file

@ -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);
};

View file

@ -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;

View file

@ -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<Array<rolePermission>>}
*/
static async getByRole(roleId: number): Promise<Array<rolePermission>> {
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<number>
* @returns {Promise<Array<rolePermission>>}
*/
static async getByRoles(roleIds: Array<number>): Promise<Array<rolePermission>> {
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");
});
}
}

View file

@ -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<role>}
*/
static async getById(id: number): Promise<role> {
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");
});
}
}

View file

@ -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<Array<permission>>}
* @param userId number
* @returns {Promise<Array<userPermission>>}
*/
static async getByUser(userId: number): Promise<Array<permission>> {
static async getByUser(userId: number): Promise<Array<userPermission>> {
return await dataSource
.getRepository(permission)
.getRepository(userPermission)
.createQueryBuilder("permission")
.where("permission.userId = :userId", { userId: userId })
.getMany()

View file

@ -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<Array<role>>}
*/
static async getAssignedRolesByUserId(userId: number): Promise<Array<role>> {
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");
});
}
}

View file

@ -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