From 72ad6ec8a648b7d75df908f676a3fad67c47997a Mon Sep 17 00:00:00 2001 From: Methapon Metanipat Date: Sat, 7 Sep 2024 20:19:34 +0700 Subject: [PATCH] fix: user permission control --- src/controllers/02-user-controller.ts | 330 ++++++++++++++++++-------- 1 file changed, 233 insertions(+), 97 deletions(-) diff --git a/src/controllers/02-user-controller.ts b/src/controllers/02-user-controller.ts index 5fe6c38..360c2f2 100644 --- a/src/controllers/02-user-controller.ts +++ b/src/controllers/02-user-controller.ts @@ -15,7 +15,7 @@ import { import { Branch, Prisma, Status, User, UserType } from "@prisma/client"; import prisma from "../db"; -import minio, { presignedGetObjectIfExist } from "../services/minio"; +import minio from "../services/minio"; import { RequestWithUser } from "../interfaces/user"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; @@ -137,6 +137,63 @@ type UserUpdate = { branchId?: string | string[]; }; +async function permissionCheck(user: RequestWithUser["user"], userId: string) { + const record = await prisma.user.findFirst({ + include: { + province: true, + district: true, + subDistrict: true, + createdBy: true, + updatedBy: true, + branch: { + include: { + branch: { + include: { + headOffice: { + include: { + branch: { where: { user: { some: { userId: user.sub } } } }, + user: { where: { userId: user.sub } }, + }, + }, + user: { where: { userId: user.sub } }, + }, + }, + }, + }, + }, + where: { id: userId }, + }); + + if (!record) { + throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound"); + } + + if (!isSystem(user)) { + record.branch.forEach(({ branch }) => { + if (!globalAllow(user) && branch.user.length === 0) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } else { + if ( + (branch.user.length === 0 && !branch.headOffice) || + (branch.headOffice && + branch.headOffice.user.length === 0 && + branch.headOffice.branch.length === 0) + ) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } + } + }); + } +} + async function userBranchCodeGen(user: User, branch: Branch) { return await prisma.$transaction( async (tx) => { @@ -192,9 +249,21 @@ export class UserController extends Controller { some: { branch: { OR: [ - { user: { some: { userId: req.user.sub } } }, { - headOffice: !globalAllow(req.user) + user: { some: { userId: req.user.sub } }, + }, + { + branch: globalAllow(req.user) + ? { some: { user: { some: { userId: req.user.sub } } } } + : undefined, + }, + { + headOffice: globalAllow(req.user) + ? { branch: { some: { user: { some: { userId: req.user.sub } } } } } + : undefined, + }, + { + headOffice: globalAllow(req.user) ? { user: { some: { userId: req.user.sub } } } : undefined, }, @@ -256,12 +325,24 @@ export class UserController extends Controller { some: { branch: { OR: [ - { user: { some: { userId: req.user.sub } } }, + { + user: { some: { userId: req.user.sub } }, + }, { branch: globalAllow(req.user) ? { some: { user: { some: { userId: req.user.sub } } } } : undefined, }, + { + headOffice: globalAllow(req.user) + ? { branch: { some: { user: { some: { userId: req.user.sub } } } } } + : undefined, + }, + { + headOffice: globalAllow(req.user) + ? { user: { some: { userId: req.user.sub } } } + : undefined, + }, ], }, }, @@ -325,7 +406,15 @@ export class UserController extends Controller { prisma.district.findFirst({ where: { id: body.districtId ?? undefined } }), prisma.subDistrict.findFirst({ where: { id: body.subDistrictId ?? undefined } }), prisma.branch.findMany({ - include: { user: { where: { userId: req.user.sub } } }, + include: { + headOffice: { + include: { + branch: { where: { user: { some: { userId: req.user.sub } } } }, + user: { where: { userId: req.user.sub } }, + }, + }, + user: { where: { userId: req.user.sub } }, + }, where: { id: { in: Array.isArray(body.branchId) ? body.branchId : [body.branchId] } }, }), prisma.user.findFirst({ @@ -360,6 +449,30 @@ export class UserController extends Controller { "minimumBranchNotMet", ); } + + if (!isSystem(req.user)) { + branch.forEach((v) => { + if (!globalAllow(req.user) && v.user.length === 0) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } else { + if ( + (v.user.length === 0 && !v.headOffice) || + (v.headOffice && v.headOffice.user.length === 0 && v.headOffice.branch.length === 0) + ) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } + } + }); + } + if (user) { throw new HttpError(HttpStatus.BAD_REQUEST, "User exists.", "userExists"); } @@ -470,11 +583,35 @@ export class UserController extends Controller { prisma.district.findFirst({ where: { id: body.districtId || undefined } }), prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), prisma.user.findFirst({ - include: { branch: true }, + include: { + branch: { + include: { + branch: { + include: { + headOffice: { + include: { + branch: { where: { user: { some: { userId: req.user.sub } } } }, + user: { where: { userId: req.user.sub } }, + }, + }, + user: { where: { userId: req.user.sub } }, + }, + }, + }, + }, + }, where: { id: userId }, }), prisma.branch.findMany({ - include: { user: { where: { userId: req.user.sub } } }, + include: { + headOffice: { + include: { + branch: { where: { user: { some: { userId: req.user.sub } } } }, + user: { where: { userId: req.user.sub } }, + }, + }, + user: { where: { userId: req.user.sub } }, + }, where: { id: { in: Array.isArray(body.branchId) ? body.branchId : body.branchId ? [body.branchId] : [], @@ -510,6 +647,48 @@ export class UserController extends Controller { "minimumBranchNotMet", ); } + if (!isSystem(req.user)) { + user.branch.forEach(({ branch: v }) => { + if (!globalAllow(req.user) && v.user.length === 0) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } else { + if ( + (v.user.length === 0 && !v.headOffice) || + (v.headOffice && v.headOffice.user.length === 0 && v.headOffice.branch.length === 0) + ) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } + } + }); + branch.forEach((v) => { + if (!globalAllow(req.user) && v.user.length === 0) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } else { + if ( + (v.user.length === 0 && !v.headOffice) || + (v.headOffice && v.headOffice.user.length === 0 && v.headOffice.branch.length === 0) + ) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } + } + }); + } const setRoleIndex = MANAGE_ROLES.findIndex((v) => v === body.userRole); const userRoleIndex = MANAGE_ROLES.reduce( (a, c, i) => (req.user.roles?.includes(c) ? i : a), @@ -652,24 +831,13 @@ export class UserController extends Controller { include: { branch: { include: { - branch: { + headOffice: { include: { - headOffice: { - include: { - user: { - where: { - userId: req.user.sub, - }, - }, - }, - }, - }, - }, - user: { - where: { - userId: req.user.sub, + branch: { where: { user: { some: { userId: req.user.sub } } } }, + user: { where: { userId: req.user.sub } }, }, }, + user: { where: { userId: req.user.sub } }, }, }, }, @@ -678,29 +846,35 @@ export class UserController extends Controller { where: { id: userId }, }); - if ( - !isSystem(req.user) && - record?.branch.some((v) => { - const allow = v.branch.user.some((u) => u.userId === req.user.sub); - if (!globalAllow(req.user) && !allow) { - return v.branch.branch.some((b) => - b.headOffice?.user.some((u) => u.userId === req.user.sub), - ); - } - return true; - }) - ) { - throw new HttpError( - HttpStatus.FORBIDDEN, - "You do not have permission to perform this action.", - "noPermission", - ); - } - if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound"); } + if (!isSystem(req.user)) { + record.branch.forEach(({ branch }) => { + if (!globalAllow(req.user) && branch.user.length === 0) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } else { + if ( + (branch.user.length === 0 && !branch.headOffice) || + (branch.headOffice && + branch.headOffice.user.length === 0 && + branch.headOffice.branch.length === 0) + ) { + throw new HttpError( + HttpStatus.FORBIDDEN, + "You do not have permission to perform this action.", + "noPermission", + ); + } + } + }); + } + if (record.status !== Status.CREATED) { throw new HttpError(HttpStatus.FORBIDDEN, "User is in used.", "userInUsed"); } @@ -739,42 +913,14 @@ export class UserController extends Controller { @Get("{userId}/image") async getUserImageByUserId(@Request() req: RequestWithUser, @Path() userId: string) { - const url = await presignedGetObjectIfExist(MINIO_BUCKET, imageLocation(userId), 60 * 60); - - if (!url) { - throw new HttpError(HttpStatus.NOT_FOUND, "Image cannot be found", "imageNotFound"); - } - + const url = await minio.presignedGetObject(MINIO_BUCKET, imageLocation(userId), 60 * 60); return req.res?.redirect(url); } @Put("{userId}/image") @Security("keycloak", ["system", "head_of_admin", "admin", "branch_manager"]) async setUserImageByUserId(@Request() req: RequestWithUser, @Path() userId: string) { - const record = await prisma.user.findFirst({ - include: { - branch: { where: { userId: req.user.sub } }, - }, - where: { - id: userId, - }, - }); - - if (!record) { - throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound"); - } - - if ( - !["system", "head_of_admin", "admin"].some((v) => req.user.roles?.includes(v)) && - !record.branch.some((v) => v.userId === req.user.sub) - ) { - throw new HttpError( - HttpStatus.FORBIDDEN, - "You do not have permission to perform this action.", - "noPermission", - ); - } - + await permissionCheck(req.user, userId); return req.res?.redirect( await minio.presignedPutObject(MINIO_BUCKET, imageLocation(userId), 12 * 60 * 60), ); @@ -800,14 +946,6 @@ export class UserProfileController extends Controller { @Get("{name}") async getImage(@Request() req: RequestWithUser, @Path() userId: string, @Path() name: string) { - const record = await prisma.user.findFirst({ - where: { id: userId }, - }); - - if (!record) { - throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound"); - } - return req.res?.redirect( await minio.presignedGetObject( MINIO_BUCKET, @@ -820,14 +958,7 @@ export class UserProfileController extends Controller { @Put("{name}") @Security("keycloak") async putImage(@Request() req: RequestWithUser, @Path() userId: string, @Path() name: string) { - const record = await prisma.user.findFirst({ - where: { id: userId }, - }); - - if (!record) { - throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound"); - } - + await permissionCheck(req.user, userId); return req.res?.redirect( await minio.presignedPutObject( MINIO_BUCKET, @@ -838,7 +969,8 @@ export class UserProfileController extends Controller { } @Delete("{name}") - async deleteImage(@Path() userId: string, @Path() name: string) { + async deleteImage(@Request() req: RequestWithUser, @Path() userId: string, @Path() name: string) { + await permissionCheck(req.user, userId); await minio.removeObject(MINIO_BUCKET, fileLocation.user.profile(userId, name), { forceDelete: true, }); @@ -860,6 +992,7 @@ export class UserAttachmentController extends Controller { } const list = await listFile(fileLocation.user.attachment(userId)); + return await Promise.all( list.map(async (v) => ({ name: v, @@ -873,14 +1006,12 @@ export class UserAttachmentController extends Controller { } @Post() - async addAttachment(@Path() userId: string, @Body() payload: { file: string[] }) { - const record = await prisma.user.findFirst({ - where: { id: userId }, - }); - - if (!record) { - throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "userNotFound"); - } + async addAttachment( + @Request() req: RequestWithUser, + @Path() userId: string, + @Body() payload: { file: string[] }, + ) { + await permissionCheck(req.user, userId); return await Promise.all( payload.file.map(async (v) => ({ @@ -896,7 +1027,12 @@ export class UserAttachmentController extends Controller { } @Delete() - async deleteAttachment(@Path() userId: string, @Body() payload: { file: string[] }) { + async deleteAttachment( + @Request() req: RequestWithUser, + @Path() userId: string, + @Body() payload: { file: string[] }, + ) { + await permissionCheck(req.user, userId); await Promise.all( payload.file.map(async (v) => { await minio.removeObject(MINIO_BUCKET, fileLocation.user.attachment(userId, v), {