diff --git a/.env.example b/.env.example index 5e7045785c..68300ecaac 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/src/config/config.ts b/src/config/config.ts index cf77d593ae..785a112b1d 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -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, @@ -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 { diff --git a/src/subdomains/generic/gs/__tests__/gs.controller.spec.ts b/src/subdomains/generic/gs/__tests__/gs.controller.spec.ts index a9fe37e0df..4504a80dce 100644 --- a/src/subdomains/generic/gs/__tests__/gs.controller.spec.ts +++ b/src/subdomains/generic/gs/__tests__/gs.controller.spec.ts @@ -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'; @@ -31,6 +34,10 @@ describe('GsController', () => { beforeEach(() => { service = createMock(); 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); }); @@ -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); + }); + }); }); diff --git a/src/subdomains/generic/gs/gs.controller.ts b/src/subdomains/generic/gs/gs.controller.ts index 52e2134dd1..9ef3a70b39 100644 --- a/src/subdomains/generic/gs/gs.controller.ts +++ b/src/subdomains/generic/gs/gs.controller.ts @@ -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'; @@ -61,6 +62,9 @@ export class GsController { @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.SUPPORT), UserActiveGuard()) async getSupportData(@Query() query: SupportDataQuery): Promise { + // 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); }