From aa1905f59066b1b7293c4f01bd2f2ad028ce32ea Mon Sep 17 00:00:00 2001 From: Roger Floriano <31597636+petruki@users.noreply.github.com> Date: Sun, 7 May 2023 14:47:24 -0700 Subject: [PATCH] #389 - Validate Relay is verified when triggered (#403) * #389 - Validate Relay is verified when triggered * Fixed CI to skip Relay verification --- .env-cmdrc-template | 1 + .github/workflows/master.yml | 1 + .github/workflows/release.yml | 1 + config/.env.dev | 1 + docker-compose.yml | 1 + src/app.js | 1 + src/client/relay/index.js | 34 ++++++--- src/client/resolvers.js | 15 ++-- src/services/config.js | 14 +++- tests/relay.test.js | 134 +++++++++++++++++++++++++++++----- 10 files changed, 164 insertions(+), 39 deletions(-) diff --git a/.env-cmdrc-template b/.env-cmdrc-template index 3ca75bf..93384bc 100644 --- a/.env-cmdrc-template +++ b/.env-cmdrc-template @@ -13,6 +13,7 @@ "JWT_ADMIN_TOKEN_RENEW_INTERVAL": "5m", "MAX_STRATEGY_OPERATION": 100, "RELAY_BYPASS_HTTPS": true, + "RELAY_BYPASS_VERIFICATION": true, "HISTORY_ACTIVATED": true, "METRICS_ACTIVATED": true, "METRICS_MAX_PAGE": 50, diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 831cb1e..f78c34a 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -48,6 +48,7 @@ jobs: GOOGLE_SKIP_AUTH: false MAX_STRATEGY_OPERATION: 100 RELAY_BYPASS_HTTPS: true + RELAY_BYPASS_VERIFICATION: true METRICS_ACTIVATED: true METRICS_MAX_PAGE: 50 SWITCHER_API_ENABLE: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ae21458..bead5c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,6 +42,7 @@ jobs: GOOGLE_SKIP_AUTH: false MAX_STRATEGY_OPERATION: 100 RELAY_BYPASS_HTTPS: true + RELAY_BYPASS_VERIFICATION: true METRICS_ACTIVATED: true METRICS_MAX_PAGE: 50 SWITCHER_API_ENABLE: false diff --git a/config/.env.dev b/config/.env.dev index c25bebf..0d955e8 100644 --- a/config/.env.dev +++ b/config/.env.dev @@ -6,6 +6,7 @@ JWT_CLIENT_TOKEN_EXP_TIME=5m JWT_ADMIN_TOKEN_RENEW_INTERVAL=10m MAX_STRATEGY_OPERATION=100 RELAY_BYPASS_HTTPS=true +RELAY_BYPASS_VERIFICATION=true REGEX_MAX_TIMEOUT=3000 REGEX_MAX_BLACLIST=50 HISTORY_ACTIVATED=true diff --git a/docker-compose.yml b/docker-compose.yml index 01383bb..ebcf8c5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,6 +44,7 @@ services: - JWT_ADMIN_TOKEN_RENEW_INTERVAL=${JWT_ADMIN_TOKEN_RENEW_INTERVAL} - MAX_STRATEGY_OPERATION=${MAX_STRATEGY_OPERATION} - RELAY_BYPASS_HTTPS=${RELAY_BYPASS_HTTPS} + - RELAY_BYPASS_VERIFICATION=${RELAY_BYPASS_VERIFICATION} - HISTORY_ACTIVATED=${HISTORY_ACTIVATED} - METRICS_ACTIVATED=${METRICS_ACTIVATED} - METRICS_MAX_PAGE=${METRICS_MAX_PAGE} diff --git a/src/app.js b/src/app.js index 65977e6..7f5e601 100644 --- a/src/app.js +++ b/src/app.js @@ -97,6 +97,7 @@ app.get('/check', (_req, res) => { switcherapi: process.env.SWITCHER_API_ENABLE, switcherapi_logger: process.env.SWITCHER_API_LOGGER, relay_bypass_https: process.env.RELAY_BYPASS_HTTPS, + relay_bypass_verification: process.env.RELAY_BYPASS_VERIFICATION, history: process.env.HISTORY_ACTIVATED, metrics: process.env.METRICS_ACTIVATED, max_metrics_pages: process.env.METRICS_MAX_PAGE, diff --git a/src/client/relay/index.js b/src/client/relay/index.js index d6e5763..e89b37f 100644 --- a/src/client/relay/index.js +++ b/src/client/relay/index.js @@ -1,20 +1,27 @@ import axios from 'axios'; import { StrategiesToRelayDataType, RelayMethods } from '../../models/config'; -export function resolveNotification(url, method, entry, auth_prefix, auth_token) { - if (method === RelayMethods.GET) { - get(url, createParams(entry), createHeader(auth_prefix, auth_token)); +export function resolveNotification(relay, entry, environment) { + const url = relay.endpoint[environment]; + const header = createHeader(relay.auth_prefix, relay.auth_token, environment); + + if (relay.method === RelayMethods.GET) { + get(url, createParams(entry), header); } else { - post(url, createBody(entry), createHeader(auth_prefix, auth_token)); + post(url, createBody(entry), header); } } -export async function resolveValidation(url, method, entry, auth_prefix, auth_token) { +export async function resolveValidation(relay, entry, environment) { let response; - if (method === RelayMethods.GET) { - response = await get(url, createParams(entry), createHeader(auth_prefix, auth_token)); + + const url = relay.endpoint[environment]; + const header = createHeader(relay.auth_prefix, relay.auth_token, environment); + + if (relay.method === RelayMethods.GET) { + response = await get(url, createParams(entry), header); } else { - response = await post(url, createBody(entry), createHeader(auth_prefix, auth_token)); + response = await post(url, createBody(entry), header); } return { @@ -56,13 +63,16 @@ function createParams(entry) { return ''; } -function createHeader(auth_prefix, auth_token) { +function createHeader(auth_prefix, auth_token, environment) { let headers = {}; headers['Content-Type'] = 'application/json'; - if (auth_prefix) - headers['Authorization'] = `${auth_prefix} ${auth_token}`; - + + if (auth_token && environment in auth_token && + auth_prefix && auth_token[environment]) { + headers['Authorization'] = `${auth_prefix} ${auth_token[environment]}`; + } + return { headers }; diff --git a/src/client/resolvers.js b/src/client/resolvers.js index 6a45844..3e2548d 100644 --- a/src/client/resolvers.js +++ b/src/client/resolvers.js @@ -9,7 +9,7 @@ import { verifyOwnership } from '../helpers'; import { resolveNotification, resolveValidation } from './relay/index'; import Component from '../models/component'; import Logger from '../helpers/logger'; -import { validateRelay } from '../services/config'; +import { isRelayVerified, isRelayValid } from '../services/config'; export const resolveConfigByKey = async (domain, key) => Config.findOne({ domain, key }, null, { lean: true }); @@ -173,15 +173,14 @@ async function checkConfigStrategies(configId, strategyFilter) { async function resolveRelay(config, environment, entry, response) { try { if (config.relay?.activated[environment]) { - validateRelay(config.relay); + isRelayValid(config.relay); + isRelayVerified(config.relay); if (config.relay.type === RelayTypes.NOTIFICATION) { - resolveNotification(config.relay.endpoint[environment], config.relay.method, entry, - config.relay.auth_prefix, config.relay.auth_token[environment]); + resolveNotification(config.relay, entry, environment); } else { - const relayResponse = await resolveValidation(config.relay.endpoint[environment], config.relay.method, entry, - config.relay.auth_prefix, config.relay.auth_token[environment]); - + const relayResponse = await resolveValidation(config.relay, entry, environment); + response.result = relayResponse.result; response.reason = relayResponse.result ? 'Success' : 'Relay does not agree'; response.message = relayResponse.message; @@ -190,7 +189,7 @@ async function resolveRelay(config, environment, entry, response) { } catch (e) { if (config.relay.type === RelayTypes.VALIDATION) { response.result = false; - response.reason = 'Relay service could not be reached'; + response.reason = `Relay service could not be reached: ${e.message}`; Logger.error(response.reason, e); } } diff --git a/src/services/config.js b/src/services/config.js index d502e88..6eb98db 100644 --- a/src/services/config.js +++ b/src/services/config.js @@ -129,7 +129,7 @@ export async function updateConfig(id, args, admin) { export async function updateConfigRelay(id, args, admin) { let config = await getConfigById(id); - validateRelay(args); + isRelayValid(args); config = await verifyOwnership(admin, config, config.domain, ActionTypes.UPDATE, RouterTypes.CONFIG); config.updatedBy = admin.email; @@ -286,7 +286,7 @@ export async function verifyRelay(id, code, admin) { return 'failed'; } -export function validateRelay(relay) { +export function isRelayValid(relay) { const bypass = process.env.RELAY_BYPASS_HTTPS === 'true' || false; if (bypass || !relay.endpoint) @@ -297,4 +297,14 @@ export function validateRelay(relay) { if (foundNotHttps.length) throw new BadRequestError('HTTPS required'); +} + +export function isRelayVerified(relay) { + const bypass = process.env.RELAY_BYPASS_VERIFICATION === 'true' || false; + + if (bypass) + return; + + if (!relay.verified) + throw new BadRequestError('Relay not verified'); } \ No newline at end of file diff --git a/tests/relay.test.js b/tests/relay.test.js index 91c7f68..f6fb792 100644 --- a/tests/relay.test.js +++ b/tests/relay.test.js @@ -3,7 +3,7 @@ import request from 'supertest'; import sinon from 'sinon'; import axios from 'axios'; import app from '../src/app'; -import { RelayMethods, RelayTypes } from '../src/models/config'; +import { Config, RelayMethods, RelayTypes } from '../src/models/config'; import { setupDatabase, adminMasterAccountToken, @@ -27,6 +27,19 @@ const changeStrategy = async (strategyId, newOperation, status, environment) => await strategy.save(); }; +const bodyRelay = (endpoint) => { + return { + type: RelayTypes.VALIDATION, + activated: { + default: true + }, + endpoint: { + default: endpoint + }, + method: RelayMethods.GET + }; +}; + beforeAll(setupDatabase); afterAll(async () => { @@ -295,7 +308,7 @@ describe('Testing Switcher Relay', () => { axiosStub.restore(); expect(req.statusCode).toBe(200); - expect(req.body.reason).toEqual('Relay service could not be reached'); + expect(req.body.reason).toEqual('Relay service could not be reached: Failed to reach http://localhost:3001 via GET'); expect(req.body.result).toBe(false); }); @@ -318,7 +331,7 @@ describe('Testing Switcher Relay', () => { axiosStub.restore(); expect(req.statusCode).toBe(200); - expect(req.body.reason).toEqual('Relay service could not be reached'); + expect(req.body.reason).toEqual('Relay service could not be reached: Failed to reach http://localhost:3001 via POST'); expect(req.body.result).toBe(false); }); @@ -326,19 +339,6 @@ describe('Testing Switcher Relay', () => { describe('Testing Switcher Relay Validation', () => { - const bodyRelay = (endpoint) => { - return { - type: RelayTypes.VALIDATION, - activated: { - default: true - }, - endpoint: { - default: endpoint - }, - method: RelayMethods.GET - }; - }; - let token; beforeAll(async () => { @@ -389,7 +389,107 @@ describe('Testing Switcher Relay Validation', () => { ]}); expect(req.statusCode).toBe(200); - expect(req.body.reason).toEqual('Relay service could not be reached'); + expect(req.body.reason).toEqual('Relay service could not be reached: HTTPS required'); + }); + +}); + +describe('Testing Switcher Relay Verification', () => { + + let token; + let axiosStub; + + beforeAll(async () => { + const response = await request(app) + .post('/criteria/auth') + .set('switcher-api-key', `${apiKey}`) + .send({ + domain: domainDocument.name, + component: component1.name, + environment: EnvType.DEFAULT + }).expect(200); + + token = response.body.token; + }); + + afterAll(() => { + process.env.RELAY_BYPASS_VERIFICATION = true; + }); + + test('RELAY_SUITE - Should return Relay could not be reached - Not verified', async () => { + // Given + // Verification required + process.env.RELAY_BYPASS_VERIFICATION = false; + + // Setup Switcher + await request(app) + .patch(`/config/updateRelay/${configId}`) + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(bodyRelay('https://localhost:3001')).expect(200); + + // Test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + } + ]}); + + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Relay service could not be reached: Relay not verified'); + }); + + test('RELAY_SUITE - Should return success when validating verified relay', async () => { + // Mock + axiosStub = sinon.stub(axios, 'get'); + + // Given + const mockRelayService = { data: { result: true, reason: 'Success' } }; + axiosStub.returns(Promise.resolve(mockRelayService)); + + // Verification required + process.env.RELAY_BYPASS_VERIFICATION = false; + + // Setup Switcher + await request(app) + .patch(`/config/updateRelay/${configId}`) + .set('Authorization', `Bearer ${adminMasterAccountToken}`) + .send(bodyRelay('https://localhost:3001')).expect(200); + + // Config has a verified Relay + const config = await Config.findById(configId).exec(); + config.relay.verified = true; + config.relay.verification_code = '123'; + await config.save(); + expect(config.relay.verified).toBe(true); + + // Test + const req = await request(app) + .post(`/criteria?key=${keyConfig}&showReason=true&showStrategy=true`) + .set('Authorization', `Bearer ${token}`) + .send({ + entry: [ + { + strategy: StrategiesType.VALUE, + input: 'USER_1' + }, + { + strategy: StrategiesType.NETWORK, + input: '10.0.0.3' + } + ]}); + + axiosStub.restore(); + expect(req.statusCode).toBe(200); + expect(req.body.reason).toEqual('Success'); }); }); \ No newline at end of file