mirror of https://github.com/boxyhq/jackson.git
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:
parent
c77a720b04
commit
330a2dcc70
|
@ -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;
|
||||
|
|
|
@ -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: '',
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 '';
|
||||
};
|
||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue