permission system - permission formatting

This commit is contained in:
Julian Krauser 2024-08-26 13:47:08 +02:00
parent d889f92643
commit 2f5d9d3f01
15 changed files with 352 additions and 18 deletions

View file

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

View file

@ -0,0 +1,48 @@
import { dataSource } from "../data-source";
import { permission } from "../entity/permission";
import InternalException from "../exceptions/internalException";
import UserService from "../service/userService";
import { CreatePermissionCommand, DeletePermissionCommand } from "./permissionCommand";
export default abstract class PermissionCommandHandler {
/**
* @description grant permission to user
* @param CreatePermissionCommand
* @returns {Promise<number>}
*/
static async create(createPermission: CreatePermissionCommand): Promise<number> {
return await dataSource
.createQueryBuilder()
.insert()
.into(permission)
.values({
permission: createPermission.permission,
user: await UserService.getById(createPermission.userId),
})
.execute()
.then((result) => {
return result.identifiers[0].id;
})
.catch((err) => {
throw new InternalException("Failed saving permission");
});
}
/**
* @description remove permission to user
* @param DeletePermissionCommand
* @returns {Promise<any>}
*/
static async deleteByToken(deletePermission: DeletePermissionCommand): Promise<any> {
return await dataSource
.createQueryBuilder()
.delete()
.from(permission)
.where("permission.id = :id", { id: deletePermission.id })
.execute()
.then((res) => {})
.catch((err) => {
throw new InternalException("failed permission removal");
});
}
}

View file

