Skip to content

Commit

Permalink
merge: merge branch 'THU/fix_dataset_parts_rbac_PROD-12954'
Browse files Browse the repository at this point in the history
  • Loading branch information
csm-thu committed Jan 19, 2024
2 parents 852509a + 7b9ce68 commit 5ca2595
Show file tree
Hide file tree
Showing 18 changed files with 384 additions and 62 deletions.
8 changes: 8 additions & 0 deletions cypress/commons/actions/generic/ScenarioParameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,21 @@ function launch(options) {
// - id (optional): id of the dataset to create
// - onDatasetCreation (optional): validation function to run on the dataset creation request
// - onDatasetUpdate (optional): validation function to run on the dataset update request
// - securityChanges (optional): object containing only the differences of security that are applied to the created
// dataset. This object defines how queries must be intercepted when stubbing is enabled. The expected format
// is the same as the security objects of API resources, with an additional field "type" with one of the values
// "post", "patch", or "delete". Example:
// {default: "viewer", accessControlList: [{id: "john.doe@example.com", role: "admin", type: "patch"}]}
function save(options = {}) {
const aliases = [];
// Events array is reversed to make tests easier to write and still match the order of cypress interceptions
// (c.f. cy.intercept doc: "cy.intercept() routes are matched in reverse order of definition")
options?.datasetsEvents?.reverse()?.forEach((datasetEvent) => {
aliases.push(api.interceptCreateDataset({ id: datasetEvent.id, validateRequest: datasetEvent.onDatasetCreation }));
aliases.push(api.interceptUpdateDataset({ id: datasetEvent.id, validateRequest: datasetEvent.onDatasetUpdate }));
aliases.push(
...api.interceptUpdateDatasetSecurity({ id: datasetEvent.id, securityChanges: datasetEvent.securityChanges })
);
aliases.push(api.interceptUploadWorkspaceFile());
});

Expand Down
6 changes: 6 additions & 0 deletions cypress/commons/constants/generic/TestConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export const API_ENDPOINT = {
SCENARIO_RUN_STATUS: URL_ROOT + '/.*/scenarioruns/(sr-[\\w]+)/status',
DATASETS: URL_ROOT + '/.*/datasets',
DATASET: URL_ROOT + '/.*/datasets/(d-[\\w]+)',
DATASET_DEFAULT_SECURITY: URL_ROOT + '/.*/datasets/((d|D)-[\\w]+)/security/default',
DATASET_SECURITY_ACL: URL_ROOT + '/.*/datasets/((d|D)-[\\w]+)/security/access',
DATASET_SECURITY_USER_ACCESS: URL_ROOT + '/.*/datasets/((d|D)-[\\w]+)/security/access/(.*)',
WORKSPACES: URL_ROOT + '/.*/workspaces',
WORKSPACE: URL_ROOT + '/.*/workspaces/((w|W)-[\\w]+)',
SOLUTIONS: URL_ROOT + '/.*/solutions',
Expand Down Expand Up @@ -156,6 +159,9 @@ export const API_REGEX = {
SCENARIO_RUN_STATUS: new RegExp('^' + API_ENDPOINT.SCENARIO_RUN_STATUS),
DATASETS: new RegExp('^' + API_ENDPOINT.DATASETS + '$'),
DATASET: new RegExp('^' + API_ENDPOINT.DATASET + '$'),
DATASET_DEFAULT_SECURITY: new RegExp('^' + API_ENDPOINT.DATASET_DEFAULT_SECURITY + '$'),
DATASET_SECURITY_ACL: new RegExp('^' + API_ENDPOINT.DATASET_SECURITY_ACL + '$'),
DATASET_SECURITY_USER_ACCESS: new RegExp('^' + API_ENDPOINT.DATASET_SECURITY_USER_ACCESS + '$'),
WORKSPACE: new RegExp('^' + API_ENDPOINT.WORKSPACE + '$'),
WORKSPACES: new RegExp('^' + API_ENDPOINT.WORKSPACES + '$'),
SOLUTION: new RegExp('^' + API_ENDPOINT.SOLUTION),
Expand Down
27 changes: 27 additions & 0 deletions cypress/commons/services/stubbing.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,33 @@ class Stubbing {
addDataset = (newDataset) => this._addResource('datasets', newDataset);
getDatasetById = (datasetId) => this._getResourceById('datasets', datasetId);
patchDataset = (datasetId, datasetPatch) => this._patchResourceById('datasets', datasetId, datasetPatch);
patchDatasetSecurity = (datasetId, defaultRole, accessControlList) =>
this.patchDataset(datasetId, { security: { default: defaultRole, accessControlList: accessControlList } });

patchDatasetDefaultSecurity = (datasetId, newDefaultSecurity) => {
const dataset = this.getDatasetById(datasetId);
this.patchDatasetSecurity(datasetId, newDefaultSecurity, dataset.security?.accessControlList ?? []);
};

addDatasetAccessControl = (datasetId, newACLSecurityItem) => {
const dataset = this.getDatasetById(datasetId);
const newACL = [...dataset.security.accessControlList, newACLSecurityItem];
this.patchDatasetSecurity(datasetId, dataset.security?.default, newACL);
};

updateDatasetAccessControl = (datasetId, aclEntryToUpdate) => {
const dataset = this.getDatasetById(datasetId);
const newACL = (dataset.security?.accessControlList ?? []).map((entry) =>
entry.id === aclEntryToUpdate.id ? aclEntryToUpdate : entry
);
this.patchDatasetSecurity(datasetId, dataset.security?.default, newACL);
};

removeDatasetAccessControl = (datasetId, idToRemove) => {
const dataset = this.getDatasetById(datasetId);
const newACL = (dataset.security?.accessControlList ?? []).filter((entry) => entry.id !== idToRemove);
this.patchDatasetSecurity(datasetId, dataset.security?.default, newACL);
};

getSolutions = () => this._getResources('solutions');
setSolutions = (newSolutions) => this._setResources('solutions', newSolutions);
Expand Down
76 changes: 76 additions & 0 deletions cypress/commons/utils/apiUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,77 @@ const interceptUpdateDataset = (options) => {
return alias;
};

// Parameters:
// - options: dict with properties:
// - id (optional): id of the dataset to create
// - securityChanges (optional): object containing only the differences of security that are applied to the created
// dataset. This object defines how queries must be intercepted when stubbing is enabled. The expected format
// is the same as the security objects of API resources, with an additional field "type" with one of the values
// "post", "patch", or "delete". Example:
// {default: "viewer", accessControlList: [{id: "john.doe@example.com", role: "admin", type: "patch"}]}
const interceptUpdateDatasetSecurity = ({ datasetId, securityChanges }) => {
const aliases = [];
if (securityChanges?.default) aliases.push(interceptSetDatasetDefaultSecurity(datasetId, securityChanges?.default));
securityChanges?.accessControlList?.forEach((entry) => {
const type = entry.type;
const aclEntry = { ...entry, type: undefined };

if (type === 'post') aliases.push(interceptAddDatasetAccessControl(datasetId, aclEntry));
else if (type === 'patch') aliases.push(interceptUpdateDatasetAccessControl(datasetId, aclEntry));
else if (type === 'delete') aliases.push(interceptRemoveDatasetAccessControl(datasetId, aclEntry));
else console.warn(`Unknown ACL entry type "${type}" in interceptUpdateDatasetSecurity`);
});
return aliases;
};

const interceptAddDatasetAccessControl = (optionalDatasetId, expectedNewEntry) => {
const alias = forgeAlias('reqAddDatasetAccessControl');
cy.intercept({ method: 'POST', url: API_REGEX.DATASET_SECURITY_ACL, times: 1 }, (req) => {
const datasetId = optionalDatasetId ?? req.url.match(API_REGEX.DATASET_SECURITY_ACL)[1];
const newEntry = req.body;
if (expectedNewEntry) expect(newEntry).to.deep.equal(expectedNewEntry);
if (stub.isEnabledFor('GET_DATASETS')) stub.addDatasetAccessControl(datasetId, newEntry);
if (stub.isEnabledFor('UPDATE_DATASET')) req.reply(newEntry);
}).as(alias);
return alias;
};

const interceptUpdateDatasetAccessControl = (optionalDatasetId, expectedUpdatedEntry) => {
const alias = forgeAlias('reqUpdateDatasetAccessControl');
cy.intercept({ method: 'PATCH', url: API_REGEX.DATASET_SECURITY_ACL, times: 1 }, (req) => {
const datasetId = optionalDatasetId ?? req.url.match(API_REGEX.DATASET_SECURITY_USER_ACCESS)[1];
const newAccess = { id: req.url.match(API_REGEX.DATASET_SECURITY_USER_ACCESS)[2], role: req.body.role };
if (expectedUpdatedEntry) expect(newAccess).to.deep.equal(expectedUpdatedEntry);
if (stub.isEnabledFor('GET_DATASETS')) stub.updateDatasetAccessControl(datasetId, newAccess);
if (stub.isEnabledFor('UPDATE_DATASET')) req.reply(newAccess);
}).as(alias);
return alias;
};

const interceptRemoveDatasetAccessControl = (optionalDatasetId, expectedIdToRemove) => {
const alias = forgeAlias('reqRemoveDatasetAccessControl');
cy.intercept({ method: 'DELETE', url: API_REGEX.DATASET_SECURITY_ACL, times: 1 }, (req) => {
const datasetId = optionalDatasetId ?? req.url.match(API_REGEX.DATASET_SECURITY_USER_ACCESS)[1];
const userId = req.url.match(API_REGEX.DATASET_SECURITY_USER_ACCESS)[2];
if (expectedIdToRemove) expect(userId).to.equal(expectedIdToRemove);
if (stub.isEnabledFor('GET_DATASETS')) stub.removeDatasetAccessControl(datasetId, userId);
if (stub.isEnabledFor('UPDATE_DATASET')) req.reply();
}).as(alias);
return alias;
};

const interceptSetDatasetDefaultSecurity = (optionalDatasetId, expectedDefaultSecurity) => {
const alias = forgeAlias('reqSetDatasetDefaultSecurity');
cy.intercept({ method: 'POST', url: API_REGEX.DATASET_DEFAULT_SECURITY, times: 1 }, (req) => {
const datasetId = optionalDatasetId ?? req.url.match(API_REGEX.DATASET_DEFAULT_SECURITY)[1];
const newDefaultSecurity = req.body.role;
if (expectedDefaultSecurity) expect(newDefaultSecurity).to.deep.equal(expectedDefaultSecurity);
if (stub.isEnabledFor('GET_DATASETS')) stub.patchDatasetDefaultSecurity(datasetId, newDefaultSecurity);
if (stub.isEnabledFor('UPDATE_DATASET')) req.reply(newDefaultSecurity);
}).as(alias);
return alias;
};

const interceptDownloadWorkspaceFile = () => {
const alias = forgeAlias('reqDownloadWorkspaceFile');
cy.intercept({ method: 'GET', url: API_REGEX.FILE_DOWNLOAD, times: 1 }, (req) => {
Expand Down Expand Up @@ -466,6 +537,11 @@ export const apiUtils = {
interceptGetDataset,
interceptCreateDataset,
interceptUpdateDataset,
interceptUpdateDatasetSecurity,
interceptAddDatasetAccessControl,
interceptUpdateDatasetAccessControl,
interceptRemoveDatasetAccessControl,
interceptSetDatasetDefaultSecurity,
interceptDownloadWorkspaceFile,
interceptUploadWorkspaceFile,
interceptGetOrganizationPermissions,
Expand Down
17 changes: 14 additions & 3 deletions cypress/e2e/brewery/FileParametersNames.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ describe('Management of file names for scenario parameters of type file', () =>
FileParameters.getFileName(getFileWithRenaming()).should('have.text', 'CSV file');

ScenarioParameters.save({
datasetsEvents: [{ id: 'd-stbddst1' }, { id: 'd-stbddst2' }], // Force ids for stubbing of datasets creation
datasetsEvents: [
{ id: 'd-stbddst1', securityChanges: { default: 'admin' } },
{ id: 'd-stbddst2', securityChanges: { default: 'admin' } },
], // Force ids for stubbing of datasets creation
updateOptions: {
validateRequest: (req) => {
const values = req.body.parametersValues;
Expand Down Expand Up @@ -84,10 +87,12 @@ describe('Management of file names for scenario parameters of type file', () =>
datasetsEvents: [
{
id: 'd-stbddst3',
securityChanges: { default: 'admin' },
onDatasetUpdate: (req) => expect(getFilePathFromDataset(req.body)).to.have.string(DUMMY_JSON),
},
{
id: 'd-stbddst4',
securityChanges: { default: 'admin' },
onDatasetUpdate: (req) => expect(getFilePathFromDataset(req.body)).to.have.string('file_with_renaming.json'),
},
],
Expand All @@ -108,7 +113,10 @@ describe('Management of file names for scenario parameters of type file', () =>
TableParameters.getRows(getTableNoRenaming()).should('have.length', 2);
TableParameters.getRows(getTableWithRenaming()).should('have.length', 2);
ScenarioParameters.save({
datasetsEvents: [{ id: 'd-stbddst5' }, { id: 'd-stbddst6' }],
datasetsEvents: [
{ id: 'd-stbddst5', securityChanges: { default: 'admin' } },
{ id: 'd-stbddst6', securityChanges: { default: 'admin' } },
],
updateOptions: {
validateRequest: (req) => {
const values = req.body.parametersValues;
Expand All @@ -126,7 +134,10 @@ describe('Management of file names for scenario parameters of type file', () =>
TableParameters.getRows(getTableNoRenaming()).should('have.length', 3);
TableParameters.getRows(getTableWithRenaming()).should('have.length', 3);
ScenarioParameters.save({
datasetsEvents: [{ id: 'd-stbddst7' }, { id: 'd-stbddst8' }],
datasetsEvents: [
{ id: 'd-stbddst7', securityChanges: { default: 'admin' } },
{ id: 'd-stbddst8', securityChanges: { default: 'admin' } },
],
updateOptions: {
validateRequest: (req) => {
const values = req.body.parametersValues;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "6.0.0-dev",
"private": true,
"dependencies": {
"@cosmotech/api-ts": "^3.0.6-dev",
"@cosmotech/api-ts": "^3.0.15-dev",
"@cosmotech/azure": "^1.3.4",
"@cosmotech/core": "^1.14.0",
"@cosmotech/ui": "^6.0.1",
Expand Down
14 changes: 12 additions & 2 deletions src/hooks/ScenarioParametersHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,19 @@ export const useUpdateParameters = () => {
parametersMetadata,
parametersValues,
updateParameterValue,
addDatasetToStore
addDatasetToStore,
currentScenarioData?.security
);
}, [addDatasetToStore, setValue, getValues, organizationId, parametersMetadata, solutionData, workspaceId]);
}, [
addDatasetToStore,
setValue,
getValues,
organizationId,
parametersMetadata,
solutionData,
workspaceId,
currentScenarioData,
]);

const getParametersToUpdate = useCallback(() => {
const parametersValues = getValues();
Expand Down
7 changes: 4 additions & 3 deletions src/services/config/accessControl/Roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export const APP_ROLES = {
};

export const ACL_ROLES = {
ORGANIZATION: { ADMIN: 'admin' },
WORKSPACE: { ADMIN: 'admin' },
SCENARIO: { ADMIN: 'admin' },
DATASET: { NONE: 'none', VIEWER: 'viewer', USER: 'user', EDITOR: 'editor', ADMIN: 'admin' },
ORGANIZATION: { NONE: 'none', VIEWER: 'viewer', USER: 'user', EDITOR: 'editor', ADMIN: 'admin' },
WORKSPACE: { NONE: 'none', VIEWER: 'viewer', USER: 'user', EDITOR: 'editor', ADMIN: 'admin' },
SCENARIO: { NONE: 'none', VIEWER: 'viewer', EDITOR: 'editor', VALIDATOR: 'validator', ADMIN: 'admin' },
};
37 changes: 29 additions & 8 deletions src/services/dataset/DatasetService.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,49 @@
// Copyright (c) Cosmo Tech.
// Licensed under the MIT license.
import { Api } from '../../services/config/Api';
import { SecurityUtils } from '../../utils';

function findDatasetById(organizationId, datasetId) {
const findDatasetById = (organizationId, datasetId) => {
return Api.Datasets.findDatasetById(organizationId, datasetId);
}
};

function createDataset(organizationId, name, description, connector, tags, main = false) {
const createDataset = (organizationId, name, description, connector, tags, main = false) => {
const newDataset = { name, description, connector, tags, main };
return Api.Datasets.createDataset(organizationId, newDataset);
}
};

function updateDataset(organizationId, datasetId, dataset) {
const updateDataset = (organizationId, datasetId, dataset) => {
return Api.Datasets.updateDataset(organizationId, datasetId, dataset);
}
};

const updateSecurity = async (organizationId, datasetId, currentSecurity, newSecurity) => {
const setDefaultSecurity = async (newRole) =>
Api.Datasets.setDatasetDefaultSecurity(organizationId, datasetId, { role: newRole });
const addAccess = async (newEntry) => Api.Datasets.addDatasetAccessControl(organizationId, datasetId, newEntry);
const updateAccess = async (userIdToUpdate, newRole) =>
Api.Datasets.updateDatasetAccessControl(organizationId, datasetId, userIdToUpdate, { role: newRole });
const removeAccess = async (userIdToRemove) =>
Api.Datasets.removeDatasetAccessControl(organizationId, datasetId, userIdToRemove);

function deleteDataset(organizationId, datasetId) {
await SecurityUtils.updateResourceSecurity(
currentSecurity,
newSecurity,
setDefaultSecurity,
addAccess,
updateAccess,
removeAccess
);
};

const deleteDataset = (organizationId, datasetId) => {
return Api.Datasets.deleteDataset(organizationId, datasetId);
}
};

const DatasetService = {
findDatasetById,
createDataset,
updateDataset,
updateSecurity,
deleteDataset,
};

Expand Down
24 changes: 24 additions & 0 deletions src/services/scenario/ScenarioService.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.
import { Api } from '../../services/config/Api';
import { SCENARIO_VALIDATION_STATUS } from '../config/ApiConstants.js';
import { SecurityUtils } from '../../utils';

async function resetValidationStatus(organizationId, workspaceId, scenarioId) {
const data = { validationStatus: SCENARIO_VALIDATION_STATUS.DRAFT };
Expand All @@ -21,10 +22,33 @@ async function setValidationStatus(organizationId, workspaceId, scenarioId, vali
return Api.Scenarios.updateScenario(organizationId, workspaceId, scenarioId, data);
}

async function updateSecurity(organizationId, workspaceId, scenarioId, currentSecurity, newSecurity) {
const setDefaultSecurity = async (newRole) =>
Api.Scenarios.setScenarioDefaultSecurity(organizationId, workspaceId, scenarioId, { role: newRole });
const addAccess = async (newEntry) =>
Api.Scenarios.addScenarioAccessControl(organizationId, workspaceId, scenarioId, newEntry);
const updateAccess = async (userIdToUpdate, newRole) =>
Api.Scenarios.updateScenarioAccessControl(organizationId, workspaceId, scenarioId, userIdToUpdate, {
role: newRole,
});
const removeAccess = async (userIdToRemove) =>
Api.Scenarios.removeScenarioAccessControl(organizationId, workspaceId, scenarioId, userIdToRemove);

return SecurityUtils.updateResourceSecurity(
currentSecurity,
newSecurity,
setDefaultSecurity,
addAccess,
updateAccess,
removeAccess
);
}

const ScenarioService = {
resetValidationStatus,
setScenarioValidationStatusToValidated,
setScenarioValidationStatusToRejected,
updateSecurity,
};

export default ScenarioService;
1 change: 1 addition & 0 deletions src/state/commons/DatasetConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
export const DATASET_ACTIONS_KEY = {
GET_ALL_DATASETS: 'GET_ALL_DATASETS',
SET_ALL_DATASETS: 'SET_ALL_DATASETS',
SET_DATASET_SECURITY: 'SET_DATASET_SECURITY',
ADD_DATASET: 'ADD_DATASET',
};
16 changes: 16 additions & 0 deletions src/state/reducers/dataset/DatasetReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DATASET_ACTIONS_KEY } from '../../commons/DatasetConstants';
import { STATUSES } from '../../commons/Constants';
import { createReducer } from '@reduxjs/toolkit';
import { combineReducers } from 'redux';
import { DatasetsUtils } from '../../../utils';

export const datasetListInitialState = {
data: [],
Expand All @@ -23,6 +24,21 @@ export const datasetListReducer = createReducer(datasetListInitialState, (builde
.addCase(DATASET_ACTIONS_KEY.ADD_DATASET, (state, action) => {
delete action.type;
state.data.push(action);
})
.addCase(DATASET_ACTIONS_KEY.SET_DATASET_SECURITY, (state, action) => {
state.data = state.data?.map((datasetData) => {
if (datasetData.id === action.datasetId) {
const datasetWithNewSecurity = { ...datasetData, security: action.security };
DatasetsUtils.patchDatasetWithCurrentUserPermissions(
datasetWithNewSecurity,
action.userEmail,
action.userId,
action.permissionsMapping
);
return { ...datasetData, security: datasetWithNewSecurity.security };
}
return datasetData;
});
});
});

Expand Down
Loading

0 comments on commit 5ca2595

Please sign in to comment.