Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/refresh #273

Merged
merged 19 commits into from
Oct 29, 2024
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
68 changes: 36 additions & 32 deletions src/server/__mocks__/mockDataFromES.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -35,12 +45,8 @@ const mockResourcePath = () => {
},
},
highlight: {
pre_tags: [
'<em>',
],
post_tags: [
'</em>',
],
pre_tags: ['<em>'],
post_tags: ['</em>'],
fields: {
'*.analyzed': {},
},
Expand Down Expand Up @@ -111,12 +117,8 @@ const mockResourcePath = () => {
},
},
highlight: {
pre_tags: [
'<em>',
],
post_tags: [
'</em>',
],
pre_tags: ['<em>'],
post_tags: ['</em>'],
fields: {
'*.analyzed': {},
},
Expand Down Expand Up @@ -172,12 +174,8 @@ const mockResourcePath = () => {
},
},
highlight: {
pre_tags: [
'<em>',
],
post_tags: [
'</em>',
],
pre_tags: ['<em>'],
post_tags: ['</em>'],
fields: {
'*.analyzed': {},
},
Expand Down Expand Up @@ -212,7 +210,8 @@ const mockArborist = () => {
.persist()
.post('/auth/mapping')
.reply(200, {
'internal-project-1': [ // accessible
'internal-project-1': [
// accessible
{
service: '*',
method: 'create',
Expand All @@ -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',
Expand All @@ -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: '*',
Expand Down Expand Up @@ -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,
Expand All @@ -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'],
},
},
],
Expand All @@ -405,6 +408,7 @@ const mockArrayConfig = () => {
const setup = () => {
mockArborist();
mockPing();
mockRefresh();
mockResourcePath();
mockESMapping();
mockArrayConfig();
Expand Down
19 changes: 16 additions & 3 deletions src/server/__tests__/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,19 @@ 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 () => {
process.env.TIER_ACCESS_LEVEL = null;
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 () => {
Expand All @@ -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 --------------- */
Expand Down Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
3 changes: 2 additions & 1 deletion src/server/auth/__tests__/authHelper.test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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']);
Expand Down
27 changes: 2 additions & 25 deletions src/server/auth/arboristClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
},
);
}
}
Expand Down
11 changes: 10 additions & 1 deletion src/server/auth/authHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getRequestResourceListFromFilter,
buildFilterWithResourceList,
getAccessibleResourcesFromArboristasync,
checkIfUserCanRefreshServer,
} from './utils';
import config from '../config';

Expand All @@ -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 }) => {
Expand All @@ -39,6 +44,10 @@ export class AuthHelper {
return this._accessibleResourceList;
}

getCanRefresh() {
return this._canRefresh;
}

getUnaccessibleResources() {
return this._unaccessibleResourceList;
}
Expand Down
50 changes: 47 additions & 3 deletions src/server/auth/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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));
Expand All @@ -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 = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider fix the failed es-lint check here

Copy link
Contributor Author

@matthewpeterkort matthewpeterkort Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

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 (
Expand Down
Loading
Loading