@ -1,17 +1,15 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { JWTHelper } from "../helpers/jwtHelper"; import { JWTHelper } from "../helpers/jwtHelper";
import { JWTData, JWTToken } from "../type/jwtTypes"; import { JWTToken } from "../type/jwtTypes";
import InternalException from "../exceptions/internalException"; import InternalException from "../exceptions/internalException";
import RefreshCommandHandler from "../command/refreshCommandHandler"; import RefreshCommandHandler from "../command/refreshCommandHandler";
import { CreateRefreshCommand, DeleteRefreshCommand } from "../command/refreshCommand"; import { CreateRefreshCommand, DeleteRefreshCommand } from "../command/refreshCommand";
import UserService from "../service/userService"; import UserService from "../service/userService";
import speakeasy from "speakeasy"; import speakeasy from "speakeasy";
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
import QRCode from "qrcode";
import { CreateUserCommand } from "../command/userCommand";
import UserCommandHandler from "../command/userCommandHandler";
import RefreshService from "../service/refreshService"; import RefreshService from "../service/refreshService";
import BadRequestException from "../exceptions/badRequestException"; import PermissionService from "../service/permissionService";
import PermissionHelper from "../helpers/permissionHelper";
/** /**
* @description Check authentication status by token * @description Check authentication status by token
@ -23,7 +21,7 @@ export async function login(req: Request, res: Response): Promise<any> {
let username = req.body.username; let username = req.body.username;
let totp = req.body.totp; let totp = req.body.totp;
let { id, secret } = await UserService.getByUsername(username); let { id, secret, mail, firstname, lastname } = await UserService.getByUsername(username);
let valid = speakeasy.totp.verify({ let valid = speakeasy.totp.verify({
secret: secret, secret: secret,
@ -36,10 +34,17 @@ export async function login(req: Request, res: Response): Promise<any> {
throw new UnauthorizedRequestException("Token not valid or expired"); 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 jwtData: JWTToken = { let jwtData: JWTToken = {
userId: id, userId: id,
mail: mail,
username: username, username: username,
rights: [], firstname: firstname,
lastname: lastname,
permissions: permissionObject,
}; };
let accessToken: string; let accessToken: string;
@ -96,12 +101,19 @@ export async function refresh(req: Request, res: Response): Promise<any> {
throw new UnauthorizedRequestException("user not identified with token and refresh"); throw new UnauthorizedRequestException("user not identified with token and refresh");
} }
let { id, username } = await UserService.getById(tokenUserId); let { id, username, mail, firstname, lastname } = await UserService.getById(tokenUserId);
let permissions = await PermissionService.getByUser(id);
let permissionStrings = permissions.map((e) => e.permission);
let permissionObject = PermissionHelper.convertToObject(permissionStrings);
let jwtData: JWTToken = { let jwtData: JWTToken = {
userId: id, userId: id,
mail: mail,
username: username, username: username,
rights: [], firstname: firstname,
lastname: lastname,
permissions: permissionObject,
}; };
let accessToken: string; let accessToken: string;

View file

@ -16,6 +16,8 @@ import InviteService from "../service/inviteService";
import UserService from "../service/userService"; import UserService from "../service/userService";
import CustomRequestException from "../exceptions/customRequestException"; import CustomRequestException from "../exceptions/customRequestException";
import { CLUB_NAME } from "../env.defaults"; import { CLUB_NAME } from "../env.defaults";
import { CreatePermissionCommand } from "../command/permissionCommand";
import PermissionCommandHandler from "../command/permissionCommandHandler";
/** /**
* @description start first user * @description start first user
@ -98,7 +100,7 @@ export async function verifyInvite(req: Request, res: Response): Promise<any> {
* @param res {Response} Express res object * @param res {Response} Express res object
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
export async function finishInvite(req: Request, res: Response): Promise<any> { export async function finishInvite(req: Request, res: Response, grantAdmin: boolean = false): Promise<any> {
let mail = req.body.mail; let mail = req.body.mail;
let token = req.body.token; let token = req.body.token;
let totp = req.body.totp; let totp = req.body.totp;
@ -127,10 +129,23 @@ export async function finishInvite(req: Request, res: Response): Promise<any> {
}; };
let id = await UserCommandHandler.create(createUser); let id = await UserCommandHandler.create(createUser);
if (grantAdmin) {
let createPermission: CreatePermissionCommand = {
permission: "*",
userId: id,
};
await PermissionCommandHandler.create(createPermission);
}
let jwtData: JWTToken = { let jwtData: JWTToken = {
userId: id, userId: id,
mail: mail,
username: username, username: username,
rights: [], firstname: firstname,
lastname: lastname,
permissions: {
...(grantAdmin ? { admin: true } : {}),
},
}; };
let accessToken: string; let accessToken: string;

View file

@ -1,15 +1,17 @@
import "dotenv/config"; import "dotenv/config";
import "reflect-metadata"; import "reflect-metadata";
import { DataSource } from "typeorm"; import { DataSource } from "typeorm";
import { DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME } from "./env.defaults";
import { user } from "./entity/user"; import { user } from "./entity/user";
import { refresh } from "./entity/refresh"; import { refresh } from "./entity/refresh";
import { invite } from "./entity/invite"; import { invite } from "./entity/invite";
import { permission } from "./entity/permission";
import { Initial1724317398939 } from "./migrations/1724317398939-initial"; import { Initial1724317398939 } from "./migrations/1724317398939-initial";
import { RefreshPrimaryChange1724573307851 } from "./migrations/1724573307851-refreshPrimaryChange"; import { RefreshPrimaryChange1724573307851 } from "./migrations/1724573307851-refreshPrimaryChange";
import { Invite1724579024939 } from "./migrations/1724579024939-invite"; import { Invite1724579024939 } from "./migrations/1724579024939-invite";
import { DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME } from "./env.defaults"; import { Permissions1724661484664 } from "./migrations/1724661484664-permissions";
const dataSource = new DataSource({ const dataSource = new DataSource({
type: "mysql", type: "mysql",
@ -21,8 +23,8 @@ const dataSource = new DataSource({
synchronize: false, synchronize: false,
logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"], logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"],
bigNumberStrings: false, bigNumberStrings: false,
entities: [user, refresh, invite], entities: [user, refresh, invite, permission],
migrations: [Initial1724317398939, RefreshPrimaryChange1724573307851, Invite1724579024939], migrations: [Initial1724317398939, RefreshPrimaryChange1724573307851, Invite1724579024939, Permissions1724661484664],
migrationsRun: true, migrationsRun: true,
migrationsTransactionMode: "each", migrationsTransactionMode: "each",
subscribers: [], subscribers: [],

15
src/entity/permission.ts Normal file
View file

@ -0,0 +1,15 @@
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
import { user } from "./user";
import { PermissionObject, PermissionString } from "../type/permissionTypes";
@Entity()
export class permission {
@PrimaryColumn({ type: "int" })
userId: number;
@PrimaryColumn({ type: "varchar", length: 255 })
permission: PermissionString;
@ManyToOne(() => user)
user: user;
}

View file

@ -0,0 +1,7 @@
import CustomRequestException from "./customRequestException";
export default class ForbiddenRequestException extends CustomRequestException {
constructor(msg: string) {
super(403, msg);
}
}

View file

@ -0,0 +1,124 @@
import { Request, Response } from "express";
import {
PermissionModule,
permissionModules,
PermissionObject,
PermissionSection,
PermissionString,
PermissionType,
permissionTypes,
} from "../type/permissionTypes";
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
export default class PermissionHelper {
static passCheckMiddleware(
section: PermissionSection,
module: PermissionModule,
requiredPermissions: Array<PermissionType> | "*"
): (req: Request, res: Response, next: Function) => void {
return (req: Request, res: Response, next: Function) => {
const permissions = req.rights;
if (permissions.admin) {
next();
} else if (permissions?.[section]?.all) {
next();
} else if (permissions?.[section]?.all) {
next();
} else if (permissions?.[section]?.[module] == "*") {
next();
} else if (
(permissions?.[section]?.[module] as Array<PermissionType>).some((e: PermissionType) =>
requiredPermissions.includes(e)
)
) {
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 {
admin: true,
};
}
let output: PermissionObject = {};
let splitPermissions = permissions.map((e) => e.split(".")) as Array<
[PermissionSection, PermissionModule | PermissionType | "*", PermissionType | "*"]
>;
for (let split of splitPermissions) {
if (!output[split[0]]) {
output[split[0]] = {};
}
if (split[1] == "*" || output[split[0]].all == "*") {
output[split[0]] = { all: "*" };
} else if (permissionTypes.includes(split[1] as PermissionType)) {
if (!output[split[0]].all || !Array.isArray(output[split[0]].all)) {
output[split[0]].all = [];
}
const permissionIndex = permissionTypes.indexOf(split[1] as PermissionType);
const appliedPermissions = permissionTypes.slice(0, permissionIndex + 1);
output[split[0]].all = appliedPermissions;
} else {
if (split[2] == "*" || output[split[0]][split[1] as PermissionModule] == "*") {
output[split[0]][split[1] as PermissionModule] = "*";
} else {
if (
!output[split[0]][split[1] as PermissionModule] ||
!Array.isArray(output[split[0]][split[1] as PermissionModule])
) {
output[split[0]][split[1] as PermissionModule] = [];
}
const permissionIndex = permissionTypes.indexOf(split[2] as PermissionType);
const appliedPermissions = permissionTypes.slice(0, permissionIndex + 1);
output[split[0]][split[1] as PermissionModule] = appliedPermissions;
}
}
}
return output;
}
static convertToStringArray(permissions: PermissionObject): Array<PermissionString> {
if (permissions.admin) {
return ["*"];
}
let output: Array<PermissionString> = [];
let sections = Object.keys(permissions) as Array<PermissionSection>;
for (let section of sections) {
if (permissions[section].all) {
let types = permissions[section].all;
if (types == "*") {
output.push(`${section}.*`);
} else {
for (let type of types) {
output.push(`${section}.${type}`);
}
}
} else {
let modules = Object.keys(permissions[section]) as Array<PermissionModule>;
for (let module of modules) {
let types = permissions[section][module];
if (types == "*") {
output.push(`${section}.${module}.*`);
} else {
for (let type of types) {
output.push(`${section}.${module}.${type}`);
}
}
}
}
}
return output;
}
static getWhatToAdd() {}
static getWhatToRemove() {}
}

View file

@ -9,7 +9,7 @@ declare global {
export interface Request { export interface Request {
userId: string; userId: string;
username: string; username: string;
rights: Array<string>; rights: PermissionObject;
} }
} }
} }
@ -20,6 +20,7 @@ dataSource.initialize();
const app = express(); const app = express();
import router from "./routes/index"; import router from "./routes/index";
import { PermissionObject } from "./type/permissionTypes";
router(app); router(app);
app.listen(SERVER_PORT, () => { app.listen(SERVER_PORT, () => {
console.log(`listening on *:${SERVER_PORT}`); console.log(`listening on *:${SERVER_PORT}`);

View file

@ -0,0 +1,46 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";
export class Permissions1724661484664 implements MigrationInterface {
name = "Permissions1724661484664";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: "permission",
columns: [
{
name: "permission",
type: "varchar",
length: "255",
isPrimary: true,
isNullable: false,
},
{
name: "userId",
type: "int",
isPrimary: true,
isNullable: false,
},
],
}),
true
);
await queryRunner.createForeignKey(
"permission",
new TableForeignKey({
columnNames: ["userId"],
referencedColumnNames: ["id"],
referencedTableName: "user",
onDelete: "No Action",
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable("permission");
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf("userId") !== -1);
await queryRunner.dropForeignKey("permission", foreignKey);
await queryRunner.dropTable("permission");
}
}

View file

@ -22,7 +22,7 @@ router.post(
); );
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => { router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
await finishInvite(req, res); await finishInvite(req, res, true);
}); });
export default router; export default router;

View file

@ -0,0 +1,24 @@
import { dataSource } from "../data-source";
import { permission } from "../entity/permission";
import InternalException from "../exceptions/internalException";
export default abstract class PermissionService {
/**
* @description get permission by user
* @param user number
* @returns {Promise<Array<permission>>}
*/
static async getByUser(userId: number): Promise<Array<permission>> {
return await dataSource
.getRepository(permission)
.createQueryBuilder("permission")
.where("permission.userId = :userId", { userId: userId })
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("permission not found by user");
});
}
}

