diff --git a/src/server/__mocks__/mockDataFromES.js b/src/server/__mocks__/mockDataFromES.js
index 0f48dceb..98d484e9 100644
--- a/src/server/__mocks__/mockDataFromES.js
+++ b/src/server/__mocks__/mockDataFromES.js
@@ -8,9 +8,19 @@ import mockNestedTermsAndMissingAggs from './mockESData/mockNestedTermsAndMissin
import mockNestedAggs from './mockESData/mockNestedAggs';
const mockPing = () => {
- nock(config.esConfig.host)
- .head('/')
- .reply(200, 'hello');
+ nock(config.esConfig.host).head('/').reply(200, 'hello');
+};
+
+const mockRefresh = () => {
+ if (config.allowRefresh) {
+ nock(config.esConfig.host)
+ .post('/_refresh')
+ .reply(200, '[Server] guppy refreshed successfully');
+ } else {
+ nock(config.esConfig.host)
+ .post('/_refresh')
+ .reply(404, '[Server] guppy _refresh functionality is not enabled');
+ }
};
const mockResourcePath = () => {
@@ -35,12 +45,8 @@ const mockResourcePath = () => {
},
},
highlight: {
- pre_tags: [
- '',
- ],
- post_tags: [
- '',
- ],
+ pre_tags: [''],
+ post_tags: [''],
fields: {
'*.analyzed': {},
},
@@ -111,12 +117,8 @@ const mockResourcePath = () => {
},
},
highlight: {
- pre_tags: [
- '',
- ],
- post_tags: [
- '',
- ],
+ pre_tags: [''],
+ post_tags: [''],
fields: {
'*.analyzed': {},
},
@@ -172,12 +174,8 @@ const mockResourcePath = () => {
},
},
highlight: {
- pre_tags: [
- '',
- ],
- post_tags: [
- '',
- ],
+ pre_tags: [''],
+ post_tags: [''],
fields: {
'*.analyzed': {},
},
@@ -212,7 +210,8 @@ const mockArborist = () => {
.persist()
.post('/auth/mapping')
.reply(200, {
- 'internal-project-1': [ // accessible
+ 'internal-project-1': [
+ // accessible
{
service: '*',
method: 'create',
@@ -234,13 +233,15 @@ const mockArborist = () => {
method: 'update',
},
],
- 'internal-project-2': [ // accessible
+ 'internal-project-2': [
+ // accessible
{
service: '*',
method: 'read',
},
],
- 'internal-project-3': [ // not accessible since method does not match
+ 'internal-project-3': [
+ // not accessible since method does not match
{
service: '*',
method: 'create',
@@ -258,19 +259,22 @@ const mockArborist = () => {
method: 'update',
},
],
- 'internal-project-4': [ // accessible
+ 'internal-project-4': [
+ // accessible
{
service: '*',
method: '*',
},
],
- 'internal-project-5': [ // accessible
+ 'internal-project-5': [
+ // accessible
{
service: 'guppy',
method: '*',
},
],
- 'internal-project-6': [ // not accessible since service does not match
+ 'internal-project-6': [
+ // not accessible since service does not match
{
service: 'indexd',
method: '*',
@@ -376,7 +380,9 @@ const mockESMapping = () => {
};
const mockArrayConfig = () => {
- const arrayConfigQuery = { query: { ids: { values: ['gen3-dev-subject', 'gen3-dev-file'] } } };
+ const arrayConfigQuery = {
+ query: { ids: { values: ['gen3-dev-subject', 'gen3-dev-file'] } },
+ };
const fakeArrayConfig = {
hits: {
total: 1,
@@ -387,10 +393,7 @@ const mockArrayConfig = () => {
_id: 'gen3-dev-subject',
_score: 1.0,
_source: {
- array: [
- 'some_array_integer_field',
- 'some_array_string_field',
- ],
+ array: ['some_array_integer_field', 'some_array_string_field'],
},
},
],
@@ -405,6 +408,7 @@ const mockArrayConfig = () => {
const setup = () => {
mockArborist();
mockPing();
+ mockRefresh();
mockResourcePath();
mockESMapping();
mockArrayConfig();
diff --git a/src/server/__tests__/config.test.js b/src/server/__tests__/config.test.js
index be7f4199..4da4c934 100644
--- a/src/server/__tests__/config.test.js
+++ b/src/server/__tests__/config.test.js
@@ -34,7 +34,9 @@ describe('config', () => {
test('should show error if invalid tier access level', async () => {
process.env.TIER_ACCESS_LEVEL = 'invalid-level';
- expect(() => (require('../config'))).toThrow(new Error(`Invalid TIER_ACCESS_LEVEL "${process.env.TIER_ACCESS_LEVEL}"`));
+ expect(() => require('../config')).toThrow(
+ new Error(`Invalid TIER_ACCESS_LEVEL "${process.env.TIER_ACCESS_LEVEL}"`),
+ );
});
test('should show error if invalid tier access level in guppy block', async () => {
@@ -42,7 +44,9 @@ describe('config', () => {
const fileName = './testConfigFiles/test-invalid-index-scoped-tier-access.json';
process.env.GUPPY_CONFIG_FILEPATH = `${__dirname}/${fileName}`;
const invalidItemType = 'subject_private';
- expect(() => (require('../config'))).toThrow(new Error(`tier_access_level invalid for index ${invalidItemType}.`));
+ expect(() => require('../config')).toThrow(
+ new Error(`tier_access_level invalid for index ${invalidItemType}.`),
+ );
});
test('clears out site-wide default tiered-access setting if index-scoped levels set', async () => {
@@ -54,7 +58,9 @@ describe('config', () => {
const { indices } = require(fileName);
expect(config.tierAccessLevel).toBeUndefined();
expect(config.tierAccessLimit).toEqual(50);
- expect(JSON.stringify(config.esConfig.indices)).toEqual(JSON.stringify(indices));
+ expect(JSON.stringify(config.esConfig.indices)).toEqual(
+ JSON.stringify(indices),
+ );
});
/* --------------- For whitelist --------------- */
@@ -97,4 +103,11 @@ describe('config', () => {
expect(config.esConfig.aggregationIncludeMissingData).toBe(true);
expect(config.esConfig.missingDataAlias).toEqual(alias);
});
+
+ /* --------------- For _refresh testing --------------- */
+ test('could not access _refresh method if not in config', async () => {
+ process.env.GUPPY_CONFIG_FILEPATH = `${__dirname}/testConfigFiles/test-no-refresh-option-provided.json`;
+ const config = require('../config').default;
+ expect(config.allowRefresh).toBe(false);
+ });
});
diff --git a/src/server/__tests__/testConfigFiles/test-no-refresh-option-provided.json b/src/server/__tests__/testConfigFiles/test-no-refresh-option-provided.json
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/src/server/__tests__/testConfigFiles/test-no-refresh-option-provided.json
@@ -0,0 +1 @@
+{}
diff --git a/src/server/auth/__tests__/authHelper.test.js b/src/server/auth/__tests__/authHelper.test.js
index 41762d7a..c3062413 100644
--- a/src/server/auth/__tests__/authHelper.test.js
+++ b/src/server/auth/__tests__/authHelper.test.js
@@ -1,5 +1,5 @@
// eslint-disable-next-line
-import nock from 'nock'; // must import this to enable mock data by nock
+import nock from 'nock'; // must import this to enable mock data by nock
import getAuthHelperInstance from '../authHelper';
import esInstance from '../../es/index';
import setupMockDataEndpoint from '../../__mocks__/mockDataFromES';
@@ -12,6 +12,7 @@ setupMockDataEndpoint();
describe('AuthHelper', () => {
test('could create auth helper instance', async () => {
const authHelper = await getAuthHelperInstance('fake-jwt');
+ expect(authHelper.getCanRefresh()).toEqual(false);
expect(authHelper.getAccessibleResources()).toEqual(['internal-project-1', 'internal-project-2', 'internal-project-4', 'internal-project-5']);
expect(authHelper.getAccessibleResources()).not.toContain(['internal-project-3', 'internal-project-6']);
expect(authHelper.getUnaccessibleResources()).toEqual(['external-project-1', 'external-project-2']);
diff --git a/src/server/auth/arboristClient.js b/src/server/auth/arboristClient.js
index ac47fc92..957da124 100644
--- a/src/server/auth/arboristClient.js
+++ b/src/server/auth/arboristClient.js
@@ -8,10 +8,10 @@ class ArboristClient {
this.baseEndpoint = arboristEndpoint;
}
- listAuthorizedResources(jwt) {
+ listAuthMapping(jwt) {
// Make request to arborist for list of resources with access
const resourcesEndpoint = `${this.baseEndpoint}/auth/mapping`;
- log.debug('[ArboristClient] listAuthorizedResources jwt: ', jwt);
+ log.debug('[ArboristClient] listAuthMapping jwt: ', jwt);
const headers = (jwt) ? { Authorization: `bearer ${jwt}` } : {};
return fetch(
@@ -40,29 +40,6 @@ class ArboristClient {
log.error(err);
throw new CodedError(500, err);
},
- ).then(
- (result) => {
- const data = {
- resources: [],
- };
- Object.keys(result).forEach((key) => {
- // logic: you have access to a project if you have the following access:
- // method 'read' (or '*' - all methods) to service 'guppy' (or '*' - all services)
- // on the project resource.
- if (result[key] && result[key].some((x) => (
- (x.method === 'read' || x.method === '*')
- && (x.service === 'guppy' || x.service === '*')
- ))) {
- data.resources.push(key);
- }
- });
- log.debug('[ArboristClient] data: ', data);
- return data;
- },
- (err) => {
- log.error(err);
- throw new CodedError(500, err);
- },
);
}
}
diff --git a/src/server/auth/authHelper.js b/src/server/auth/authHelper.js
index b6aa798c..92f822d2 100644
--- a/src/server/auth/authHelper.js
+++ b/src/server/auth/authHelper.js
@@ -5,6 +5,7 @@ import {
getRequestResourceListFromFilter,
buildFilterWithResourceList,
getAccessibleResourcesFromArboristasync,
+ checkIfUserCanRefreshServer,
} from './utils';
import config from '../config';
@@ -15,8 +16,12 @@ export class AuthHelper {
async initialize() {
try {
- this._accessibleResourceList = await getAccessibleResourcesFromArboristasync(this._jwt);
+ const [accessibleResourceList, arboristResources] = await getAccessibleResourcesFromArboristasync(this._jwt);
+ this._accessibleResourceList = accessibleResourceList;
+ this._arboristResources = arboristResources;
log.debug('[AuthHelper] accessible resources:', this._accessibleResourceList);
+ this._canRefresh = await checkIfUserCanRefreshServer(this._arboristResources);
+ log.debug('[AuthHelper] can user refresh:', this._canRefresh);
const promiseList = [];
config.esConfig.indices.forEach(({ index, type }) => {
@@ -39,6 +44,10 @@ export class AuthHelper {
return this._accessibleResourceList;
}
+ getCanRefresh() {
+ return this._canRefresh;
+ }
+
getUnaccessibleResources() {
return this._unaccessibleResourceList;
}
diff --git a/src/server/auth/utils.js b/src/server/auth/utils.js
index f2965315..2e06980a 100644
--- a/src/server/auth/utils.js
+++ b/src/server/auth/utils.js
@@ -6,6 +6,23 @@ import arboristClient from './arboristClient';
import CodedError from '../utils/error';
import config from '../config';
+export const resourcePathsWithServiceMethodCombination = (userAuthMapping, services, methods = {}) => {
+ const data = {
+ resources: [],
+ };
+ Object.keys(userAuthMapping).forEach((key) => {
+ // logic: you have access to a project if you have
+ // access to any of the combinations made by the method and service lists
+ if (userAuthMapping[key] && userAuthMapping[key].some((x) => (
+ methods.includes(x.method)
+ && services.includes(x.service)
+ ))) {
+ data.resources.push(key);
+ }
+ });
+ return data;
+};
+
export const getAccessibleResourcesFromArboristasync = async (jwt) => {
let data;
if (config.internalLocalTest) {
@@ -16,7 +33,7 @@ export const getAccessibleResourcesFromArboristasync = async (jwt) => {
],
};
} else {
- data = await arboristClient.listAuthorizedResources(jwt);
+ data = await arboristClient.listAuthMapping(jwt);
}
log.debug('[authMiddleware] list resources: ', JSON.stringify(data, null, 4));
@@ -27,8 +44,35 @@ export const getAccessibleResourcesFromArboristasync = async (jwt) => {
}
throw new CodedError(data.error.code, data.error.message);
}
- const resources = data.resources ? _.uniq(data.resources) : [];
- return resources;
+
+ const read = resourcePathsWithServiceMethodCombination(data, ['guppy', '*'], ['read', '*']);
+ const readResources = read.resources ? _.uniq(read.resources) : [];
+ return [readResources, data];
+};
+
+export const checkIfUserCanRefreshServer = async (passedData) => {
+ let data = passedData;
+ if (config.internalLocalTest) {
+ data = {
+ resources: [ // these are just for testing
+ '/programs/DEV/projects/test',
+ '/programs/jnkns/projects/jenkins',
+ ],
+ };
+ }
+
+ log.debug('[authMiddleware] list resources: ', JSON.stringify(data, null, 4));
+ if (data && data.error) {
+ // if user is not in arborist db, assume has no access to any
+ if (data.error.code === 404) {
+ return false;
+ }
+ throw new CodedError(data.error.code, data.error.message);
+ }
+ const adminAccess = resourcePathsWithServiceMethodCombination(data, ['guppy'], ['admin_access', '*']);
+
+ // Only guppy_admin resource path can control guppy admin access
+ return adminAccess.resources ? adminAccess.resources.includes('/guppy_admin') : false;
};
export const getRequestResourceListFromFilter = async (
diff --git a/src/server/config.js b/src/server/config.js
index 894e2a10..3aceeb5e 100644
--- a/src/server/config.js
+++ b/src/server/config.js
@@ -5,7 +5,11 @@ let inputConfig = {};
if (process.env.GUPPY_CONFIG_FILEPATH) {
const configFilepath = process.env.GUPPY_CONFIG_FILEPATH;
inputConfig = JSON.parse(readFileSync(configFilepath).toString());
- log.info('[config] read guppy config from', configFilepath, JSON.stringify(inputConfig, null, 4));
+ log.info(
+ '[config] read guppy config from',
+ configFilepath,
+ JSON.stringify(inputConfig, null, 4),
+ );
}
const config = {
@@ -21,9 +25,14 @@ const config = {
type: 'file',
},
],
- configIndex: (inputConfig.indices) ? inputConfig.config_index : 'gen3-dev-config',
+ configIndex: inputConfig.indices
+ ? inputConfig.config_index
+ : 'gen3-dev-config',
authFilterField: inputConfig.auth_filter_field || 'auth_resource_path',
- aggregationIncludeMissingData: typeof inputConfig.aggs_include_missing_data === 'undefined' ? true : inputConfig.aggs_include_missing_data,
+ aggregationIncludeMissingData:
+ typeof inputConfig.aggs_include_missing_data === 'undefined'
+ ? true
+ : inputConfig.aggs_include_missing_data,
missingDataAlias: inputConfig.missing_data_alias || 'no data',
},
port: 80,
@@ -31,13 +40,23 @@ const config = {
arboristEndpoint: 'http://arborist-service',
tierAccessLevel: 'private',
tierAccessLimit: 1000,
- tierAccessSensitiveRecordExclusionField: inputConfig.tier_access_sensitive_record_exclusion_field,
+ tierAccessSensitiveRecordExclusionField:
+ inputConfig.tier_access_sensitive_record_exclusion_field,
logLevel: inputConfig.log_level || 'INFO',
- enableEncryptWhiteList: typeof inputConfig.enable_encrypt_whitelist === 'undefined' ? false : inputConfig.enable_encrypt_whitelist,
- encryptWhitelist: inputConfig.encrypt_whitelist || ['__missing__', 'unknown', 'not reported', 'no data'],
+ enableEncryptWhiteList:
+ typeof inputConfig.enable_encrypt_whitelist === 'undefined'
+ ? false
+ : inputConfig.enable_encrypt_whitelist,
+ encryptWhitelist: inputConfig.encrypt_whitelist || [
+ '__missing__',
+ 'unknown',
+ 'not reported',
+ 'no data',
+ ],
analyzedTextFieldSuffix: '.analyzed',
matchedTextHighlightTagName: 'em',
allowedMinimumSearchLen: 2,
+ allowRefresh: inputConfig.allowRefresh || false,
};
if (process.env.GEN3_ES_ENDPOINT) {
@@ -59,7 +78,9 @@ const allowedTierAccessLevels = ['private', 'regular', 'libre'];
if (process.env.TIER_ACCESS_LEVEL) {
if (!allowedTierAccessLevels.includes(process.env.TIER_ACCESS_LEVEL)) {
- throw new Error(`Invalid TIER_ACCESS_LEVEL "${process.env.TIER_ACCESS_LEVEL}"`);
+ throw new Error(
+ `Invalid TIER_ACCESS_LEVEL "${process.env.TIER_ACCESS_LEVEL}"`,
+ );
}
config.tierAccessLevel = process.env.TIER_ACCESS_LEVEL;
}
@@ -86,9 +107,14 @@ if (process.env.ANALYZED_TEXT_FIELD_SUFFIX) {
let allIndicesHaveTierAccessSettings = true;
config.esConfig.indices.forEach((item) => {
if (!item.tier_access_level && !config.tierAccessLevel) {
- throw new Error('Either set all index-scoped tiered-access levels or a site-wide tiered-access level.');
+ throw new Error(
+ 'Either set all index-scoped tiered-access levels or a site-wide tiered-access level.',
+ );
}
- if (item.tier_access_level && !allowedTierAccessLevels.includes(item.tier_access_level)) {
+ if (
+ item.tier_access_level
+ && !allowedTierAccessLevels.includes(item.tier_access_level)
+ ) {
throw new Error(`tier_access_level invalid for index ${item.type}.`);
}
if (!item.tier_access_level) {
@@ -112,6 +138,9 @@ if (config.enableEncryptWhiteList) {
}
log.setLogLevel(config.logLevel);
-log.info('[config] starting server using config', JSON.stringify(config, null, 4));
+log.info(
+ '[config] starting server using config',
+ JSON.stringify(config, null, 4),
+);
export default config;
diff --git a/src/server/es/index.js b/src/server/es/index.js
index 4e708744..815c6c08 100644
--- a/src/server/es/index.js
+++ b/src/server/es/index.js
@@ -54,7 +54,7 @@ class ES {
};
validatedQueryBody.track_total_hits = true;
- var start = Date.now();
+ const start = Date.now();
return this.client.search({
index: esIndex,
body: validatedQueryBody,
@@ -62,10 +62,10 @@ class ES {
log.error(`[ES.query] error during querying: ${err.message}`);
throw new Error(err.message);
}).finally(() => {
- var end = Date.now();
- var durationInMS = end - start;
+ const end = Date.now();
+ const durationInMS = end - start;
- log.info('[ES.query] DurationInMS:' + durationInMS + '. index, type, query body: ', esIndex, esType, JSON.stringify(validatedQueryBody));
+ log.info(`[ES.query] DurationInMS:${durationInMS}. index, type, query body: `, esIndex, esType, JSON.stringify(validatedQueryBody));
});
}
diff --git a/src/server/server.js b/src/server/server.js
index 25c22266..5e503110 100644
--- a/src/server/server.js
+++ b/src/server/server.js
@@ -19,6 +19,7 @@ import downloadRouter from './download';
import CodedError from './utils/error';
import { statusRouter, versionRouter } from './endpoints';
+let server;
const app = express();
app.use(cors());
app.use(helmet());
@@ -29,12 +30,9 @@ const startServer = async () => {
const typeDefs = getSchema(config.esConfig, esInstance);
const resolvers = getResolver(config.esConfig, esInstance);
const schema = makeExecutableSchema({ typeDefs, resolvers });
- const schemaWithMiddleware = applyMiddleware(
- schema,
- ...middlewares,
- );
- // create graphql server instance
- const server = new ApolloServer({
+ const schemaWithMiddleware = applyMiddleware(schema, ...middlewares);
+ // create graphql server instance
+ server = new ApolloServer({
mocks: false,
schema: schemaWithMiddleware,
validationRules: [depthLimit(10)],
@@ -57,43 +55,79 @@ const startServer = async () => {
path: config.path,
}),
);
+ log.info(`[Server] guppy listening on port ${config.port}!`);
+};
+
+const initializeAndStartServer = async () => {
+ await esInstance.initialize();
+ await startServer();
+};
- // simple health check endpoint
- // eslint-disable-next-line no-unused-vars
- app.get('/_status', statusRouter, (err, req, res, next) => {
- if (err instanceof CodedError) {
- // deepcode ignore ServerLeak: no important information exists in error
- res.status(err.code).send(err.msg);
+const refreshRouter = async (req, res, next) => {
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
+ try {
+ if (config.allowRefresh === false) {
+ const disabledRefresh = new CodedError(404, '[Refresh] guppy _refresh functionality is not enabled');
+ throw disabledRefresh;
} else {
- // deepcode ignore ServerLeak: no important information exists in error
- res.status(500).send(err);
+ log.debug('[Refresh] ', JSON.stringify(req.body, null, 4));
+ const jwt = headerParser.parseJWT(req);
+ if (!jwt) {
+ const noJwtError = new CodedError(401, '[Refresh] no JWT user token provided to _refresh function');
+ throw noJwtError;
+ }
+ const authHelper = await getAuthHelperInstance(jwt);
+ if (authHelper._canRefresh === undefined || authHelper._canRefresh === false) {
+ const noPermsUser = new CodedError(401, '[Refresh] User cannot refresh Guppy without a valid token that has admin_access method on guppy service for resource path /guppy_admin');
+ throw noPermsUser;
+ }
+ await server.stop();
+ await initializeAndStartServer();
}
- });
+ res.send('[Refresh] guppy refreshed successfully');
+ } catch (err) {
+ log.error(err);
+ next(err);
+ }
+ return 0;
+};
- // eslint-disable-next-line no-unused-vars
- app.get('/_version', versionRouter);
+// simple health check endpoint
+// eslint-disable-next-line no-unused-vars
+app.get('/_status', statusRouter, (req, res, err, next) => {
+ if (err instanceof CodedError) {
+ // deepcode ignore ServerLeak: no important information exists in error
+ res.status(err.code).send(err.msg);
+ } else {
+ // deepcode ignore ServerLeak: no important information exists in error
+ res.status(500).send(err);
+ }
+});
- // download endpoint for fetching data directly from es
- app.post(
- '/download',
- downloadRouter,
- (err, req, res, next) => { // eslint-disable-line no-unused-vars
- if (err instanceof CodedError) {
- // deepcode ignore ServerLeak: no important information exists in error
- res.status(err.code).send(err.msg);
- } else {
- // deepcode ignore ServerLeak: no important information exists in error
- res.status(500).send(err);
- }
- },
- );
+app.get('/_version', versionRouter);
- app.listen(config.port, () => {
- log.info(`[Server] guppy listening on port ${config.port}!`);
- });
-};
+// download endpoint for fetching data directly from es
+// eslint-disable-next-line no-unused-vars
+app.post('/download', downloadRouter, (err, req, res, next) => {
+ if (err instanceof CodedError) {
+ // deepcode ignore ServerLeak: no important information exists in error
+ res.status(err.code).send(err.msg);
+ } else {
+ // deepcode ignore ServerLeak: no important information exists in error
+ res.status(500).send(err);
+ }
+});
+
+// eslint-disable-next-line no-unused-vars
+app.post('/_refresh', refreshRouter, (err, req, res, next) => {
+ if (err instanceof CodedError) {
+ res.status(err.code).send(err.msg);
+ } else {
+ res.status(500).send(err);
+ }
+});
// need to connect to ES and initialize before setting up a server
-esInstance.initialize().then(() => {
- startServer();
+app.listen(config.port, async () => {
+ await initializeAndStartServer();
});