Skip to content

Commit

Permalink
Add signature to all webhook calls in Job Launcher (#1050)
Browse files Browse the repository at this point in the history
  • Loading branch information
flopez7 authored Oct 17, 2023
1 parent 7865838 commit e45d28f
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 76 deletions.
5 changes: 5 additions & 0 deletions packages/apps/job-launcher/server/src/common/enums/role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum Role {
Exchange = 'exchange',
Recording = 'recording',
Reputation = 'reputation',
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, UnauthorizedException, BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { SignatureAuthGuard } from './signature.auth';
import { verifySignature } from '../utils/signature';
import { ChainId, EscrowUtils } from '@human-protocol/sdk';
import { MOCK_ADDRESS } from '../../../test/constants';
import { Role } from '../enums/role';

jest.mock('../../common/utils/signature');

jest.mock('@human-protocol/sdk', () => ({
...jest.requireActual('@human-protocol/sdk'),
EscrowUtils: {
getEscrow: jest.fn().mockResolvedValue({
launcher: '0x1234567890123456789012345678901234567890',
exchangeOracle: '0x1234567890123456789012345678901234567891',
reputationOracle: '0x1234567890123456789012345678901234567892',
}),
},
}));

describe('SignatureAuthGuard', () => {
let guard: SignatureAuthGuard;
let mockConfigService: Partial<ConfigService>;

beforeEach(async () => {
mockConfigService = {
get: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SignatureAuthGuard,
{ provide: ConfigService, useValue: mockConfigService }
{
provide: SignatureAuthGuard,
useValue: new SignatureAuthGuard([
Role.Exchange,
Role.Recording,
Role.Reputation,
]),
},
],
}).compile();

Expand All @@ -44,27 +57,39 @@ describe('SignatureAuthGuard', () => {
};
context = {
switchToHttp: jest.fn().mockReturnThis(),
getRequest: jest.fn(() => mockRequest)
getRequest: jest.fn(() => mockRequest),
} as any as ExecutionContext;
});

it('should return true if signature is verified', async () => {
mockRequest.headers['header-signature-key'] = 'validSignature';
mockRequest.body = {
escrowAddress: MOCK_ADDRESS,
chainId: ChainId.LOCALHOST,
};
(verifySignature as jest.Mock).mockReturnValue(true);

const result = await guard.canActivate(context as any);
expect(result).toBeTruthy();
expect(EscrowUtils.getEscrow).toHaveBeenCalledWith(
ChainId.LOCALHOST,
MOCK_ADDRESS,
);
});

it('should throw unauthorized exception if signature is not verified', async () => {
(verifySignature as jest.Mock).mockReturnValue(false);

await expect(guard.canActivate(context as any)).rejects.toThrow(UnauthorizedException);
await expect(guard.canActivate(context as any)).rejects.toThrow(
UnauthorizedException,
);
});

it('should throw unauthorized exception for unrecognized oracle type', async () => {
mockRequest.originalUrl = '/some/random/path';
await expect(guard.canActivate(context as any)).rejects.toThrow(UnauthorizedException);
await expect(guard.canActivate(context as any)).rejects.toThrow(
UnauthorizedException,
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,31 @@ import {
} from '@nestjs/common';
import { verifySignature } from '../utils/signature';
import { HEADER_SIGNATURE_KEY } from '../constants';
import { ConfigService } from '@nestjs/config';
import { ConfigNames } from '../config';
import { EscrowUtils } from '@human-protocol/sdk';
import { Role } from '../enums/role';

@Injectable()
export class SignatureAuthGuard implements CanActivate {
constructor(public readonly configService: ConfigService) {}
constructor(private role: Role[]) {}

public async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();

const data = request.body;
const signature = request.headers[HEADER_SIGNATURE_KEY];
const oracleAdresses = [
this.configService.get<string>(
ConfigNames.FORTUNE_EXCHANGE_ORACLE_ADDRESS,
)!,
this.configService.get<string>(ConfigNames.CVAT_EXCHANGE_ORACLE_ADDRESS)!,
];

const oracleAdresses: string[] = [];
try {
const escrowData = await EscrowUtils.getEscrow(
data.chainId,
data.escrowAddress,
);
if (this.role.includes(Role.Exchange))
oracleAdresses.push(escrowData.exchangeOracle!);
if (this.role.includes(Role.Recording))
oracleAdresses.push(escrowData.recordingOracle!);
if (this.role.includes(Role.Reputation))
oracleAdresses.push(escrowData.reputationOracle!);

const isVerified = verifySignature(data, signature, oracleAdresses);

if (isVerified) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { JobRequestType, JobStatusFilter } from '../../common/enums/job';
import { Public } from '../../common/decorators';
import { HEADER_SIGNATURE_KEY } from '../../common/constants';
import { ChainId } from '@human-protocol/sdk';
import { Role } from '../../common/enums/role';

@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
Expand Down Expand Up @@ -109,9 +110,9 @@ export class JobController {
}

@Public()
@UseGuards(SignatureAuthGuard)
@UseGuards(new SignatureAuthGuard([Role.Exchange]))
@Post('/escrow-failed-webhook')
public async (
public async(
@Headers(HEADER_SIGNATURE_KEY) _: string,
@Body() data: EscrowFailedWebhookDto,
): Promise<any> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,6 @@ describe('JobService', () => {
});
});


describe('cancelCronJob', () => {
let escrowClientMock: any,
getManifestMock: any,
Expand All @@ -673,13 +672,13 @@ describe('JobService', () => {
getBalance: jest.fn().mockResolvedValue(new Decimal(10)),
};

jobEntityMock = {
status: JobStatus.TO_CANCEL,
fundAmount: 100,
userId: 1,
id: 1,
manifestUrl: MOCK_FILE_URL,
escrowAddress: MOCK_ADDRESS,
jobEntityMock = {
status: JobStatus.TO_CANCEL,
fundAmount: 100,
userId: 1,
id: 1,
manifestUrl: MOCK_FILE_URL,
escrowAddress: MOCK_ADDRESS,
chainId: ChainId.LOCALHOST,
save: jest.fn(),
};
Expand Down Expand Up @@ -717,7 +716,12 @@ describe('JobService', () => {
it('should return true when the job is successfully canceled', async () => {
findOneJobMock.mockResolvedValue(jobEntityMock as any);

jest.spyOn(jobService, 'processEscrowCancellation').mockResolvedValueOnce({ txHash: MOCK_TRANSACTION_HASH, amountRefunded: BigNumber.from(1) });
jest
.spyOn(jobService, 'processEscrowCancellation')
.mockResolvedValueOnce({
txHash: MOCK_TRANSACTION_HASH,
amountRefunded: BigNumber.from(1),
});
const manifestMock = {
requestType: JobRequestType.FORTUNE,
};
Expand All @@ -727,7 +731,9 @@ describe('JobService', () => {
const result = await jobService.cancelCronJob();

expect(result).toBeTruthy();
expect(jobService.processEscrowCancellation).toHaveBeenCalledWith(jobEntityMock);
expect(jobService.processEscrowCancellation).toHaveBeenCalledWith(
jobEntityMock,
);
expect(jobEntityMock.save).toHaveBeenCalled();
});

Expand All @@ -737,8 +743,12 @@ describe('JobService', () => {
escrowAddress: undefined,
};

jest.spyOn(jobRepository, 'findOne').mockResolvedValueOnce(jobEntityWithoutEscrow as any);
jest.spyOn(jobService, 'processEscrowCancellation').mockResolvedValueOnce(undefined as any);
jest
.spyOn(jobRepository, 'findOne')
.mockResolvedValueOnce(jobEntityWithoutEscrow as any);
jest
.spyOn(jobService, 'processEscrowCancellation')
.mockResolvedValueOnce(undefined as any);
const manifestMock = {
requestType: JobRequestType.FORTUNE,
};
Expand All @@ -754,23 +764,29 @@ describe('JobService', () => {
getStatus: jest.fn().mockResolvedValue(EscrowStatus.Complete),
}));

await expect(jobService.processEscrowCancellation(jobEntityMock as any)).rejects.toThrow(BadRequestException);
await expect(
jobService.processEscrowCancellation(jobEntityMock as any),
).rejects.toThrow(BadRequestException);
});

it('should throw bad request exception if escrowStatus is Paid', async () => {
(EscrowClient.build as any).mockImplementation(() => ({
getStatus: jest.fn().mockResolvedValue(EscrowStatus.Paid),
}));

await expect(jobService.processEscrowCancellation(jobEntityMock as any)).rejects.toThrow(BadRequestException);
await expect(
jobService.processEscrowCancellation(jobEntityMock as any),
).rejects.toThrow(BadRequestException);
});

it('should throw bad request exception if escrowStatus is Cancelled', async () => {
(EscrowClient.build as any).mockImplementation(() => ({
getStatus: jest.fn().mockResolvedValue(EscrowStatus.Cancelled),
}));

await expect(jobService.processEscrowCancellation(jobEntityMock as any)).rejects.toThrow(BadRequestException);
await expect(
jobService.processEscrowCancellation(jobEntityMock as any),
).rejects.toThrow(BadRequestException);
});

it('should throw bad request exception if escrow balance is zero', async () => {
Expand All @@ -779,7 +795,9 @@ describe('JobService', () => {
getBalance: jest.fn().mockResolvedValue({ eq: () => true }),
}));

await expect(jobService.processEscrowCancellation(jobEntityMock as any)).rejects.toThrow(BadRequestException);
await expect(
jobService.processEscrowCancellation(jobEntityMock as any),
).rejects.toThrow(BadRequestException);
});
});

Expand Down Expand Up @@ -1274,7 +1292,11 @@ describe('JobService', () => {
escrowAddress: 'address',
reason: 'invalid manifest',
};
const mockJobEntity = { status: JobStatus.LAUNCHED, failedReason: dto.reason, save: jest.fn() };
const mockJobEntity = {
status: JobStatus.LAUNCHED,
failedReason: dto.reason,
save: jest.fn(),
};
jobRepository.findOne = jest.fn().mockResolvedValue(mockJobEntity);

const result = await jobService.escrowFailedWebhook(dto);
Expand Down
Loading

1 comment on commit e45d28f

@vercel
Copy link

@vercel vercel bot commented on e45d28f Oct 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

job-launcher-server – ./packages/apps/job-launcher/server

job-launcher-server-git-develop-humanprotocol.vercel.app
job-launcher-server-nine.vercel.app
job-launcher-server-humanprotocol.vercel.app

Please sign in to comment.