View file

@ -1,11 +1,16 @@
import { PermissionObject } from "./permissionTypes";
export type JWTData = { export type JWTData = {
[key: string]: string | number | Array<string>; [key: string]: string | number | PermissionObject;
}; };
export type JWTToken = { export type JWTToken = {
userId: number; userId: number;
mail: string;
username: string; username: string;
rights: Array<string>; firstname: string;
lastname: string;
permissions: PermissionObject;
} & JWTData; } & JWTData;
export type JWTRefresh = { export type JWTRefresh = {

View file

@ -0,0 +1,24 @@
export type PermissionSection = "club" | "settings" | "user";
export type PermissionModule = "protocoll" | "user";
export type PermissionType = "read" | "create" | "update" | "delete";
export type PermissionString =
| `${PermissionSection}.${PermissionModule}.${PermissionType}` // für spezifische Berechtigungen
| `${PermissionSection}.${PermissionModule}.*` // für alle Berechtigungen in einem Modul
| `${PermissionSection}.${PermissionType}` // für spezifische Berechtigungen in einem Abschnitt
| `${PermissionSection}.*` // für alle Berechtigungen in einem Abschnitt
| "*"; // für Admin
export type PermissionObject = {
[section in PermissionSection]?: {
[module in PermissionModule]?: Array<PermissionType> | "*";
} & { all?: Array<PermissionType> | "*" };
} & {
admin?: boolean;
};
export const permissionSections: Array<PermissionSection> = ["club", "settings", "user"];
export const permissionModules: Array<PermissionModule> = ["protocoll", "user"];
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];

View file

@ -0,0 +1 @@
export interface PermissionViewModel {}