From 4edddd1e5a2850f520c15b73a191e7087e3b42d3 Mon Sep 17 00:00:00 2001 From: Rohan Port <59544282+rohan-bes@users.noreply.github.com> Date: Fri, 16 Sep 2022 17:07:13 +1000 Subject: [PATCH] RN-632: Show Data-Tables in the Admin-Panel (#4175) --- .../src/pages/resources/DataTablesPage.js | 55 +++++++++++++++++ .../admin-panel/src/pages/resources/index.js | 1 + packages/admin-panel/src/routes.js | 6 ++ .../src/apiV2/dataTables/GETDataTables.js | 30 +++++++++ .../dataTables/assertDataTablePermissions.js | 61 +++++++++++++++++++ .../src/apiV2/dataTables/index.js | 6 ++ packages/central-server/src/apiV2/index.js | 2 + 7 files changed, 161 insertions(+) create mode 100644 packages/admin-panel/src/pages/resources/DataTablesPage.js create mode 100644 packages/central-server/src/apiV2/dataTables/GETDataTables.js create mode 100644 packages/central-server/src/apiV2/dataTables/assertDataTablePermissions.js create mode 100644 packages/central-server/src/apiV2/dataTables/index.js diff --git a/packages/admin-panel/src/pages/resources/DataTablesPage.js b/packages/admin-panel/src/pages/resources/DataTablesPage.js new file mode 100644 index 0000000000..5b69342004 --- /dev/null +++ b/packages/admin-panel/src/pages/resources/DataTablesPage.js @@ -0,0 +1,55 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { ResourcePage } from './ResourcePage'; + +const DATA_TABLES_ENDPOINT = 'dataTables'; + +const FIELDS = [ + { + Header: 'Code', + source: 'code', + type: 'tooltip', + }, + { + Header: 'Description', + source: 'description', + }, + { + Header: 'Type', + source: 'type', + }, + { + Header: 'Config', + source: 'config', + type: 'jsonTooltip', + editConfig: { type: 'jsonEditor' }, + }, + { + Header: 'Permission groups', + source: 'permission_groups', + type: 'tooltip', + editConfig: { + type: 'jsonArray', + }, + }, +]; + +const COLUMNS = [...FIELDS]; + +export const DataTablesPage = ({ getHeaderEl }) => ( + +); + +DataTablesPage.propTypes = { + getHeaderEl: PropTypes.func.isRequired, +}; diff --git a/packages/admin-panel/src/pages/resources/index.js b/packages/admin-panel/src/pages/resources/index.js index 15705a65e4..6183a59f37 100644 --- a/packages/admin-panel/src/pages/resources/index.js +++ b/packages/admin-panel/src/pages/resources/index.js @@ -20,6 +20,7 @@ export { UsersPage } from './UsersPage'; export { SocialFeedPage } from './SocialFeedPage'; export { DisasterResponsePage } from './DisasterResponsePage'; export { DataGroupsPage, DataElementsPage } from './DataSourcesPage'; +export { DataTablesPage } from './DataTablesPage'; export { AccessRequestsPage } from './AccessRequestsPage'; export { MapOverlaysPage } from './MapOverlaysPage'; export { MapOverlayGroupsPage } from './MapOverlayGroupsPage'; diff --git a/packages/admin-panel/src/routes.js b/packages/admin-panel/src/routes.js index 5022f693c6..7e370031c6 100644 --- a/packages/admin-panel/src/routes.js +++ b/packages/admin-panel/src/routes.js @@ -31,6 +31,7 @@ import { DataGroupsPage, ProjectsPage, SyncGroupsPage, + DataTablesPage, } from './pages/resources'; export const ROUTES = [ @@ -121,6 +122,11 @@ export const ROUTES = [ to: '/indicators', component: IndicatorsPage, }, + { + label: 'Data-Tables', + to: '/dataTables', + component: DataTablesPage, + }, { label: 'Social Feed', to: '/social-feed', diff --git a/packages/central-server/src/apiV2/dataTables/GETDataTables.js b/packages/central-server/src/apiV2/dataTables/GETDataTables.js new file mode 100644 index 0000000000..06e1676061 --- /dev/null +++ b/packages/central-server/src/apiV2/dataTables/GETDataTables.js @@ -0,0 +1,30 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd + */ + +import { GETHandler } from '../GETHandler'; +import { + assertDataTableGETPermissions, + createDataTableDBFilter, +} from './assertDataTablePermissions'; +import { assertAnyPermissions, assertBESAdminAccess } from '../../permissions'; + +export class GETDataTables extends GETHandler { + permissionsFilteredInternally = true; + + async findSingleRecord(dataTableId, options) { + const dataGroupPermissionChecker = accessPolicy => + assertDataTableGETPermissions(accessPolicy, this.models, dataTableId); + await this.assertPermissions( + assertAnyPermissions([assertBESAdminAccess, dataGroupPermissionChecker]), + ); + + return super.findSingleRecord(dataTableId, options); + } + + async getPermissionsFilter(criteria, options) { + const dbConditions = await createDataTableDBFilter(this.accessPolicy, this.models, criteria); + return { dbConditions, dbOptions: options }; + } +} diff --git a/packages/central-server/src/apiV2/dataTables/assertDataTablePermissions.js b/packages/central-server/src/apiV2/dataTables/assertDataTablePermissions.js new file mode 100644 index 0000000000..f8fa3ad0f9 --- /dev/null +++ b/packages/central-server/src/apiV2/dataTables/assertDataTablePermissions.js @@ -0,0 +1,61 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd + */ + +import { hasBESAdminAccess } from '../../permissions'; + +const getPermissionListWithWildcard = async accessPolicy => { + const userPermissionGroups = accessPolicy.getPermissionGroups(); + return ['*', ...userPermissionGroups]; +}; + +export const assertDataTableGETPermissions = async (accessPolicy, models, dataTableId) => { + // User requires access to any permission group + if (await assertDataTablePermissions(accessPolicy, models, dataTableId, 'some')) { + return true; + } + throw new Error('You do not have permission to view this data-table'); +}; + +export const assertDataTableEditPermissions = async (accessPolicy, models, dataTableId) => { + // User requires access to all permission groups + if (await assertDataTablePermissions(accessPolicy, models, dataTableId, 'every')) { + return true; + } + throw new Error( + 'You require access to all of a data-tables permission groups to perform this action', + ); +}; + +const assertDataTablePermissions = async (accessPolicy, models, dataTableId, test) => { + const dataTable = await models.dataTable.findById(dataTableId); + if (!dataTable) { + throw new Error(`No data-table exists with id ${dataTableId}`); + } + const userPermissions = await getPermissionListWithWildcard(accessPolicy); + // Test if user has access to any or all permission groups against the data-table + if ( + dataTable.permission_groups.length <= 0 || + dataTable.permission_groups[test](code => userPermissions.includes(code)) + ) { + return true; + } + return false; +}; + +export const createDataTableDBFilter = async (accessPolicy, models, criteria) => { + if (hasBESAdminAccess(accessPolicy)) { + return criteria; + } + const dbConditions = { ...criteria }; + const userPermissions = await getPermissionListWithWildcard(accessPolicy); + + // Permission groups on the data-table overlap with our permission groups + // Wildcard is added to our list so it will be included + dbConditions.permission_groups = { + comparator: '&&', // Checks two array have any elements in common + comparisonValue: userPermissions, + }; + return dbConditions; +}; diff --git a/packages/central-server/src/apiV2/dataTables/index.js b/packages/central-server/src/apiV2/dataTables/index.js new file mode 100644 index 0000000000..3fd03e0575 --- /dev/null +++ b/packages/central-server/src/apiV2/dataTables/index.js @@ -0,0 +1,6 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + */ + +export { GETDataTables } from './GETDataTables'; diff --git a/packages/central-server/src/apiV2/index.js b/packages/central-server/src/apiV2/index.js index 064dee3ac6..bdcb8dc0b0 100644 --- a/packages/central-server/src/apiV2/index.js +++ b/packages/central-server/src/apiV2/index.js @@ -30,6 +30,7 @@ import { GETClinics } from './GETClinics'; import { GETDisasters } from './GETDisasters'; import { GETDataElements, EditDataElements, DeleteDataElements } from './dataElements'; import { GETDataGroups, EditDataGroups, DeleteDataGroups } from './dataGroups'; +import { GETDataTables } from './dataTables'; import { GETEntities } from './GETEntities'; import { GETFeedItems } from './GETFeedItems'; import { GETGeographicalAreas } from './GETGeographicalAreas'; @@ -212,6 +213,7 @@ apiV2.get('/accessRequests/:recordId?', useRouteHandler(GETAccessRequests)); apiV2.get('/dataElements/:recordId?', useRouteHandler(GETDataElements)); apiV2.get('/dataGroups/:parentRecordId/dataElements', useRouteHandler(GETDataElements)); apiV2.get('/dataGroups/:recordId?', useRouteHandler(GETDataGroups)); +apiV2.get('/dataTables/:recordId?', useRouteHandler(GETDataTables)); apiV2.get('/dataElementDataGroups', useRouteHandler(GETDataElementDataGroups)); apiV2.get('/entities/:recordId?', useRouteHandler(GETEntities)); apiV2.get('/entities/:parentRecordId/surveyResponses', useRouteHandler(GETSurveyResponses));