From c4ad0171b05f2889bc3ce647a6ed88fd49adffbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Thu, 22 Jun 2023 20:09:17 +0200 Subject: [PATCH] feat: add missing abilities (#354) feat: add all missing abilities rules on resolvers --- server/src/ability/ability.factory.ts | 5 ++ .../comment-thread-target.ability-handler.ts | 50 ++++++++++++++++--- .../comment-thread.ability-handler.ts | 48 +++++++++++++++--- .../handlers/comment.ability-handler.ts | 42 +++++++++++++--- .../handlers/company.ability-handler.ts | 42 +++++++++++++--- .../handlers/person.ability-handler.ts | 42 +++++++++++++--- .../handlers/refresh-token.ability-handler.ts | 48 +++++++++++++++--- .../ability/handlers/user.ability-handler.ts | 42 +++++++++++++--- .../workspace-member.ability-handler.ts | 48 +++++++++++++++--- .../handlers/workspace.ability-handler.ts | 42 +++++++++++++--- .../resolvers/comment-thread.resolver.spec.ts | 5 ++ .../resolvers/comment-thread.resolver.ts | 30 ++++++++--- .../resolvers/comment.resolver.spec.ts | 5 ++ .../comment/resolvers/comment.resolver.ts | 8 +++ .../src/core/company/company.resolver.spec.ts | 5 ++ server/src/core/company/company.resolver.ts | 32 +++++++++--- .../src/core/person/person.resolver.spec.ts | 5 ++ server/src/core/person/person.resolver.ts | 33 +++++++++--- server/src/core/user/user.resolver.spec.ts | 5 ++ server/src/core/user/user.resolver.ts | 13 +++-- server/src/utils/prepare-find-many.ts | 15 ------ 21 files changed, 461 insertions(+), 104 deletions(-) delete mode 100644 server/src/utils/prepare-find-many.ts diff --git a/server/src/ability/ability.factory.ts b/server/src/ability/ability.factory.ts index 07bd9fccf85d..1f92dc4a6da8 100644 --- a/server/src/ability/ability.factory.ts +++ b/server/src/ability/ability.factory.ts @@ -45,6 +45,11 @@ export class AbilityFactory { ); // User + can(AbilityAction.Read, 'User', { + workspaceMember: { + workspaceId: workspace.id, + }, + }); can(AbilityAction.Update, 'User', { id: user.id }); cannot(AbilityAction.Delete, 'User'); diff --git a/server/src/ability/handlers/comment-thread-target.ability-handler.ts b/server/src/ability/handlers/comment-thread-target.ability-handler.ts index 94a55401781f..bfc0df1daa35 100644 --- a/server/src/ability/handlers/comment-thread-target.ability-handler.ts +++ b/server/src/ability/handlers/comment-thread-target.ability-handler.ts @@ -2,14 +2,24 @@ import { PrismaService } from 'src/database/prisma.service'; import { AbilityAction } from '../ability.action'; import { AppAbility } from '../ability.factory'; import { IAbilityHandler } from '../interfaces/ability-handler.interface'; -import { Injectable } from '@nestjs/common'; +import { + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { CommentThreadTargetWhereInput } from 'src/core/@generated/comment-thread-target/comment-thread-target-where.input'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { assert } from 'src/utils/assert'; +import { subject } from '@casl/ability'; + +class CommentThreadTargetArgs { + where?: CommentThreadTargetWhereInput; +} @Injectable() export class ManageCommentThreadTargetAbilityHandler implements IAbilityHandler { - constructor(private readonly prismaService: PrismaService) {} - async handle(ability: AppAbility) { return ability.can(AbilityAction.Manage, 'CommentThreadTarget'); } @@ -35,8 +45,21 @@ export class CreateCommentThreadTargetAbilityHandler export class UpdateCommentThreadTargetAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Update, 'CommentThreadTarget'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const commentThreadTarget = + await this.prismaService.commentThreadTarget.findFirst({ + where: args.where, + }); + assert(commentThreadTarget, '', NotFoundException); + + return ability.can( + AbilityAction.Update, + subject('CommentThreadTarget', commentThreadTarget), + ); } } @@ -44,7 +67,20 @@ export class UpdateCommentThreadTargetAbilityHandler export class DeleteCommentThreadTargetAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Delete, 'CommentThreadTarget'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const commentThreadTarget = + await this.prismaService.commentThreadTarget.findFirst({ + where: args.where, + }); + assert(commentThreadTarget, '', NotFoundException); + + return ability.can( + AbilityAction.Delete, + subject('CommentThreadTarget', commentThreadTarget), + ); } } diff --git a/server/src/ability/handlers/comment-thread.ability-handler.ts b/server/src/ability/handlers/comment-thread.ability-handler.ts index 3c2beb0f1388..ac838db1e58a 100644 --- a/server/src/ability/handlers/comment-thread.ability-handler.ts +++ b/server/src/ability/handlers/comment-thread.ability-handler.ts @@ -2,12 +2,22 @@ import { PrismaService } from 'src/database/prisma.service'; import { AbilityAction } from '../ability.action'; import { AppAbility } from '../ability.factory'; import { IAbilityHandler } from '../interfaces/ability-handler.interface'; -import { Injectable } from '@nestjs/common'; +import { + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { CommentThreadWhereInput } from 'src/core/@generated/comment-thread/comment-thread-where.input'; +import { assert } from 'src/utils/assert'; +import { subject } from '@casl/ability'; + +class CommentThreadArgs { + where?: CommentThreadWhereInput; +} @Injectable() export class ManageCommentThreadAbilityHandler implements IAbilityHandler { - constructor(private readonly prismaService: PrismaService) {} - async handle(ability: AppAbility) { return ability.can(AbilityAction.Manage, 'CommentThread'); } @@ -29,14 +39,38 @@ export class CreateCommentThreadAbilityHandler implements IAbilityHandler { @Injectable() export class UpdateCommentThreadAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Update, 'CommentThread'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const commentThread = await this.prismaService.commentThread.findFirst({ + where: args.where, + }); + assert(commentThread, '', NotFoundException); + + return ability.can( + AbilityAction.Update, + subject('CommentThread', commentThread), + ); } } @Injectable() export class DeleteCommentThreadAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Delete, 'CommentThread'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const commentThread = await this.prismaService.commentThread.findFirst({ + where: args.where, + }); + assert(commentThread, '', NotFoundException); + + return ability.can( + AbilityAction.Delete, + subject('CommentThread', commentThread), + ); } } diff --git a/server/src/ability/handlers/comment.ability-handler.ts b/server/src/ability/handlers/comment.ability-handler.ts index ffe74dfdbb93..d4893d965986 100644 --- a/server/src/ability/handlers/comment.ability-handler.ts +++ b/server/src/ability/handlers/comment.ability-handler.ts @@ -2,12 +2,22 @@ import { PrismaService } from 'src/database/prisma.service'; import { AbilityAction } from '../ability.action'; import { AppAbility } from '../ability.factory'; import { IAbilityHandler } from '../interfaces/ability-handler.interface'; -import { Injectable } from '@nestjs/common'; +import { + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { subject } from '@casl/ability'; +import { CommentWhereInput } from 'src/core/@generated/comment/comment-where.input'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { assert } from 'src/utils/assert'; + +class CommentArgs { + where?: CommentWhereInput; +} @Injectable() export class ManageCommentAbilityHandler implements IAbilityHandler { - constructor(private readonly prismaService: PrismaService) {} - async handle(ability: AppAbility) { return ability.can(AbilityAction.Manage, 'Comment'); } @@ -29,14 +39,32 @@ export class CreateCommentAbilityHandler implements IAbilityHandler { @Injectable() export class UpdateCommentAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Update, 'Comment'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const comment = await this.prismaService.comment.findFirst({ + where: args.where, + }); + assert(comment, '', NotFoundException); + + return ability.can(AbilityAction.Update, subject('Comment', comment)); } } @Injectable() export class DeleteCommentAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Delete, 'Comment'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const comment = await this.prismaService.comment.findFirst({ + where: args.where, + }); + assert(comment, '', NotFoundException); + + return ability.can(AbilityAction.Delete, subject('Comment', comment)); } } diff --git a/server/src/ability/handlers/company.ability-handler.ts b/server/src/ability/handlers/company.ability-handler.ts index 6969f5b9c4d7..eecdff821924 100644 --- a/server/src/ability/handlers/company.ability-handler.ts +++ b/server/src/ability/handlers/company.ability-handler.ts @@ -2,12 +2,22 @@ import { PrismaService } from 'src/database/prisma.service'; import { AbilityAction } from '../ability.action'; import { AppAbility } from '../ability.factory'; import { IAbilityHandler } from '../interfaces/ability-handler.interface'; -import { Injectable } from '@nestjs/common'; +import { + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { CompanyWhereInput } from 'src/core/@generated/company/company-where.input'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { assert } from 'src/utils/assert'; +import { subject } from '@casl/ability'; + +class CompanyArgs { + where?: CompanyWhereInput; +} @Injectable() export class ManageCompanyAbilityHandler implements IAbilityHandler { - constructor(private readonly prismaService: PrismaService) {} - async handle(ability: AppAbility) { return ability.can(AbilityAction.Manage, 'Company'); } @@ -29,14 +39,32 @@ export class CreateCompanyAbilityHandler implements IAbilityHandler { @Injectable() export class UpdateCompanyAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Update, 'Company'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const company = await this.prismaService.company.findFirst({ + where: args.where, + }); + assert(company, '', NotFoundException); + + return ability.can(AbilityAction.Update, subject('Company', company)); } } @Injectable() export class DeleteCompanyAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Delete, 'Company'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const company = await this.prismaService.company.findFirst({ + where: args.where, + }); + assert(company, '', NotFoundException); + + return ability.can(AbilityAction.Delete, subject('Company', company)); } } diff --git a/server/src/ability/handlers/person.ability-handler.ts b/server/src/ability/handlers/person.ability-handler.ts index 12dbcb71c81b..a3c0f1c006be 100644 --- a/server/src/ability/handlers/person.ability-handler.ts +++ b/server/src/ability/handlers/person.ability-handler.ts @@ -2,12 +2,22 @@ import { PrismaService } from 'src/database/prisma.service'; import { AbilityAction } from '../ability.action'; import { AppAbility } from '../ability.factory'; import { IAbilityHandler } from '../interfaces/ability-handler.interface'; -import { Injectable } from '@nestjs/common'; +import { + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { PersonWhereInput } from 'src/core/@generated/person/person-where.input'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { assert } from 'src/utils/assert'; +import { subject } from '@casl/ability'; + +class PersonArgs { + where?: PersonWhereInput; +} @Injectable() export class ManagePersonAbilityHandler implements IAbilityHandler { - constructor(private readonly prismaService: PrismaService) {} - async handle(ability: AppAbility) { return ability.can(AbilityAction.Manage, 'Person'); } @@ -29,14 +39,32 @@ export class CreatePersonAbilityHandler implements IAbilityHandler { @Injectable() export class UpdatePersonAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Update, 'Person'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const person = await this.prismaService.person.findFirst({ + where: args.where, + }); + assert(person, '', NotFoundException); + + return ability.can(AbilityAction.Update, subject('Person', person)); } } @Injectable() export class DeletePersonAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Delete, 'Person'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const person = await this.prismaService.person.findFirst({ + where: args.where, + }); + assert(person, '', NotFoundException); + + return ability.can(AbilityAction.Delete, subject('Person', person)); } } diff --git a/server/src/ability/handlers/refresh-token.ability-handler.ts b/server/src/ability/handlers/refresh-token.ability-handler.ts index e2e35c883ccf..bfe6fe3c8660 100644 --- a/server/src/ability/handlers/refresh-token.ability-handler.ts +++ b/server/src/ability/handlers/refresh-token.ability-handler.ts @@ -2,12 +2,22 @@ import { PrismaService } from 'src/database/prisma.service'; import { AbilityAction } from '../ability.action'; import { AppAbility } from '../ability.factory'; import { IAbilityHandler } from '../interfaces/ability-handler.interface'; -import { Injectable } from '@nestjs/common'; +import { + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { subject } from '@casl/ability'; +import { RefreshTokenWhereInput } from 'src/core/@generated/refresh-token/refresh-token-where.input'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { assert } from 'src/utils/assert'; + +class RefreshTokenArgs { + where?: RefreshTokenWhereInput; +} @Injectable() export class ManageRefreshTokenAbilityHandler implements IAbilityHandler { - constructor(private readonly prismaService: PrismaService) {} - async handle(ability: AppAbility) { return ability.can(AbilityAction.Manage, 'RefreshToken'); } @@ -29,14 +39,38 @@ export class CreateRefreshTokenAbilityHandler implements IAbilityHandler { @Injectable() export class UpdateRefreshTokenAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Update, 'RefreshToken'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const refreshToken = await this.prismaService.refreshToken.findFirst({ + where: args.where, + }); + assert(refreshToken, '', NotFoundException); + + return ability.can( + AbilityAction.Update, + subject('RefreshToken', refreshToken), + ); } } @Injectable() export class DeleteRefreshTokenAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Delete, 'RefreshToken'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const refreshToken = await this.prismaService.refreshToken.findFirst({ + where: args.where, + }); + assert(refreshToken, '', NotFoundException); + + return ability.can( + AbilityAction.Delete, + subject('RefreshToken', refreshToken), + ); } } diff --git a/server/src/ability/handlers/user.ability-handler.ts b/server/src/ability/handlers/user.ability-handler.ts index 371b223a6e79..664f5f5f5dd1 100644 --- a/server/src/ability/handlers/user.ability-handler.ts +++ b/server/src/ability/handlers/user.ability-handler.ts @@ -2,12 +2,22 @@ import { PrismaService } from 'src/database/prisma.service'; import { AbilityAction } from '../ability.action'; import { AppAbility } from '../ability.factory'; import { IAbilityHandler } from '../interfaces/ability-handler.interface'; -import { Injectable } from '@nestjs/common'; +import { + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { assert } from 'src/utils/assert'; +import { UserWhereInput } from 'src/core/@generated/user/user-where.input'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { subject } from '@casl/ability'; + +class UserArgs { + where?: UserWhereInput; +} @Injectable() export class ManageUserAbilityHandler implements IAbilityHandler { - constructor(private readonly prismaService: PrismaService) {} - async handle(ability: AppAbility) { return ability.can(AbilityAction.Manage, 'User'); } @@ -29,14 +39,32 @@ export class CreateUserAbilityHandler implements IAbilityHandler { @Injectable() export class UpdateUserAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Update, 'User'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const user = await this.prismaService.user.findFirst({ + where: args.where, + }); + assert(user, '', NotFoundException); + + return ability.can(AbilityAction.Update, subject('User', user)); } } @Injectable() export class DeleteUserAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Delete, 'User'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const user = await this.prismaService.user.findFirst({ + where: args.where, + }); + assert(user, '', NotFoundException); + + return ability.can(AbilityAction.Delete, subject('User', user)); } } diff --git a/server/src/ability/handlers/workspace-member.ability-handler.ts b/server/src/ability/handlers/workspace-member.ability-handler.ts index d75f10995e21..0d2c6249807d 100644 --- a/server/src/ability/handlers/workspace-member.ability-handler.ts +++ b/server/src/ability/handlers/workspace-member.ability-handler.ts @@ -2,12 +2,22 @@ import { PrismaService } from 'src/database/prisma.service'; import { AbilityAction } from '../ability.action'; import { AppAbility } from '../ability.factory'; import { IAbilityHandler } from '../interfaces/ability-handler.interface'; -import { Injectable } from '@nestjs/common'; +import { + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { subject } from '@casl/ability'; +import { WorkspaceMemberWhereInput } from 'src/core/@generated/workspace-member/workspace-member-where.input'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { assert } from 'src/utils/assert'; + +class WorksapceMemberArgs { + where?: WorkspaceMemberWhereInput; +} @Injectable() export class ManageWorkspaceMemberAbilityHandler implements IAbilityHandler { - constructor(private readonly prismaService: PrismaService) {} - async handle(ability: AppAbility) { return ability.can(AbilityAction.Manage, 'WorkspaceMember'); } @@ -29,14 +39,38 @@ export class CreateWorkspaceMemberAbilityHandler implements IAbilityHandler { @Injectable() export class UpdateWorkspaceMemberAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Update, 'WorkspaceMember'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const workspaceMember = await this.prismaService.workspaceMember.findFirst({ + where: args.where, + }); + assert(workspaceMember, '', NotFoundException); + + return ability.can( + AbilityAction.Update, + subject('WorkspaceMember', workspaceMember), + ); } } @Injectable() export class DeleteWorkspaceMemberAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Delete, 'WorkspaceMember'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const workspaceMember = await this.prismaService.workspaceMember.findFirst({ + where: args.where, + }); + assert(workspaceMember, '', NotFoundException); + + return ability.can( + AbilityAction.Delete, + subject('WorkspaceMember', workspaceMember), + ); } } diff --git a/server/src/ability/handlers/workspace.ability-handler.ts b/server/src/ability/handlers/workspace.ability-handler.ts index 368485c0ce26..4a8b17a8ad07 100644 --- a/server/src/ability/handlers/workspace.ability-handler.ts +++ b/server/src/ability/handlers/workspace.ability-handler.ts @@ -2,12 +2,22 @@ import { PrismaService } from 'src/database/prisma.service'; import { AbilityAction } from '../ability.action'; import { AppAbility } from '../ability.factory'; import { IAbilityHandler } from '../interfaces/ability-handler.interface'; -import { Injectable } from '@nestjs/common'; +import { + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { subject } from '@casl/ability'; +import { WorkspaceWhereInput } from 'src/core/@generated/workspace/workspace-where.input'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { assert } from 'src/utils/assert'; + +class WorksapceArgs { + where?: WorkspaceWhereInput; +} @Injectable() export class ManageWorkspaceAbilityHandler implements IAbilityHandler { - constructor(private readonly prismaService: PrismaService) {} - async handle(ability: AppAbility) { return ability.can(AbilityAction.Manage, 'Workspace'); } @@ -29,14 +39,32 @@ export class CreateWorkspaceAbilityHandler implements IAbilityHandler { @Injectable() export class UpdateWorkspaceAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Update, 'Workspace'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const workspace = await this.prismaService.workspace.findFirst({ + where: args.where, + }); + assert(workspace, '', NotFoundException); + + return ability.can(AbilityAction.Update, subject('Workspace', workspace)); } } @Injectable() export class DeleteWorkspaceAbilityHandler implements IAbilityHandler { - handle(ability: AppAbility) { - return ability.can(AbilityAction.Delete, 'Workspace'); + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const workspace = await this.prismaService.workspace.findFirst({ + where: args.where, + }); + assert(workspace, '', NotFoundException); + + return ability.can(AbilityAction.Delete, subject('Workspace', workspace)); } } diff --git a/server/src/core/comment/resolvers/comment-thread.resolver.spec.ts b/server/src/core/comment/resolvers/comment-thread.resolver.spec.ts index da08a3fd119d..3a90ff3d0900 100644 --- a/server/src/core/comment/resolvers/comment-thread.resolver.spec.ts +++ b/server/src/core/comment/resolvers/comment-thread.resolver.spec.ts @@ -4,6 +4,7 @@ import { CommentThreadService } from '../services/comment-thread.service'; import { CanActivate } from '@nestjs/common'; import { CreateOneCommentGuard } from 'src/guards/create-one-comment.guard'; import { CreateOneCommentThreadGuard } from 'src/guards/create-one-comment-thread.guard'; +import { AbilityFactory } from 'src/ability/ability.factory'; describe('CommentThreadResolver', () => { let resolver: CommentThreadResolver; @@ -18,6 +19,10 @@ describe('CommentThreadResolver', () => { provide: CommentThreadService, useValue: {}, }, + { + provide: AbilityFactory, + useValue: {}, + }, ], }) .overrideGuard(CreateOneCommentGuard) diff --git a/server/src/core/comment/resolvers/comment-thread.resolver.ts b/server/src/core/comment/resolvers/comment-thread.resolver.ts index e204f78ca9c4..764ea34f208b 100644 --- a/server/src/core/comment/resolvers/comment-thread.resolver.ts +++ b/server/src/core/comment/resolvers/comment-thread.resolver.ts @@ -8,13 +8,22 @@ import { CreateOneCommentThreadArgs } from '../../../core/@generated/comment-thr import { CreateOneCommentThreadGuard } from '../../../guards/create-one-comment-thread.guard'; import { FindManyCommentThreadArgs } from '../../../core/@generated/comment-thread/find-many-comment-thread.args'; import { CommentThreadService } from '../services/comment-thread.service'; -import { prepareFindManyArgs } from 'src/utils/prepare-find-many'; import { UpdateOneCommentThreadArgs } from 'src/core/@generated/comment-thread/update-one-comment-thread.args'; import { Prisma } from '@prisma/client'; import { PrismaSelector, PrismaSelect, } from 'src/decorators/prisma-select.decorator'; +import { AbilityGuard } from 'src/guards/ability.guard'; +import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; +import { + CreateCommentThreadAbilityHandler, + ReadCommentThreadAbilityHandler, + UpdateCommentThreadAbilityHandler, +} from 'src/ability/handlers/comment-thread.ability-handler'; +import { UserAbility } from 'src/decorators/user-ability.decorator'; +import { AppAbility } from 'src/ability/ability.factory'; +import { accessibleBy } from '@casl/prisma'; @UseGuards(JwtAuthGuard) @Resolver(() => CommentThread) @@ -25,6 +34,8 @@ export class CommentThreadResolver { @Mutation(() => CommentThread, { nullable: false, }) + @UseGuards(AbilityGuard) + @CheckAbilities(CreateCommentThreadAbilityHandler) async createOneCommentThread( @Args() args: CreateOneCommentThreadArgs, @AuthWorkspace() workspace: Workspace, @@ -53,6 +64,8 @@ export class CommentThreadResolver { @Mutation(() => CommentThread, { nullable: false, }) + @UseGuards(AbilityGuard) + @CheckAbilities(UpdateCommentThreadAbilityHandler) async updateOneCommentThread( @Args() args: UpdateOneCommentThreadArgs, @PrismaSelector({ modelName: 'CommentThread' }) @@ -67,19 +80,20 @@ export class CommentThreadResolver { } @Query(() => [CommentThread]) + @UseGuards(AbilityGuard) + @CheckAbilities(ReadCommentThreadAbilityHandler) async findManyCommentThreads( @Args() args: FindManyCommentThreadArgs, - @AuthWorkspace() workspace: Workspace, + @UserAbility() ability: AppAbility, @PrismaSelector({ modelName: 'CommentThread' }) prismaSelect: PrismaSelect<'CommentThread'>, ): Promise[]> { - const preparedArgs = prepareFindManyArgs( - args, - workspace, - ); - const result = await this.commentThreadService.findMany({ - ...preparedArgs, + ...args, + where: { + ...args.where, + AND: [accessibleBy(ability).CommentThread], + }, select: prismaSelect.value, }); diff --git a/server/src/core/comment/resolvers/comment.resolver.spec.ts b/server/src/core/comment/resolvers/comment.resolver.spec.ts index 1554710b19d8..8955ec7bce05 100644 --- a/server/src/core/comment/resolvers/comment.resolver.spec.ts +++ b/server/src/core/comment/resolvers/comment.resolver.spec.ts @@ -3,6 +3,7 @@ import { CommentResolver } from './comment.resolver'; import { CommentService } from '../services/comment.service'; import { CreateOneCommentGuard } from 'src/guards/create-one-comment.guard'; import { CanActivate } from '@nestjs/common'; +import { AbilityFactory } from 'src/ability/ability.factory'; describe('CommentResolver', () => { let resolver: CommentResolver; @@ -17,6 +18,10 @@ describe('CommentResolver', () => { provide: CommentService, useValue: {}, }, + { + provide: AbilityFactory, + useValue: {}, + }, ], }) .overrideGuard(CreateOneCommentGuard) diff --git a/server/src/core/comment/resolvers/comment.resolver.ts b/server/src/core/comment/resolvers/comment.resolver.ts index 15c033589b4b..385fb83dd06d 100644 --- a/server/src/core/comment/resolvers/comment.resolver.ts +++ b/server/src/core/comment/resolvers/comment.resolver.ts @@ -12,6 +12,11 @@ import { PrismaSelector, PrismaSelect, } from 'src/decorators/prisma-select.decorator'; +import { AbilityGuard } from 'src/guards/ability.guard'; +import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; +import { CreateCommentAbilityHandler } from 'src/ability/handlers/comment.ability-handler'; +import { AuthUser } from 'src/decorators/auth-user.decorator'; +import { User } from 'src/core/@generated/user/user.model'; @UseGuards(JwtAuthGuard) @Resolver(() => Comment) @@ -22,8 +27,11 @@ export class CommentResolver { @Mutation(() => Comment, { nullable: false, }) + @UseGuards(AbilityGuard) + @CheckAbilities(CreateCommentAbilityHandler) async createOneComment( @Args() args: CreateOneCommentArgs, + @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, @PrismaSelector({ modelName: 'Comment' }) prismaSelect: PrismaSelect<'Comment'>, diff --git a/server/src/core/company/company.resolver.spec.ts b/server/src/core/company/company.resolver.spec.ts index 78696bfbc483..c93c65b1f2f1 100644 --- a/server/src/core/company/company.resolver.spec.ts +++ b/server/src/core/company/company.resolver.spec.ts @@ -5,6 +5,7 @@ import { UpdateOneGuard } from 'src/guards/update-one.guard'; import { CanActivate } from '@nestjs/common'; import { DeleteManyGuard } from 'src/guards/delete-many.guard'; import { CreateOneGuard } from 'src/guards/create-one.guard'; +import { AbilityFactory } from 'src/ability/ability.factory'; describe('CompanyResolver', () => { let resolver: CompanyResolver; @@ -19,6 +20,10 @@ describe('CompanyResolver', () => { provide: CompanyService, useValue: {}, }, + { + provide: AbilityFactory, + useValue: {}, + }, ], }) .overrideGuard(UpdateOneGuard) diff --git a/server/src/core/company/company.resolver.ts b/server/src/core/company/company.resolver.ts index e622ad89d9e9..155db7997d36 100644 --- a/server/src/core/company/company.resolver.ts +++ b/server/src/core/company/company.resolver.ts @@ -14,11 +14,21 @@ import { UpdateOneGuard } from '../../guards/update-one.guard'; import { DeleteManyGuard } from '../../guards/delete-many.guard'; import { CreateOneGuard } from '../../guards/create-one.guard'; import { CompanyService } from './company.service'; -import { prepareFindManyArgs } from 'src/utils/prepare-find-many'; import { PrismaSelect, PrismaSelector, } from 'src/decorators/prisma-select.decorator'; +import { AbilityGuard } from 'src/guards/ability.guard'; +import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; +import { + CreateCompanyAbilityHandler, + DeleteCompanyAbilityHandler, + ReadCompanyAbilityHandler, +} from 'src/ability/handlers/company.ability-handler'; +import { UserAbility } from 'src/decorators/user-ability.decorator'; +import { AppAbility } from 'src/ability/ability.factory'; +import { accessibleBy } from '@casl/prisma'; +import { UpdateCommentAbilityHandler } from 'src/ability/handlers/comment.ability-handler'; @UseGuards(JwtAuthGuard) @Resolver(() => Company) @@ -26,18 +36,20 @@ export class CompanyResolver { constructor(private readonly companyService: CompanyService) {} @Query(() => [Company]) + @UseGuards(AbilityGuard) + @CheckAbilities(ReadCompanyAbilityHandler) async findManyCompany( @Args() args: FindManyCompanyArgs, - @AuthWorkspace() workspace: Workspace, + @UserAbility() ability: AppAbility, @PrismaSelector({ modelName: 'Company' }) prismaSelect: PrismaSelect<'Company'>, ): Promise[]> { - const preparedArgs = prepareFindManyArgs( - args, - workspace, - ); return this.companyService.findMany({ - ...preparedArgs, + ...args, + where: { + ...args.where, + AND: [accessibleBy(ability).Company], + }, select: prismaSelect.value, }); } @@ -46,6 +58,8 @@ export class CompanyResolver { @Mutation(() => Company, { nullable: true, }) + @UseGuards(AbilityGuard) + @CheckAbilities(UpdateCommentAbilityHandler) async updateOneCompany( @Args() args: UpdateOneCompanyArgs, @PrismaSelector({ modelName: 'Company' }) @@ -65,6 +79,8 @@ export class CompanyResolver { @Mutation(() => AffectedRows, { nullable: false, }) + @UseGuards(AbilityGuard) + @CheckAbilities(DeleteCompanyAbilityHandler) async deleteManyCompany( @Args() args: DeleteManyCompanyArgs, ): Promise { @@ -77,6 +93,8 @@ export class CompanyResolver { @Mutation(() => Company, { nullable: false, }) + @UseGuards(AbilityGuard) + @CheckAbilities(CreateCompanyAbilityHandler) async createOneCompany( @Args() args: CreateOneCompanyArgs, @AuthWorkspace() workspace: Workspace, diff --git a/server/src/core/person/person.resolver.spec.ts b/server/src/core/person/person.resolver.spec.ts index 67f7433bd967..eec60604b3d1 100644 --- a/server/src/core/person/person.resolver.spec.ts +++ b/server/src/core/person/person.resolver.spec.ts @@ -5,6 +5,7 @@ import { UpdateOneGuard } from 'src/guards/update-one.guard'; import { CanActivate } from '@nestjs/common'; import { DeleteManyGuard } from 'src/guards/delete-many.guard'; import { CreateOneGuard } from 'src/guards/create-one.guard'; +import { AbilityFactory } from 'src/ability/ability.factory'; describe('PersonResolver', () => { let resolver: PersonResolver; @@ -19,6 +20,10 @@ describe('PersonResolver', () => { provide: PersonService, useValue: {}, }, + { + provide: AbilityFactory, + useValue: {}, + }, ], }) .overrideGuard(UpdateOneGuard) diff --git a/server/src/core/person/person.resolver.ts b/server/src/core/person/person.resolver.ts index b83319d086f0..729efb1d18ba 100644 --- a/server/src/core/person/person.resolver.ts +++ b/server/src/core/person/person.resolver.ts @@ -14,11 +14,21 @@ import { UpdateOneGuard } from '../../guards/update-one.guard'; import { DeleteManyGuard } from '../../guards/delete-many.guard'; import { CreateOneGuard } from '../../guards/create-one.guard'; import { PersonService } from './person.service'; -import { prepareFindManyArgs } from 'src/utils/prepare-find-many'; import { PrismaSelect, PrismaSelector, } from 'src/decorators/prisma-select.decorator'; +import { AbilityGuard } from 'src/guards/ability.guard'; +import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; +import { + CreatePersonAbilityHandler, + DeletePersonAbilityHandler, + ReadPersonAbilityHandler, + UpdatePersonAbilityHandler, +} from 'src/ability/handlers/person.ability-handler'; +import { UserAbility } from 'src/decorators/user-ability.decorator'; +import { AppAbility } from 'src/ability/ability.factory'; +import { accessibleBy } from '@casl/prisma'; @UseGuards(JwtAuthGuard) @Resolver(() => Person) @@ -28,19 +38,20 @@ export class PersonResolver { @Query(() => [Person], { nullable: false, }) + @UseGuards(AbilityGuard) + @CheckAbilities(ReadPersonAbilityHandler) async findManyPerson( @Args() args: FindManyPersonArgs, - @AuthWorkspace() workspace: Workspace, + @UserAbility() ability: AppAbility, @PrismaSelector({ modelName: 'Person' }) prismaSelect: PrismaSelect<'Person'>, ): Promise[]> { - const preparedArgs = prepareFindManyArgs( - args, - workspace, - ); - return this.personService.findMany({ - ...preparedArgs, + ...args, + where: { + ...args.where, + AND: [accessibleBy(ability).Person], + }, select: prismaSelect.value, }); } @@ -49,6 +60,8 @@ export class PersonResolver { @Mutation(() => Person, { nullable: true, }) + @UseGuards(AbilityGuard) + @CheckAbilities(UpdatePersonAbilityHandler) async updateOnePerson( @Args() args: UpdateOnePersonArgs, @PrismaSelector({ modelName: 'Person' }) @@ -68,6 +81,8 @@ export class PersonResolver { @Mutation(() => AffectedRows, { nullable: false, }) + @UseGuards(AbilityGuard) + @CheckAbilities(DeletePersonAbilityHandler) async deleteManyPerson( @Args() args: DeleteManyPersonArgs, ): Promise { @@ -80,6 +95,8 @@ export class PersonResolver { @Mutation(() => Person, { nullable: false, }) + @UseGuards(AbilityGuard) + @CheckAbilities(CreatePersonAbilityHandler) async createOnePerson( @Args() args: CreateOnePersonArgs, @AuthWorkspace() workspace: Workspace, diff --git a/server/src/core/user/user.resolver.spec.ts b/server/src/core/user/user.resolver.spec.ts index cb796fea76b4..4f4f887f4243 100644 --- a/server/src/core/user/user.resolver.spec.ts +++ b/server/src/core/user/user.resolver.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UserResolver } from './user.resolver'; import { UserService } from './user.service'; +import { AbilityFactory } from 'src/ability/ability.factory'; describe('UserResolver', () => { let resolver: UserResolver; @@ -13,6 +14,10 @@ describe('UserResolver', () => { provide: UserService, useValue: {}, }, + { + provide: AbilityFactory, + useValue: {}, + }, ], }).compile(); diff --git a/server/src/core/user/user.resolver.ts b/server/src/core/user/user.resolver.ts index f5e876df9546..7078a89295a2 100644 --- a/server/src/core/user/user.resolver.ts +++ b/server/src/core/user/user.resolver.ts @@ -11,6 +11,12 @@ import { PrismaSelect, PrismaSelector, } from 'src/decorators/prisma-select.decorator'; +import { AbilityGuard } from 'src/guards/ability.guard'; +import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; +import { ReadUserAbilityHandler } from 'src/ability/handlers/user.ability-handler'; +import { UserAbility } from 'src/decorators/user-ability.decorator'; +import { AppAbility } from 'src/ability/ability.factory'; +import { accessibleBy } from '@casl/prisma'; @UseGuards(JwtAuthGuard) @Resolver(() => User) @@ -21,9 +27,12 @@ export class UserResolver { @Query(() => [User], { nullable: false, }) + @UseGuards(AbilityGuard) + @CheckAbilities(ReadUserAbilityHandler) async findManyUser( @Args() args: FindManyUserArgs, @AuthWorkspace() workspace: Workspace, + @UserAbility() ability: AppAbility, @PrismaSelector({ modelName: 'User' }) prismaSelect: PrismaSelect<'User'>, ): Promise[]> { @@ -31,9 +40,7 @@ export class UserResolver { ...args, where: { ...args.where, - workspaceMember: { - is: { workspace: { is: { id: { equals: workspace.id } } } }, - }, + AND: [accessibleBy(ability).User], }, select: prismaSelect.value, }); diff --git a/server/src/utils/prepare-find-many.ts b/server/src/utils/prepare-find-many.ts deleted file mode 100644 index 65d6625ba306..000000000000 --- a/server/src/utils/prepare-find-many.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Workspace } from '@prisma/client'; - -type FindManyArgsType = { where?: object; orderBy?: object }; - -export const prepareFindManyArgs = ( - args: T, - workspace: Workspace, -): T => { - args.where = { - ...args.where, - ...{ workspace: { is: { id: { equals: workspace.id } } } }, - }; - - return args; -};