diff --git a/.env-cmdrc-template b/.env-cmdrc-template index a454eed..b839bb4 100644 --- a/.env-cmdrc-template +++ b/.env-cmdrc-template @@ -32,6 +32,8 @@ "GOOGLE_SKIP_AUTH": true, "SWITCHER_API_LOGGER": false, + "SWITCHER_API_LOGGER_LEVEL": "debug", + "SWITCHER_API_ENABLE": false, "SWITCHER_API_THROTTLE": 5000, "SWITCHER_API_URL": "http://localhost:3000", diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index ce0cb1e..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "env": { - "node": true, - "commonjs": false, - "es6": true, - "jest": true - }, - "extends": "eslint:recommended", - "rules": { - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ] - }, - "parserOptions": { - "ecmaVersion": 2022, - "sourceType": "module", - "ecmaFeatures": { - "experimentalObjectRestSpread": true - } - } -} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 8508345..72da5fc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -67,7 +67,9 @@ services: - BITBUCKET_OAUTH_SECRET=${BITBUCKET_OAUTH_SECRET} - GOOGLE_RECAPTCHA_SECRET=${GOOGLE_RECAPTCHA_SECRET} - GOOGLE_SKIP_AUTH=${GOOGLE_SKIP_AUTH} + - SWITCHER_API_LOGGER=${SWITCHER_API_LOGGER} + - SWITCHER_API_LOGGER_LEVEL=${SWITCHER_API_LOGGER_LEVEL} - SWITCHER_API_ENABLE=${SWITCHER_API_ENABLE} - SWITCHER_API_URL=${SWITCHER_API_URL} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..dfc87bd --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,24 @@ +import js from "@eslint/js"; +import globals from "globals"; + +export default [ + { + ...js.configs.recommended, + files: ["src/**/*.js"] + }, + { + files: ["src/**/*.js"], + rules: { + quotes: ['error', 'single'], + semi: ['error', 'always'], + curly: ['error', 'multi-line'], + }, + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + ...globals.node + } + } + } +]; \ No newline at end of file diff --git a/package.json b/package.json index 8fd3edb..6cb644d 100644 --- a/package.json +++ b/package.json @@ -49,9 +49,9 @@ "helmet": "^7.1.0", "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", - "mongodb": "^6.6.1", - "mongoose": "^8.3.4", - "pino": "^9.0.0", + "mongodb": "^6.6.2", + "mongoose": "^8.4.0", + "pino": "^9.1.0", "pino-pretty": "^11.0.0", "swagger-ui-express": "^5.0.0", "switcher-client": "^4.1.0", @@ -59,12 +59,12 @@ }, "devDependencies": { "env-cmd": "^10.1.0", - "eslint": "^8.57.0", + "eslint": "^9.3.0", "jest": "^29.7.0", "jest-sonar-reporter": "^2.0.0", "node-notifier": "^10.0.1", "nodemon": "^3.1.0", - "sinon": "^17.0.2", + "sinon": "^18.0.0", "supertest": "^7.0.0" }, "overrides": { diff --git a/src/client/configuration-resolvers.js b/src/client/configuration-resolvers.js index ebb8acb..c507a24 100644 --- a/src/client/configuration-resolvers.js +++ b/src/client/configuration-resolvers.js @@ -7,6 +7,7 @@ import { getEnvironments } from '../services/environment.js'; import { getSlack } from '../services/slack.js'; import { getDomainById } from '../services/domain.js'; import { getComponents } from '../services/component.js'; +import Logger from '../helpers/logger.js'; async function resolveConfigByConfig(domainId, key, configId) { const config = await getConfigs({ @@ -85,6 +86,7 @@ export async function resolveFlatConfigStrategy(source, context) { context.admin, strategies, source.config[0].domain, ActionTypes.READ, RouterTypes.STRATEGY, true); } } catch (e) { + Logger.debug('resolveFlatConfigStrategy', e); return null; } @@ -109,6 +111,7 @@ export async function resolveFlatConfig(source, context) { context.admin, configs, domainId, ActionTypes.READ, RouterTypes.CONFIG, true); } } catch (e) { + Logger.debug('resolveFlatConfig', e); return null; } @@ -130,6 +133,7 @@ export async function resolveFlatGroupConfig(source, context) { context.admin, group, group[0].domain, ActionTypes.READ, RouterTypes.GROUP, true); } } catch (e) { + Logger.debug('resolveFlatGroupConfig', e); return null; } @@ -149,6 +153,7 @@ export async function resolveFlatDomain(_source, context) { context.admin, domain, domain._id, ActionTypes.READ, RouterTypes.DOMAIN, true); } } catch (e) { + Logger.debug('resolveFlatDomain', e); return null; } diff --git a/src/client/permission-resolvers.js b/src/client/permission-resolvers.js index 3b33788..c2c768f 100644 --- a/src/client/permission-resolvers.js +++ b/src/client/permission-resolvers.js @@ -3,6 +3,7 @@ import { RouterTypes } from '../models/permission.js'; import { getConfigs } from '../services/config.js'; import { getGroupConfigs } from '../services/group-config.js'; import { permissionCache } from '../helpers/cache.js'; +import Logger from '../helpers/logger.js'; export async function resolvePermission(args, admin) { const cacheKey = permissionCache.permissionKey(admin._id, args.domain, args.parent, @@ -27,6 +28,7 @@ export async function resolvePermission(args, admin) { await verifyOwnership(admin, element, args.domain, action_perm, args.router, false, args.environment); result[result.length - 1].permissions.push({ action: action_perm.toString(), result: 'ok' }); } catch (e) { + Logger.debug('resolvePermission', e); result[result.length - 1].permissions.push({ action: action_perm.toString(), result: 'nok' }); } } diff --git a/src/client/relay/index.js b/src/client/relay/index.js index 02ecc44..1542b0d 100644 --- a/src/client/relay/index.js +++ b/src/client/relay/index.js @@ -2,6 +2,7 @@ import axios from 'axios'; import https from 'https'; import { StrategiesToRelayDataType, RelayMethods } from '../../models/config.js'; import { checkHttpsAgent } from '../../external/switcher-api-facade.js'; +import Logger from '../../helpers/logger.js'; const agent = async (url) => { const response = await checkHttpsAgent(url); @@ -53,6 +54,7 @@ async function post(url, data, headers) { try { return await axios.post(url, data, { httpsAgent: await agent(url), headers }); } catch (error) { + Logger.debug('post', error); throw new Error(`Failed to reach ${url} via POST`); } } @@ -61,6 +63,7 @@ async function get(url, data, headers) { try { return await axios.get(`${url}${data}`, { httpsAgent: await agent(url), headers }); } catch (error) { + Logger.debug('get', error); throw new Error(`Failed to reach ${url} via GET`); } } diff --git a/src/client/resolvers.js b/src/client/resolvers.js index b2a87bf..b32c8ad 100644 --- a/src/client/resolvers.js +++ b/src/client/resolvers.js @@ -48,6 +48,7 @@ export async function resolveConfigStrategy(source, _id, strategy, operation, ac strategies = await verifyOwnership(context.admin, strategies, parentConfig.domain, ActionTypes.READ, RouterTypes.STRATEGY); } } catch (e) { + Logger.debug('resolveConfigStrategy', e); return null; } @@ -60,19 +61,20 @@ export async function resolveConfig(source, _id, key, activated, context) { if (_id) { args._id = _id; } if (key) { args.key = key; } if (context._component) { args.components = context._component; } - + let configs = await Config.find({ group: source._id, ...args }).lean().exec(); - + if (activated !== undefined) { configs = configs.filter(config => config.activated[context.environment] === activated); } - + try { if (context.admin) { let parentGroup = await GroupConfig.findById(source._id).exec(); configs = await verifyOwnership(context.admin, configs, parentGroup.domain, ActionTypes.READ, RouterTypes.CONFIG, true); } } catch (e) { + Logger.debug('resolveConfig', e); return null; } @@ -96,6 +98,7 @@ export async function resolveGroupConfig(source, _id, name, activated, context) groups = await verifyOwnership(context.admin, groups, source._id, ActionTypes.READ, RouterTypes.GROUP, true); } } catch (e) { + Logger.debug('resolveGroupConfig', e); return null; } @@ -119,10 +122,8 @@ export async function resolveDomain(_id, name, activated, context) { } let domain = await Domain.findOne({ ...args }).lean().exec(); - if (activated !== undefined) { - if (domain.activated[context.environment] !== activated) { - return null; - } + if (activated !== undefined && domain.activated[context.environment] !== activated) { + return null; } try { @@ -130,6 +131,7 @@ export async function resolveDomain(_id, name, activated, context) { domain = await verifyOwnership(context.admin, domain, domain._id, ActionTypes.READ, RouterTypes.DOMAIN, true); } } catch (e) { + Logger.debug('resolveDomain', e); return null; } @@ -222,8 +224,9 @@ function checkFlags(config, group, domain, environment) { async function checkStrategy(entry, strategies, environment) { if (strategies) { for (const strategy of strategies) { - if (!strategy.activated[environment]) + if (!strategy.activated[environment]) { continue; + } await checkStrategyInput(entry, strategy); } diff --git a/src/external/google-recaptcha.js b/src/external/google-recaptcha.js index 1dd6ffc..b825bc2 100644 --- a/src/external/google-recaptcha.js +++ b/src/external/google-recaptcha.js @@ -3,11 +3,13 @@ import axios from 'axios'; export const url = 'https://www.google.com/recaptcha/api/siteverify'; export async function validate_token(token, remoteAddress) { - if (!process.env.GOOGLE_RECAPTCHA_SECRET || process.env.GOOGLE_SKIP_AUTH == 'true') + if (!process.env.GOOGLE_RECAPTCHA_SECRET || process.env.GOOGLE_SKIP_AUTH == 'true') { return; + } - if (token === null || token === undefined) + if (token === null || token === undefined) { throw new GoogleRecaptchaError('Token is empty or invalid'); + } const response = await axios.post( `${url}?secret=${process.env.GOOGLE_RECAPTCHA_SECRET}&response=${token}&remoteip=${remoteAddress}`, null, diff --git a/src/external/oauth-bitbucket.js b/src/external/oauth-bitbucket.js index 9040f86..7253e1d 100644 --- a/src/external/oauth-bitbucket.js +++ b/src/external/oauth-bitbucket.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import Logger from '../helpers/logger.js'; export const bitBucketAccessTokenUrl = 'https://bitbucket.org/site/oauth2/access_token'; export const bitBucketAPIUserUrl = 'https://api.bitbucket.org/2.0/user'; @@ -22,6 +23,7 @@ export async function getBitBucketToken(code) { return response.data.access_token; } catch (error) { + Logger.debug('getBitBucketToken', error); throw new BitBucketAuthError('Failed to get Bitbucket access token'); } } @@ -42,7 +44,8 @@ export async function getBitBucketUserInfo(token) { avatar: response.data?.links?.avatar?.href }; } catch (error) { - throw new BitBucketAuthError('Failed to get Bitbucket user info'); + Logger.debug('getBitBucketUserInfo', error); + throw new BitBucketAuthError('Failed to get Bitbucket user info'); } } diff --git a/src/external/oauth-git.js b/src/external/oauth-git.js index bf74829..bb800aa 100644 --- a/src/external/oauth-git.js +++ b/src/external/oauth-git.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import Logger from '../helpers/logger.js'; export const githubAccessTokenUrl = 'https://github.com/login/oauth/access_token'; export const githubAPIUserUrl = 'https://api.github.com/user'; @@ -15,6 +16,7 @@ export async function getGitToken(code) { return response.data.access_token; } catch (error) { + Logger.debug('getGitToken', error); throw new GitAuthError('Failed to get GitHub access token'); } } @@ -35,7 +37,8 @@ export async function getGitUserInfo(token) { avatar: response.data.avatar_url }; } catch (error) { - throw new GitAuthError('Failed to get GitHub user info'); + Logger.debug('getGitUserInfo', error); + throw new GitAuthError('Failed to get GitHub user info'); } } diff --git a/src/external/sendgrid.js b/src/external/sendgrid.js index f92c206..6c78055 100644 --- a/src/external/sendgrid.js +++ b/src/external/sendgrid.js @@ -11,8 +11,9 @@ export function sendAccountRecoveryCode(email, name, code) { } function sendMail(email, name, code, template_id) { - if (!process.env.SENDGRID_API_KEY) + if (!process.env.SENDGRID_API_KEY) { return; + } axios.post(sendGridApiUrl, { from: { email: process.env.SENDGRID_MAIL_FROM }, diff --git a/src/external/switcher-api-facade.js b/src/external/switcher-api-facade.js index 02f9db3..4551602 100644 --- a/src/external/switcher-api-facade.js +++ b/src/external/switcher-api-facade.js @@ -47,8 +47,9 @@ function getFeatureFlag(feature) { } export async function checkDomain(req) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return; + } const total = await getTotalDomainsByOwner(req.admin._id); const featureFlag = await getFeatureFlag(SwitcherKeys.ELEMENT_CREATION) @@ -62,8 +63,9 @@ export async function checkDomain(req) { } export async function checkGroup(domain) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return; + } const total = await getTotalGroupsByDomainId(domain._id); const featureFlag = await getFeatureFlag(SwitcherKeys.ELEMENT_CREATION) @@ -77,8 +79,9 @@ export async function checkGroup(domain) { } export async function checkSwitcher(group) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return; + } const total = await getTotalConfigsByDomainId(group.domain); const { owner } = await getDomainById(group.domain); @@ -93,8 +96,9 @@ export async function checkSwitcher(group) { } export async function checkComponent(domain) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return; + } const total = await getTotalComponentsByDomainId(domain); const { owner } = await getDomainById(domain); @@ -109,8 +113,9 @@ export async function checkComponent(domain) { } export async function checkEnvironment(domain) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return; + } const total = await getTotalEnvByDomainId(domain); const { owner } = await getDomainById(domain); @@ -125,8 +130,9 @@ export async function checkEnvironment(domain) { } export async function checkTeam(domain) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return; + } const total = await getTotalTeamsByDomainId(domain); const { owner } = await getDomainById(domain); @@ -141,8 +147,9 @@ export async function checkTeam(domain) { } export async function checkMetrics(config) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return true; + } const { owner } = await getDomainById(config.domain); const response = await getFeatureFlag(SwitcherKeys.ELEMENT_CREATION) @@ -164,8 +171,9 @@ export async function checkMetrics(config) { } export async function checkHistory(domain) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return true; + } const { owner } = await getDomainById(domain); return getFeatureFlag(SwitcherKeys.ELEMENT_CREATION) @@ -176,8 +184,9 @@ export async function checkHistory(domain) { } export async function checkAdmin(login) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return; + } const featureFlag = await getFeatureFlag(SwitcherKeys.ACCOUNT_CREATION) .checkValue(login) @@ -187,8 +196,9 @@ export async function checkAdmin(login) { } export async function checkSlackIntegration(value) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return; + } const featureFlag = await getFeatureFlag(SwitcherKeys.SLACK_INTEGRATION) .checkValue(value) @@ -198,8 +208,9 @@ export async function checkSlackIntegration(value) { } export function notifyAcCreation(adminid) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return; + } Client.getSwitcher(SwitcherKeys.ACCOUNT_IN_NOTIFY) .checkValue(adminid) @@ -207,8 +218,9 @@ export function notifyAcCreation(adminid) { } export function notifyAcDeletion(adminid) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return; + } Client.getSwitcher(SwitcherKeys.ACCOUNT_OUT_NOTIFY) .checkValue(adminid) @@ -231,8 +243,9 @@ export async function getRateLimit(key, component) { } export async function checkHttpsAgent(value) { - if (process.env.SWITCHER_API_ENABLE != 'true') + if (process.env.SWITCHER_API_ENABLE != 'true') { return; + } return getFeatureFlag(SwitcherKeys.HTTPS_AGENT) .checkRegex(value) diff --git a/src/helpers/index.js b/src/helpers/index.js index 534883c..eab10c1 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -3,6 +3,7 @@ import { EnvType } from '../models/environment.js'; import { getDomainById } from '../services/domain.js'; import { getEnvironments } from '../services/environment.js'; import { getTeams } from '../services/team.js'; +import Logger from './logger.js'; import { verifyPermissions, verifyPermissionsCascade } from './permission.js'; const PATTERN_ALPHANUMERIC_SPACE = /^[a-zA-Z0-9_\- ]*$/; @@ -21,16 +22,19 @@ export async function checkEnvironmentStatusRemoval(domainId, environmentName, s export function payloadReader(payload) { let payloadRead = payload + '' === payload || payload || 0; - if (Array.isArray(payloadRead)) + if (Array.isArray(payloadRead)) { return payloadRead.flatMap(p => payloadReader(p)); + } return Object.keys(payloadRead) .flatMap(field => [field, ...payloadReader(payload[field]) .map(nestedField => `${field}.${nestedField}`)]) .filter(field => isNaN(Number(field))) .reduce((acc, curr) => { - if (!acc.includes(curr)) + if (!acc.includes(curr)) { acc.push(curr); + } + return acc; }, []); } @@ -39,6 +43,7 @@ export function parseJSON(str) { try { return JSON.parse(str); } catch (e) { + Logger.debug('parseJSON', e); return undefined; } } @@ -100,25 +105,28 @@ export function sortBy(args) { } export function validatePagingArgs(args) { - if (args.limit && !Number.isInteger(parseInt(args.limit)) || - parseInt(args.limit) < 1) + if (args.limit && !Number.isInteger(parseInt(args.limit)) || parseInt(args.limit) < 1) { return false; + } - if (args.skip && !Number.isInteger(parseInt(args.skip)) || - parseInt(args.skip) < 0) + if (args.skip && !Number.isInteger(parseInt(args.skip)) || parseInt(args.skip) < 0) { return false; + } if (args.sortBy) { const parts = args.sortBy.split(':'); - if (parts.length != 2) + if (parts.length != 2) { return false; + } - if (!parts[0].match(/^[A-Za-z]+$/)) + if (!parts[0].match(/^[A-Za-z]+$/)) { return false; + } - if (parts[1] != 'asc' && parts[1] != 'desc') + if (parts[1] != 'asc' && parts[1] != 'desc') { return false; + } } return true; @@ -170,7 +178,7 @@ export async function verifyOwnership(admin, element, domainId, actions, routerT hasPermission.push(allowedElement); } } - + if (!hasPermission.length) { throw new PermissionError('Action forbidden'); } diff --git a/src/helpers/logger.js b/src/helpers/logger.js index 236d0b2..2072e10 100644 --- a/src/helpers/logger.js +++ b/src/helpers/logger.js @@ -1,6 +1,7 @@ import pino from 'pino'; const logger = pino({ + level: process.env.SWITCHER_API_LOGGER_LEVEL || 'info', transport: { target: 'pino-pretty' } @@ -12,17 +13,26 @@ export default class Logger { } static info(message, obj) { - if (process.env.SWITCHER_API_LOGGER == 'true') + if (process.env.SWITCHER_API_LOGGER == 'true') { logger.info(obj, message); + } } static error(message, err) { - if (process.env.SWITCHER_API_LOGGER == 'true') + if (process.env.SWITCHER_API_LOGGER == 'true') { logger.error(err, message); + } + } + + static debug(message, obj) { + if (process.env.SWITCHER_API_LOGGER == 'true') { + logger.debug(obj, message); + } } static httpError(name, code, message, err) { - if (process.env.SWITCHER_API_LOGGER == 'true') + if (process.env.SWITCHER_API_LOGGER == 'true') { logger.error(err, `${name} [${code}]: ${message}`); + } } } \ No newline at end of file diff --git a/src/helpers/timed-match/index.js b/src/helpers/timed-match/index.js index fa3a877..c903936 100644 --- a/src/helpers/timed-match/index.js +++ b/src/helpers/timed-match/index.js @@ -27,8 +27,9 @@ export default class TimedMatch { let result = false; let timer, resolveListener; - if (this._isBlackListed({ values, input })) + if (this._isBlackListed({ values, input })) { return false; + } const matchPromise = new Promise((resolve) => { resolveListener = resolve; @@ -88,8 +89,9 @@ export default class TimedMatch { this._worker.kill(); this._worker = this._createChildProcess(); - if (this._blacklisted.length == this._maxBlackListed) + if (this._blacklisted.length == this._maxBlackListed) { this._blacklisted.splice(0, 1); + } this._blacklisted.push({ res: values, diff --git a/src/middleware/limiter.js b/src/middleware/limiter.js index 73984cd..723ab35 100644 --- a/src/middleware/limiter.js +++ b/src/middleware/limiter.js @@ -6,8 +6,9 @@ const ERROR_MESSAGE = { }; const getMaxRate = (rate_limit) => { - if (rate_limit === 0) + if (rate_limit === 0) { return parseInt(DEFAULT_RATE_LIMIT); + } return rate_limit; }; diff --git a/src/models/admin.js b/src/models/admin.js index f1971ae..f01636c 100644 --- a/src/models/admin.js +++ b/src/models/admin.js @@ -139,13 +139,13 @@ adminSchema.statics.findByCredentials = async (email, password) => { const admin = await Admin.findOne({ email, active: true }).exec(); if (!admin) { - throw new Error('Unable to login'); + throw new Error('Unable to login - account not found or not active.'); } const isMatch = await bcryptjs.compare(password, admin.password); if (!isMatch) { - throw new Error('Unable to login'); + throw new Error('Unable to login - invalid credentials.'); } return admin; @@ -205,8 +205,9 @@ adminSchema.pre('save', async function (next) { }); adminSchema.post('save', function(error, _doc, next) { - if (error.name === 'MongoServerError' && error.code === 11000) + if (error.name === 'MongoServerError' && error.code === 11000) { return next(new Error('Account is already registered.')); + } next(error); }); diff --git a/src/models/config-strategy.js b/src/models/config-strategy.js index a1dbdd0..2b4b99f 100644 --- a/src/models/config-strategy.js +++ b/src/models/config-strategy.js @@ -278,8 +278,9 @@ async function processREGEX(operation, input, values) { function processPAYLOAD(operation, input, values) { const inputJson = parseJSON(input); - if (!inputJson) + if (!inputJson) { return false; + } const keys = payloadReader(inputJson); switch(operation) { diff --git a/src/models/domain.js b/src/models/domain.js index 52e3f79..551af2d 100644 --- a/src/models/domain.js +++ b/src/models/domain.js @@ -149,8 +149,9 @@ domainSchema.pre('save', async function (next) { }); domainSchema.post('save', function(error, _doc, next) { - if (error.name === 'MongoServerError' && error.code === 11000) + if (error.name === 'MongoServerError' && error.code === 11000) { return next(new Error('The domain name is already in use.')); + } next(error); }); diff --git a/src/routers/admin.js b/src/routers/admin.js index 1269d3c..0165c52 100644 --- a/src/routers/admin.js +++ b/src/routers/admin.js @@ -8,6 +8,7 @@ import { responseException } from '../exceptions/index.js'; import * as Services from '../services/admin.js'; import { SwitcherKeys } from '../external/switcher-api-facade.js'; import { checkActionType } from '../models/permission.js'; +import Logger from '../helpers/logger.js'; const router = new express.Router(); @@ -63,6 +64,7 @@ router.post('/admin/login', [ res.send({ admin, jwt }); } catch (e) { + Logger.httpError('Login', 401, e.message, e); res.status(401).send({ error: 'Invalid email/password' }); } }); @@ -128,6 +130,7 @@ router.post('/admin/collaboration/permission', auth, [ req.body.router, false, req.body.environment); result.push({ action: action_perm, result: 'ok' }); } catch (e) { + Logger.debug('resolvePermission', e); result.push({ action : action_perm, result: 'nok' }); } } diff --git a/src/services/admin.js b/src/services/admin.js index 92f8225..7edb2aa 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -30,8 +30,9 @@ export async function signUp(args, remoteAddress) { const admin = new Admin(args); const code = await admin.generateAuthCode(); - if (process.env.GOOGLE_SKIP_AUTH == 'true') + if (process.env.GOOGLE_SKIP_AUTH == 'true') { admin.active = true; + } await admin.save(); diff --git a/src/services/config.js b/src/services/config.js index 1d80c21..5df9acf 100644 --- a/src/services/config.js +++ b/src/services/config.js @@ -313,22 +313,26 @@ export async function verifyRelay(id, env, admin) { export function isRelayValid(relay) { const bypass = process.env.RELAY_BYPASS_HTTPS === 'true' || false; - if (bypass || !relay.endpoint) + if (bypass || !relay.endpoint) { return; + } const foundNotHttps = Object.values(relay.endpoint) .filter(endpoint => !endpoint.toLowerCase().startsWith('https')); - if (foundNotHttps.length) + if (foundNotHttps.length) { throw new BadRequestError('HTTPS required'); + } } export function isRelayVerified(relay, environment) { const bypass = process.env.RELAY_BYPASS_VERIFICATION === 'true' || false; - if (bypass) + if (bypass) { return; + } - if (!relay.verified[environment]) + if (!relay.verified[environment]) { throw new BadRequestError('Relay not verified'); + } } \ No newline at end of file diff --git a/src/services/history.js b/src/services/history.js index fd305e4..f31a8f3 100644 --- a/src/services/history.js +++ b/src/services/history.js @@ -3,8 +3,9 @@ import { sortBy, validatePagingArgs } from '../helpers/index.js'; import { BadRequestError } from '../exceptions/index.js'; export async function getHistory(query, domainId, elementId, pagingArgs = {}) { - if (!validatePagingArgs(pagingArgs)) + if (!validatePagingArgs(pagingArgs)) { throw new BadRequestError('Invalid paging args'); + } return History.find({ domainId, elementId }) .select(query) diff --git a/src/services/slack.js b/src/services/slack.js index d8e2a35..b6737cd 100644 --- a/src/services/slack.js +++ b/src/services/slack.js @@ -14,8 +14,9 @@ import { containsValue } from '../helpers/index.js'; */ async function canCreateTicket(slack, ticket_content) { const existingTicket = slack.isTicketOpened(ticket_content); - if (existingTicket.length) + if (existingTicket.length) { return existingTicket[0]; + } let group, config; await Promise.all([ @@ -25,15 +26,17 @@ async function canCreateTicket(slack, ticket_content) { ]).then(result => { group = result[1]; - if (!group) + if (!group) { throw new NotFoundError('Group not found'); + } }); if (ticket_content.switcher) { config = await getConfig({ domain: slack.domain, group: group._id, key: ticket_content.switcher }); - if (!config) + if (!config) { throw new NotFoundError('Switcher not found'); + } } } @@ -52,8 +55,9 @@ async function deleteSlackInstallation(slack) { export async function getSlackOrError(where) { const slack = await getSlack(where); - if (!slack) + if (!slack) { throw new NotFoundError('Slack installation not found'); + } return slack; } @@ -83,8 +87,9 @@ export async function authorizeSlackInstallation(domain, team_id, admin) { const slack = await getSlackOrError({ team_id }); const _domain = await getDomainById(domain); - if (String(_domain.owner) != String(admin._id)) + if (String(_domain.owner) != String(admin._id)) { throw new PermissionError('Only the domain owner can authorize a Slack integration'); + } // update Domain integrations _domain.integrations.slack = slack._id; @@ -98,15 +103,17 @@ export async function authorizeSlackInstallation(domain, team_id, admin) { export async function deleteSlack(enterprise_id, team_id) { const slack = await getSlack({ enterprise_id, team_id }); - if (slack) + if (slack) { return deleteSlackInstallation(slack); + } } export async function unlinkSlack(domainid, admin) { const domain = await getDomainById(domainid); - if (String(domain.owner) != String(admin._id)) + if (String(domain.owner) != String(admin._id)) { throw new PermissionError('Only the domain owner can unlink integrations'); + } const slack = await getSlackOrError({ id: domain.integrations.slack }); return deleteSlackInstallation(slack); @@ -131,8 +138,9 @@ export async function resetTicketHistory(enterprise_id, team_id, admin) { const slack = await getSlackOrError({ enterprise_id, team_id }); const domain = await getDomainById(slack.domain); - if (String(domain.owner) != String(admin._id)) + if (String(domain.owner) != String(admin._id)) { throw new PermissionError('Only the domain owner can reset Ticket history'); + } slack.tickets = []; return slack.save(); @@ -144,8 +152,9 @@ export async function validateTicket(ticket_content, enterprise_id, team_id) { const ticket = await canCreateTicket(slack, ticket_content); const { ignored_environments, frozen_environments } = slack.settings; - if (containsValue(frozen_environments, ticket_content.environment)) + if (containsValue(frozen_environments, ticket_content.environment)) { return { result: TicketValidationType.FROZEN_ENVIRONMENT }; + } if (containsValue(ignored_environments, ticket_content.environment)) { await approveChange(slack.domain, ticket_content); @@ -183,8 +192,9 @@ export async function processTicket(enterprise_id, team_id, ticket_id, approved) t => String(t.id) === String(ticket_id) && t.ticket_status === TicketStatusType.OPENED); - if (!ticket.length) + if (!ticket.length) { throw new NotFoundError('Ticket not found'); + } if (approved) { ticket[0].ticket_status = TicketStatusType.APPROVED; diff --git a/src/services/team.js b/src/services/team.js index d75c4e5..a86b2d3 100644 --- a/src/services/team.js +++ b/src/services/team.js @@ -68,8 +68,10 @@ export async function getTeamInvites(where) { export async function getTeamInvite(where, validate = true) { let teamInvite = await TeamInvite.findOne(where).exec(); - if (validate) + if (validate) { return response(teamInvite, 'Invite request not found'); + } + return teamInvite; } diff --git a/tests/client-api.test.js b/tests/client-api.test.js index f2330a8..6aada1c 100644 --- a/tests/client-api.test.js +++ b/tests/client-api.test.js @@ -2,7 +2,7 @@ import mongoose from 'mongoose'; import request from 'supertest'; import sinon from 'sinon'; import app from '../src/app'; -import { ActionTypes, RouterTypes } from '../src/models/permission'; +import { ActionTypes, Permission, RouterTypes } from '../src/models/permission'; import { permissionCache } from '../src/helpers/cache'; import Domain from '../src/models/domain'; import GroupConfig from '../src/models/group-config'; @@ -30,6 +30,7 @@ import { adminAccountId, slack } from './fixtures/db_client'; +import { Team } from '../src/models/team'; const changeStrategy = async (strategyId, newOperation, status, environment) => { const strategy = await ConfigStrategy.findById(strategyId).exec(); @@ -81,6 +82,21 @@ const createRequestAuth = async () => { }); }; +const setPermissionsToTeam = async (teamId, permission, reset) => { + const permissionId = new mongoose.Types.ObjectId(); + permission._id = permissionId; + + await new Permission(permission).save(); + const team = await Team.findById(teamId).exec(); + + if (reset) { + team.permissions = []; + } + + team.permissions.push(permissionId); + await team.save(); +}; + beforeAll(setupDatabase); afterAll(async () => { @@ -344,7 +360,7 @@ describe('Testing criteria [GraphQL] ', () => { }); test('CLIENT_SUITE - Should not add to metrics when Config has disabled metric flag = true', async () => { - //given + // Given await changeConfigStatus(configId, true, EnvType.DEFAULT); //add one metric data @@ -1075,12 +1091,76 @@ describe('Testing domain [Adm-GraphQL] ', () => { }); }); +describe('Testing domain [Adm-GraphQL] - Permission', () => { + + afterAll(setupDatabase); + + test('CLIENT_SUITE - Should return domain partial structure based on permission', async () => { + // Given + const admin = await Admin.findById(adminAccountId).exec(); + await setPermissionsToTeam(admin.teams[0], { + action: ActionTypes.READ, + active: true, + identifiedBy: 'key', + values: ['TEST_CONFIG_KEY_PRD_QA'], + router: RouterTypes.CONFIG + }, true); + + // Test + const req = await request(app) + .post('/adm-graphql') + .set('Authorization', `Bearer ${adminAccountToken}`) + .send(graphqlUtils.domainQuery([['_id', domainId], ['environment', EnvType.DEFAULT]])); + + expect(req.statusCode).toBe(200); + expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected1071)); + }); + + test('CLIENT_SUITE - Should NOT return complete domain structure - no valid COnfig permission', async () => { + // Given + const admin = await Admin.findById(adminAccountId).exec(); + await setPermissionsToTeam(admin.teams[0], { + action: ActionTypes.READ, + active: true, + router: RouterTypes.GROUP + }, true); + + // Test + const req = await request(app) + .post('/adm-graphql') + .set('Authorization', `Bearer ${adminAccountToken}`) + .send(graphqlUtils.domainQuery([['_id', domainId], ['environment', EnvType.DEFAULT]])); + + expect(req.statusCode).toBe(200); + expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected1072)); + }); + + test('CLIENT_SUITE - Should NOT return complete domain structure - no valid Group permission', async () => { + // Given + const admin = await Admin.findById(adminAccountId).exec(); + await setPermissionsToTeam(admin.teams[0], { + action: ActionTypes.READ, + active: true, + router: RouterTypes.DOMAIN + }, true); + + // Test + const req = await request(app) + .post('/adm-graphql') + .set('Authorization', `Bearer ${adminAccountToken}`) + .send(graphqlUtils.domainQuery([['_id', domainId], ['environment', EnvType.DEFAULT]])); + + expect(req.statusCode).toBe(200); + expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected1073)); + }); +}); + describe('Testing domain/configuration [Adm-GraphQL] - Excluded team member ', () => { afterAll(setupDatabase); test('CLIENT_SUITE - Should NOT return domain structure for an excluded team member', async () => { - //given + // Given const admin = await Admin.findById(adminAccountId).exec(); admin.teams = []; await admin.save(); diff --git a/tests/graphql-utils/index.js b/tests/graphql-utils/index.js index c3f6d9b..1d5fcde 100644 --- a/tests/graphql-utils/index.js +++ b/tests/graphql-utils/index.js @@ -289,6 +289,29 @@ export const expected107 = ` {"key":"TEST_CONFIG_KEY_PRD_QA","description":"Test config 2 - Off in PRD and ON in QA","statusByEnv":[{"env":"default","value":false},{"env":"QA","value":true}],"strategies":null}]}]}}} `; +export const expected1071 = ` + {"data": + {"domain":{"name":"Domain","description":"Test Domain","statusByEnv":[{"env":"default","value":true}], + "group":[ + {"name":"Group Test","description":"Test Group","statusByEnv":[{"env":"default","value":true}], + "config":[ + {"key":"TEST_CONFIG_KEY_PRD_QA","description":"Test config 2 - Off in PRD and ON in QA","statusByEnv":[{"env":"default","value":false},{"env":"QA","value":true}],"strategies":null}]}]}}} + `; + +export const expected1072 = ` + {"data": + {"domain":{"name":"Domain","description":"Test Domain","statusByEnv":[{"env":"default","value":true}], + "group":[ + {"name":"Group Test","description":"Test Group","statusByEnv":[{"env":"default","value":true}]} + ]}}} + `; + +export const expected1073 = ` + {"data": + {"domain":{"name":"Domain","description":"Test Domain","statusByEnv":[{"env":"default","value":true}]}}} + `; + + export const expected108 = ` {"data": {"configuration":