Terminus - Role based masking support (#1580)

* init

* added default masking as Redact

* dynamic roles for masking
dynamic cue generation
added api to get roles

* fixes

* lint fix
This commit is contained in:
Utkarsh Mehta 2023-09-12 02:47:57 +05:30 committed by GitHub
parent c77a720b04
commit 330a2dcc70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 120 additions and 25 deletions

View File

@ -5,6 +5,7 @@ import { useTranslation } from 'next-i18next';
import Blockly from 'blockly/core';
import 'blockly/blocks';
import { maskSetup } from '@components/terminus/blocks/customblocks';
import locale from 'blockly/msg/en';
Blockly.setLocale(locale);
@ -36,7 +37,7 @@ function BlocklyComponent(props) {
const uploadModel = async () => {
const domToPretty = modelToXML();
const cueModel = generateModel(primaryWorkspace.current);
const cueModel = generateModel(primaryWorkspace.current, roles);
const body = {
cue_schema: cueModel,
@ -49,7 +50,6 @@ function BlocklyComponent(props) {
};
const rsp = await fetch(getEndpoint(), requestOptions);
if (rsp.ok) {
successToast(t('model_published_successfully'));
return;
@ -59,12 +59,12 @@ function BlocklyComponent(props) {
};
const [retrieveModalVisible, setRetrieveModalVisible] = useState(false);
const [roles, setRoles] = useState([]);
const toggleRetrieveConfirm = () => setRetrieveModalVisible(!retrieveModalVisible);
const retrieveModel = async () => {
const rsp = await fetch(getEndpoint());
const response = await rsp.json();
(primaryWorkspace.current! as any).clear();
const textToDom = Blockly.utils.xml.textToDom(response.data);
Blockly.Xml.domToWorkspace(textToDom, primaryWorkspace.current! as any);
@ -76,7 +76,7 @@ function BlocklyComponent(props) {
if (primaryWorkspace.current) {
return;
}
getRolesAndSetupMasks();
const { initialXml, ...rest } = props;
primaryWorkspace.current = Blockly.inject(blocklyDiv.current as any, {
toolbox: toolbox.current,
@ -98,7 +98,7 @@ function BlocklyComponent(props) {
}
retrieveModel();
}, [primaryWorkspace, toolbox, blocklyDiv, props]);
}, [primaryWorkspace, toolbox, blocklyDiv, roles, props]);
return (
<div>
@ -139,6 +139,13 @@ function BlocklyComponent(props) {
onCancel={toggleRetrieveConfirm}></ConfirmationModal>
</div>
);
async function getRolesAndSetupMasks() {
const rolesResp = await fetch(`/api/admin/terminus/roles`);
const rolesList = (await rolesResp.json())?.data;
maskSetup(rolesList);
setRoles(rolesList);
}
}
export default BlocklyComponent;

View File

@ -204,16 +204,20 @@ Blockly.Blocks['data_object_field_encryption'] = {
// this.setStyle('loop_blocks');
},
};
Blockly.Blocks['data_object_field_mask'] = {
init: function () {
this.jsonInit({
type: 'data_object_field_mask',
message0: 'mask %1 %2',
message0: 'mask (Admin:%1) (Member:%2) %3',
args0: [
{
type: 'field_dropdown',
name: 'object_type',
name: 'object_type_ADMIN',
options: getMasks(),
},
{
type: 'field_dropdown',
name: 'object_type_MEMBER',
options: getMasks(),
},
{
@ -229,3 +233,44 @@ Blockly.Blocks['data_object_field_mask'] = {
});
},
};
const capitalize = (s: string) => {
if (typeof s !== 'string') return '';
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
};
export const maskSetup = (roles: string[]) => {
let maskMessage = 'mask (';
const args: any[] = [];
for (let i = 0; i < roles.length; i++) {
maskMessage += `${capitalize(roles[i])}:%${i + 1}`;
if (i < roles.length - 1) {
maskMessage += ') (';
}
args.push({
type: 'field_dropdown',
name: `object_type_${roles[i]}`,
options: getMasks(),
});
}
Blockly.Blocks['data_object_field_mask'] = {
init: function () {
this.jsonInit({
type: 'data_object_field_mask',
message0: maskMessage + `) %${roles.length + 1}`,
args0: [
...args,
{
type: 'input_value',
name: 'object_type',
check: ['Boolean', 'String'],
},
],
output: null,
colour: 230,
tooltip: '',
helpUrl: '',
});
},
};
};

View File

@ -19,11 +19,23 @@ const regexMap = {
'defs.#SimpleDateFormat': '^20[0-9]{2}-[0-1][1-2]-[0-2][1-8]$', // regex restricted.
};
export const generateModel = (workspace) => {
export const generateModel = (workspace, roles: string[]) => {
ObjectMap.clear();
javascriptGenerator['data_object_field_mask'] = function (block) {
for (let i = 0; i < roles.length; i++) {
const objName = block.getFieldValue(`object_type_${roles[i]}`);
currentField[2 + i] = objName; // mask
}
javascriptGenerator.statementToCode(block, 'input', javascriptGenerator.ORDER_NONE);
return '';
};
// trigger the BLOCKLY processing which will run our custom code generation
javascriptGenerator.workspaceToCode(workspace);
const ret = generateCUEStructure();
const ret = generateCUEStructure(roles);
// add specific BoxyHQ imports
return `
@ -33,7 +45,7 @@ export const generateModel = (workspace) => {
};
// Rudimentary way of generating a CUE file
const generateCUEStructure = () => {
const generateCUEStructure = (roles: string[]) => {
let defs = ``;
const encrObjects = [];
for (const [key, value] of Object.entries(Object.fromEntries(ObjectMap))) {
@ -67,19 +79,23 @@ const generateCUEStructure = () => {
}
// MASKS
let masks = ``;
let maskString = '';
for (const [field, values] of Object.entries(valuesMap)) {
if (IGNORE_FIELDS.includes(field)) {
continue;
}
masks += `\n\t\t\t${field}: ${values[2]}`;
let index = 2;
for (const role of roles) {
const maskKey = `#Mask_${role.toLowerCase()}`;
const maskVal = `\n\t\t\t${field}: ${values.length > index ? values[index++] : 'masking.#MRedact'}`;
maskString += `\n${maskKey}: { ${maskVal}
}`;
}
}
const objectOutput = `\n#${key}: {
#Definition: { ${definitions}
}
#Encryption: ${encryption}
#Mask_admin: { ${masks}
}
#Encryption: ${encryption}${maskString}
}`;
defs += objectOutput;
}
@ -122,6 +138,8 @@ javascriptGenerator['data_object_field_wrapper'] = function (block) {
javascriptGenerator['data_object_field_type'] = function (block) {
const objectName = block.getFieldValue('object_type');
currentField[0] = objectName; // type
currentField[2] = 'masking.#MRedact'; // mask
currentField[3] = 'masking.#MRedact'; // mask
javascriptGenerator.statementToCode(block, 'input', javascriptGenerator.ORDER_NONE);
@ -138,12 +156,3 @@ javascriptGenerator['data_object_field_encryption'] = function (block) {
return '';
};
javascriptGenerator['data_object_field_mask'] = function (block) {
const objectName = block.getFieldValue('object_type');
currentField[2] = objectName; // mask
javascriptGenerator.statementToCode(block, 'input', javascriptGenerator.ORDER_NONE);
return '';
};

View File

@ -0,0 +1,34 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import axios from 'axios';
import { terminusOptions } from '@lib/env';
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req;
switch (method) {
case 'GET':
return await getRoles(req, res);
default:
res.setHeader('Allow', 'GET');
res.status(405).json({
data: null,
error: { message: `Method ${method} Not Allowed` },
});
}
}
const getRoles = async (req: NextApiRequest, res: NextApiResponse) => {
const { data } = await axios.get<any>(`${terminusOptions.hostUrl}/v1/admin/roles`, {
headers: {
Authorization: `api-key ${terminusOptions.adminToken}`,
'x-access-token': terminusOptions.adminToken,
},
});
return res.status(201).json({
data,
error: null,
});
};
export default handler;