-
Notifications
You must be signed in to change notification settings - Fork 1
[PROD RELEASE] - EngagementPaymentApprover role implementation #143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,15 +24,14 @@ import { TopcoderMembersService } from 'src/shared/topcoder/members.service'; | |
| import { Role } from 'src/core/auth/auth.constants'; | ||
| import { Roles, User } from 'src/core/auth/decorators'; | ||
|
|
||
| import { UserInfo } from 'src/dto/user.type'; | ||
|
|
||
| import { AdminService } from './admin.service'; | ||
| import { ResponseDto, ResponseStatusType } from 'src/dto/api-response.dto'; | ||
| import { WinningAuditDto, AuditPayoutDto } from './dto/audit.dto'; | ||
|
|
||
| import { WinningRequestDto, SearchWinningResult } from 'src/dto/winning.dto'; | ||
| import { WinningsRepository } from '../repository/winnings.repo'; | ||
| import { WinningUpdateRequestDto } from './dto/winnings.dto'; | ||
| import { AccessControlService } from 'src/shared/access-control'; | ||
|
|
||
| @ApiTags('AdminWinnings') | ||
| @Controller('/admin') | ||
|
|
@@ -42,20 +41,14 @@ export class AdminController { | |
| private readonly adminService: AdminService, | ||
| private readonly winningsRepo: WinningsRepository, | ||
| private readonly tcMembersService: TopcoderMembersService, | ||
| private readonly accessControlService: AccessControlService, | ||
| ) {} | ||
|
|
||
| private isBaAdmin(user?: { roles?: string[] }) { | ||
| return (user?.roles || []).some( | ||
| (r) => | ||
| r && | ||
| r.trim().toLowerCase() === Role.PaymentBaAdmin.trim().toLowerCase(), | ||
| ); | ||
| } | ||
|
|
||
| @Post('/winnings/search') | ||
| @Roles( | ||
| Role.PaymentAdmin, | ||
| Role.PaymentBaAdmin, | ||
| Role.EngagementPaymentApprover, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| Role.PaymentEditor, | ||
| Role.PaymentViewer, | ||
| ) | ||
|
|
@@ -77,13 +70,14 @@ export class AdminController { | |
| @Body() body: WinningRequestDto, | ||
| @User() user: any, | ||
| ): Promise<ResponseDto<SearchWinningResult>> { | ||
| const result = await this.winningsRepo.searchWinnings( | ||
| await this.adminService.applyBaAdminUserFilters( | ||
| const filters = | ||
| await this.accessControlService.applyFilters<WinningRequestDto>( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| user.id, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| this.isBaAdmin(user), | ||
| user.roles, | ||
| body, | ||
| ), | ||
| ); | ||
| ); | ||
|
|
||
| const result = await this.winningsRepo.searchWinnings(filters); | ||
|
|
||
| if (result.error) { | ||
| result.status = ResponseStatusType.ERROR; | ||
|
|
@@ -98,6 +92,7 @@ export class AdminController { | |
| @Roles( | ||
| Role.PaymentAdmin, | ||
| Role.PaymentBaAdmin, | ||
| Role.EngagementPaymentApprover, | ||
| Role.PaymentEditor, | ||
| Role.PaymentViewer, | ||
| ) | ||
|
|
@@ -118,16 +113,16 @@ export class AdminController { | |
| @Header('Content-Type', 'text/csv') | ||
| @Header('Content-Disposition', 'attachment; filename="winnings.csv"') | ||
| async exportWinnings(@Body() body: WinningRequestDto, @User() user: any) { | ||
| const result = await this.winningsRepo.searchWinnings( | ||
| await this.adminService.applyBaAdminUserFilters( | ||
| const filters = | ||
| await this.accessControlService.applyFilters<WinningRequestDto>( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| user.id, | ||
| this.isBaAdmin(user), | ||
| user.roles, | ||
| { | ||
| ...body, | ||
| limit: 999, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| }, | ||
| ), | ||
| ); | ||
| ); | ||
| const result = await this.winningsRepo.searchWinnings(filters); | ||
|
|
||
| const handles = await this.tcMembersService.getHandlesByUserIds( | ||
| result.data.winnings.map((d) => d.winnerId), | ||
|
|
@@ -181,7 +176,12 @@ export class AdminController { | |
| } | ||
|
|
||
| @Patch('/winnings') | ||
| @Roles(Role.PaymentAdmin, Role.PaymentBaAdmin, Role.PaymentEditor) | ||
| @Roles( | ||
| Role.PaymentAdmin, | ||
| Role.PaymentBaAdmin, | ||
| Role.EngagementPaymentApprover, | ||
| Role.PaymentEditor, | ||
| ) | ||
| @ApiOperation({ | ||
| summary: 'Update winnings with given parameter', | ||
| description: | ||
|
|
@@ -194,7 +194,7 @@ export class AdminController { | |
| }) | ||
| async updateWinning( | ||
| @Body() body: WinningUpdateRequestDto, | ||
| @User() user: UserInfo, | ||
| @User() user: any, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| ): Promise<ResponseDto<string>> { | ||
| if ( | ||
| !body.paymentAmount && | ||
|
|
@@ -210,7 +210,7 @@ export class AdminController { | |
| const result = await this.adminService.updateWinnings( | ||
| body, | ||
| user.id, | ||
| this.isBaAdmin(user), | ||
| user.roles, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| ); | ||
|
|
||
| result.status = ResponseStatusType.SUCCESS; | ||
|
|
@@ -225,6 +225,7 @@ export class AdminController { | |
| @Roles( | ||
| Role.PaymentAdmin, | ||
| Role.PaymentBaAdmin, | ||
| Role.EngagementPaymentApprover, | ||
| Role.PaymentEditor, | ||
| Role.PaymentViewer, | ||
| ) | ||
|
|
@@ -246,9 +247,11 @@ export class AdminController { | |
| @Param('winningID') winningId: string, | ||
| @User() user: any, | ||
| ): Promise<ResponseDto<WinningAuditDto[]>> { | ||
| if (this.isBaAdmin(user)) { | ||
| await this.adminService.verifyBaAdminAccessToWinning(winningId, user.id); | ||
| } | ||
| await this.adminService.verifyUserAccessToWinning( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| winningId, | ||
| user.id, | ||
| user.roles, | ||
| ); | ||
|
|
||
| const result = await this.adminService.getWinningAudit(winningId); | ||
|
|
||
|
|
@@ -264,6 +267,7 @@ export class AdminController { | |
| @Roles( | ||
| Role.PaymentAdmin, | ||
| Role.PaymentBaAdmin, | ||
| Role.EngagementPaymentApprover, | ||
| Role.PaymentEditor, | ||
| Role.PaymentViewer, | ||
| ) | ||
|
|
@@ -286,9 +290,11 @@ export class AdminController { | |
| @Param('winningID') winningId: string, | ||
| @User() user: any, | ||
| ): Promise<ResponseDto<AuditPayoutDto[]>> { | ||
| if (this.isBaAdmin(user)) { | ||
| await this.adminService.verifyBaAdminAccessToWinning(winningId, user.id); | ||
| } | ||
| await this.adminService.verifyUserAccessToWinning( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| winningId, | ||
| user.id, | ||
| user.roles, | ||
| ); | ||
|
|
||
| const result = await this.adminService.getWinningAuditPayout(winningId); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,18 +3,19 @@ import { | |
| HttpStatus, | ||
| NotFoundException, | ||
| BadRequestException, | ||
| UnauthorizedException, | ||
| } from '@nestjs/common'; | ||
|
|
||
| import { Prisma } from '@prisma/client'; | ||
| import { PrismaService } from 'src/shared/global/prisma.service'; | ||
| import { PaymentsService } from 'src/shared/payments'; | ||
| import { AccessControlService } from 'src/shared/access-control/access-control.service'; | ||
|
|
||
| import { ResponseDto } from 'src/dto/api-response.dto'; | ||
| import { PaymentStatus } from 'src/dto/payment.dto'; | ||
| import { WinningAuditDto, AuditPayoutDto } from './dto/audit.dto'; | ||
| import { WinningUpdateRequestDto } from './dto/winnings.dto'; | ||
| import { Logger } from 'src/shared/global'; | ||
| import { WinningRequestDto } from 'src/dto/winning.dto'; | ||
| import { BillingAccountsService } from 'src/shared/topcoder/billing-accounts.service'; | ||
|
|
||
| /** | ||
|
|
@@ -32,21 +33,19 @@ export class AdminService { | |
| private readonly prisma: PrismaService, | ||
| private readonly paymentsService: PaymentsService, | ||
| private readonly baService: BillingAccountsService, | ||
| private readonly accessControlService: AccessControlService, | ||
| ) {} | ||
|
|
||
| async applyBaAdminUserFilters( | ||
| async verifyUserAccessToWinning( | ||
| winningsId: string, | ||
| userId: string, | ||
| isBaAdmin?: boolean, | ||
| filters: WinningRequestDto = {}, | ||
| ) { | ||
| if (!isBaAdmin) { | ||
| return filters; | ||
| roles: string[] = [], | ||
| ): Promise<void> { | ||
| try { | ||
| await this.accessControlService.verifyAccess(winningsId, userId, roles); | ||
| } catch (err) { | ||
| throw new UnauthorizedException(err?.message ?? 'access denied'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| } | ||
|
|
||
| return { | ||
| ...filters, | ||
| billingAccounts: await this.baService.getBillingAccountsForUser(userId), | ||
| }; | ||
| } | ||
|
|
||
| private getWinningById(winningId: string) { | ||
|
|
@@ -121,7 +120,7 @@ export class AdminService { | |
| async updateWinnings( | ||
| body: WinningUpdateRequestDto, | ||
| userId: string, | ||
| isBaAdmin?: boolean, | ||
| roles: string[] = [], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ): Promise<ResponseDto<string>> { | ||
| const result = new ResponseDto<string>(); | ||
|
|
||
|
|
@@ -132,9 +131,7 @@ export class AdminService { | |
| ); | ||
| this.logger.log(`updateWinnings payload: ${JSON.stringify(body)}`); | ||
|
|
||
| if (isBaAdmin) { | ||
| await this.verifyBaAdminAccessToWinning(body.winningsId, userId); | ||
| } | ||
| await this.verifyUserAccessToWinning(body.winningsId, userId, roles); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
|
|
||
| try { | ||
| const payments = await this.getPaymentsByWinningsId( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { Module } from '@nestjs/common'; | ||
| import { AccessControlService } from 'src/shared/access-control/access-control.service'; | ||
| import { PaymentBaProvider } from 'src/shared/access-control/payment-ba.provider'; | ||
| import { EngagementPaymentApproverProvider } from 'src/shared/access-control/engagement-pa.provider'; | ||
| import { Injectable } from '@nestjs/common'; | ||
| import { TopcoderModule } from '../topcoder/topcoder.module'; | ||
|
|
||
| @Injectable() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| class AccessControlRegistrar { | ||
| constructor( | ||
| accessControlService: AccessControlService, | ||
| paymentBaProvider: PaymentBaProvider, | ||
| engagementPaymentApproverProvider: EngagementPaymentApproverProvider, | ||
| ) { | ||
| accessControlService.register(paymentBaProvider); | ||
| accessControlService.register(engagementPaymentApproverProvider); | ||
| } | ||
| } | ||
|
|
||
| @Module({ | ||
| imports: [TopcoderModule], | ||
| controllers: [], | ||
| providers: [ | ||
| AccessControlService, | ||
| PaymentBaProvider, | ||
| EngagementPaymentApproverProvider, | ||
| AccessControlRegistrar, | ||
| ], | ||
| exports: [AccessControlService], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| }) | ||
| export class AccessControlModule {} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { Injectable } from '@nestjs/common'; | ||
| import { RoleAccessProvider } from './role-access.interface'; | ||
|
|
||
| @Injectable() | ||
| export class AccessControlService { | ||
| private providers = new Map<string, RoleAccessProvider<any>>(); | ||
|
|
||
| register(provider: RoleAccessProvider) { | ||
| this.providers.set(provider.roleName.trim().toLowerCase(), provider); | ||
| } | ||
|
|
||
| async applyFilters<T>( | ||
| userId: string, | ||
| roles: string[] = [], | ||
| req: any, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ): Promise<T> { | ||
| let out = { ...req }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| for (const r of roles || []) { | ||
| const p = this.providers.get(r?.trim().toLowerCase()); | ||
| if (p?.applyFilter) { | ||
| out = await p.applyFilter(userId, out); | ||
| } | ||
| } | ||
| return out as T; | ||
| } | ||
|
|
||
| async verifyAccess(resourceId: string, userId: string, roles: string[] = []) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| for (const r of roles || []) { | ||
| const p = this.providers.get(r?.trim().toLowerCase()); | ||
| if (p?.verifyAccessToResource) { | ||
| await p.verifyAccessToResource(resourceId, userId); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { Injectable } from '@nestjs/common'; | ||
| import { RoleAccessProvider } from './role-access.interface'; | ||
| import { PrismaService } from 'src/shared/global/prisma.service'; | ||
| import { Role } from 'src/core/auth/auth.constants'; | ||
| import { winnings_category } from '@prisma/client'; | ||
|
|
||
| @Injectable() | ||
| export class EngagementPaymentApproverProvider implements RoleAccessProvider { | ||
| roleName = Role.EngagementPaymentApprover; | ||
|
|
||
| constructor(private readonly prisma: PrismaService) {} | ||
|
|
||
| // disable rule: prefer this format instead of returning resolved promise (required by interface) | ||
| // eslint-disable-next-line @typescript-eslint/require-await | ||
| async applyFilter<T>(userId: string, req: any): Promise<T> { | ||
| return { ...req, category: winnings_category.ENGAGEMENT_PAYMENT } as T; | ||
| } | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| async verifyAccessToResource(winningsId: string | string[], _userId: string) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| const winningsIds = ([] as string[]).concat(winningsId); | ||
|
|
||
| const winnings = await this.prisma.winnings.findMany({ | ||
| where: { winning_id: { in: winningsIds } }, | ||
| select: { category: true }, | ||
| }); | ||
|
|
||
| const unauthorized = winnings.filter( | ||
| (w) => w.category !== winnings_category.ENGAGEMENT_PAYMENT, | ||
| ); | ||
| if (unauthorized.length > 0) { | ||
| throw new Error( | ||
| `${Role.EngagementPaymentApprover} user is trying to access winning with category='${unauthorized.map(w => w.category).join(', ')}'`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export * from './access-control.module'; | ||
| export * from './access-control.service'; | ||
| export * from './payment-ba.provider'; | ||
| export * from './role-access.interface'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[⚠️
security]Consider verifying that
accessControlServiceis used appropriately in the controller to ensure that the new roleEngagementPaymentApproveris correctly integrated and that access control checks are performed where necessary.