Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ YAPEAL_ROOT_CA=
YAPEAL_WEBHOOK_API_KEY=
YAPEAL_ACCOUNT_IDENTIFIER=

PAYMENT_URL=https://dev.payment.dfx.swiss
SERVICES_URL=https://dev.app.dfx.swiss;https://dev.services.dfx.swiss

LIMIT_REQUEST_SUPPORT_NAME=
Expand Down
5 changes: 4 additions & 1 deletion src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,10 @@ export class Configuration {
];

support = {
// Master switch for the support data endpoint (`GET /v1/gs/support`) — hard-coded (intentionally
// no env/DB/setting), default OFF. Turn it back on only once that endpoint validates its `key`
// query parameter against an allowlist of the target entity's columns.
dataEndpointEnabled: false,
limitRequest: {
mailName: process.env.LIMIT_REQUEST_SUPPORT_NAME,
mailAddress: process.env.LIMIT_REQUEST_SUPPORT_MAIL,
Expand Down Expand Up @@ -686,7 +690,6 @@ export class Configuration {
frontend = {
allowedUrls: (process.env.SERVICES_URL ?? '').split(';'),
services: (process.env.SERVICES_URL ?? '').split(';')[0],
payment: process.env.PAYMENT_URL,

isRedirectUrlAllowed: (url: string): boolean => {
try {
Expand Down
34 changes: 33 additions & 1 deletion src/subdomains/generic/gs/__tests__/gs.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { BadRequestException } from '@nestjs/common';
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { Config, ConfigService, Configuration } from 'src/config/config';
import { JwtPayload } from 'src/shared/auth/jwt-payload.interface';
import { UserRole } from 'src/shared/auth/user-role.enum';
import { DfxLogger } from 'src/shared/services/dfx-logger';
import * as processServiceModule from 'src/shared/services/process.service';
import { DbQueryDto } from 'src/subdomains/generic/gs/dto/db-query.dto';
import { SupportTable } from 'src/subdomains/generic/gs/dto/gs.dto';
import { GsTriggerType } from 'src/subdomains/generic/gs/dto/gs-trigger-type.enum';
import { SupportDataQuery } from 'src/subdomains/generic/gs/dto/support-data.dto';
import { GsController } from 'src/subdomains/generic/gs/gs.controller';
import { GsService } from 'src/subdomains/generic/gs/gs.service';

Expand All @@ -31,6 +34,10 @@ describe('GsController', () => {
beforeEach(() => {
service = createMock<GsService>();
controller = new GsController(service);
// This suite builds the controller directly instead of through a Nest TestingModule, so the
// module-level `Config` has to be installed by hand — that is what the ConfigService constructor
// does. A fresh one per test keeps the endpoint switch at its shipped default.
new ConfigService(new Configuration());
verboseSpy = jest.spyOn(DfxLogger.prototype, 'verbose').mockImplementation();
jest.spyOn(processServiceModule, 'DisabledProcess').mockReturnValue(false);
});
Expand Down Expand Up @@ -114,4 +121,29 @@ describe('GsController', () => {
expect(verboseSpy.mock.calls[1][0]).toBe('DB data call for asset in x?forged failed:');
});
});

describe('getSupportData', () => {
const supportQuery = Object.assign(new SupportDataQuery(), { table: SupportTable.USER_DATA, key: 'id', value: 1 });

it('rejects while the endpoint switch is off, without reaching the GS service', async () => {
// Set explicitly rather than relying on the shipped default: flipping that default is meant to
// fail exactly one test — the one below that guards it.
Config.support.dataEndpointEnabled = false;

await expect(controller.getSupportData(supportQuery)).rejects.toBeInstanceOf(ForbiddenException);
expect(service.getSupportData).not.toHaveBeenCalled();
});

it('ships with the endpoint switch off', () => {
expect(Config.support.dataEndpointEnabled).toBe(false);
});

it('reaches the GS service once the switch is flipped on', async () => {
Config.support.dataEndpointEnabled = true;

await controller.getSupportData(supportQuery);

expect(service.getSupportData).toHaveBeenCalledWith(supportQuery);
});
});
});
4 changes: 4 additions & 0 deletions src/subdomains/generic/gs/gs.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BadRequestException, Body, Controller, ForbiddenException, Get, Post, Query, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth, ApiExcludeEndpoint } from '@nestjs/swagger';
import { Config } from 'src/config/config';
import { GetJwt } from 'src/shared/auth/get-jwt.decorator';
import { JwtPayload } from 'src/shared/auth/jwt-payload.interface';
import { RoleGuard } from 'src/shared/auth/role.guard';
Expand Down Expand Up @@ -61,6 +62,9 @@ export class GsController {
@ApiExcludeEndpoint()
@UseGuards(AuthGuard(), RoleGuard(UserRole.SUPPORT), UserActiveGuard())
async getSupportData(@Query() query: SupportDataQuery): Promise<SupportReturnData> {
// Off by default; the switch in `Config.support` states what has to hold before it goes back on.
if (!Config.support.dataEndpointEnabled) throw new ForbiddenException('Endpoint disabled');

return this.gsService.getSupportData(query);
}

Expand Down
Loading