diff --git a/plugins/rbac/src/__fixtures__/mockConditions.ts b/plugins/rbac/src/__fixtures__/mockConditions.ts index 604c135135..6737fd1d7d 100644 --- a/plugins/rbac/src/__fixtures__/mockConditions.ts +++ b/plugins/rbac/src/__fixtures__/mockConditions.ts @@ -42,4 +42,85 @@ export const mockConditions: RoleConditionalPolicyDecision[] = roleEntityRef: 'role:default/rbac_admin', permissionMapping: ['delete', 'update'], }, + { + id: 3, + result: AuthorizeResult.CONDITIONAL, + pluginId: 'catalog', + resourceType: 'catalog-entity', + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/ciiay'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/ciiay'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { + kinds: ['User'], + }, + }, + ], + }, + ], + }, + roleEntityRef: 'role:default/rbac_admin', + permissionMapping: ['read', 'delete', 'update'], + }, + { + id: 4, + result: AuthorizeResult.CONDITIONAL, + pluginId: 'catalog', + resourceType: 'catalog-entity', + conditions: { + not: { + rule: 'HAS_LABEL', + resourceType: 'catalog-entity', + params: { label: 'temp' }, + }, + }, + roleEntityRef: 'role:default/rbac_admin', + permissionMapping: ['delete', 'update'], + }, + { + id: 5, + result: AuthorizeResult.CONDITIONAL, + pluginId: 'catalog', + resourceType: 'catalog.location.read', + conditions: { + not: { + anyOf: [ + { + rule: 'HAS_LABEL', + resourceType: 'catalog-entity', + params: { label: 'temp' }, + }, + { + rule: 'HAS_METADATA', + resourceType: 'catalog-entity', + params: { key: 'status' }, + }, + ], + }, + }, + roleEntityRef: 'role:default/rbac_admin', + permissionMapping: ['delete'], + }, ]; diff --git a/plugins/rbac/src/components/ConditionalAccess/ConditionsForm.tsx b/plugins/rbac/src/components/ConditionalAccess/ConditionsForm.tsx index ac9d37e4e1..a361bbced5 100644 --- a/plugins/rbac/src/components/ConditionalAccess/ConditionsForm.tsx +++ b/plugins/rbac/src/components/ConditionalAccess/ConditionsForm.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import { PermissionCondition } from '@backstage/plugin-permission-common'; + import { makeStyles } from '@material-ui/core'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -67,13 +69,13 @@ export const ConditionsForm = ({ return !conditions.condition?.rule; } case criterias.not: { - return !conditions.not?.rule; + return !(conditions.not as PermissionCondition)?.rule; } case criterias.allOf: { - return !!conditions.allOf?.find(c => !c.rule); + return !!conditions.allOf?.find(c => !(c as PermissionCondition).rule); } case criterias.anyOf: { - return !!conditions.anyOf?.find(c => !c.rule); + return !!conditions.anyOf?.find(c => !(c as PermissionCondition).rule); } default: return true; diff --git a/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRow.tsx b/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRow.tsx index 0a22376314..1ecca38911 100644 --- a/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRow.tsx +++ b/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRow.tsx @@ -2,7 +2,14 @@ import React from 'react'; import { PermissionCondition } from '@backstage/plugin-permission-common'; -import { IconButton, makeStyles, useTheme } from '@material-ui/core'; +import { + FormControlLabel, + IconButton, + makeStyles, + Radio, + RadioGroup, + useTheme, +} from '@material-ui/core'; import AddIcon from '@mui/icons-material/Add'; import RemoveIcon from '@mui/icons-material/Remove'; import Box from '@mui/material/Box'; @@ -12,7 +19,12 @@ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import { ConditionsFormRowFields } from './ConditionsFormRowFields'; import { conditionButtons, criterias } from './const'; -import { ConditionsData, RuleParamsErrors, RulesData } from './types'; +import { + Condition, + ConditionsData, + RuleParamsErrors, + RulesData, +} from './types'; const useStyles = makeStyles(theme => ({ conditionRow: { @@ -24,16 +36,38 @@ const useStyles = makeStyles(theme => ({ backgroundColor: `${theme.palette.background.paper}!important`, }, }, + nestedConditionRow: { + padding: '20px', + marginLeft: '20px', + border: `1px solid ${theme.palette.border}`, + borderRadius: '4px', + backgroundColor: theme.palette.background.default, + '& input': { + backgroundColor: `${theme.palette.background.paper}!important`, + }, + }, criteriaButtonGroup: { backgroundColor: theme.palette.background.paper, width: '80%', }, + nestedConditioncriteriaButtonGroup: { + backgroundColor: theme.palette.background.paper, + width: '60%', + height: '100%', + }, criteriaButton: { width: '100%', textTransform: 'none', padding: theme.spacing(1), }, addRuleButton: { + display: 'flex', + color: theme.palette.primary.light, + textTransform: 'none', + marginTop: theme.spacing(1), + }, + addNestedConditionButton: { + display: 'flex', color: theme.palette.primary.light, textTransform: 'none', marginTop: theme.spacing(1), @@ -44,6 +78,11 @@ const useStyles = makeStyles(theme => ({ alignSelf: 'baseline', marginTop: theme.spacing(3.3), }, + removeNestedRuleButton: { + color: theme.palette.grey[500], + flexGrow: 0, + alignSelf: 'baseline', + }, })); type ConditionFormRowProps = { @@ -57,6 +96,8 @@ type ConditionFormRowProps = { setRemoveAllClicked: React.Dispatch>; }; +type NotConditionType = 'simple-condition' | 'nested-condition'; + export const ConditionsFormRow = ({ conditionRulesData, conditionRow, @@ -69,6 +110,59 @@ export const ConditionsFormRow = ({ }: ConditionFormRowProps) => { const classes = useStyles(); const theme = useTheme(); + const nestedConditionButtons = conditionButtons.filter( + button => button.val !== 'condition', + ); + + const [nestedConditionRow, setNestedConditionRow] = React.useState< + ConditionsData[] + >([]); + const [notConditionType, setNotConditionType] = + React.useState('simple-condition'); + + React.useEffect(() => { + let nestedConditions: ConditionsData[] = []; + + const extractNestedConditions = ( + conditions: ConditionsData[], + criteriaTypes: string[], + ) => { + Array.isArray(conditions) && + conditions?.forEach(c => { + criteriaTypes.forEach(ct => { + if (Object.keys(c).includes(ct)) { + nestedConditions.push(c as ConditionsData); + } + }); + }); + }; + + switch (criteria) { + case criterias.allOf: + extractNestedConditions( + (conditionRow.allOf as ConditionsData[]) || [], + [criterias.allOf, criterias.anyOf, criterias.not], + ); + break; + case criterias.anyOf: + extractNestedConditions( + (conditionRow.anyOf as ConditionsData[]) || [], + [criterias.allOf, criterias.anyOf, criterias.not], + ); + break; + case criterias.not: + extractNestedConditions((conditionRow.not as ConditionsData[]) || [], [ + criterias.allOf, + criterias.anyOf, + criterias.not, + ]); + break; + default: + break; + } + + setNestedConditionRow(nestedConditions); + }, [conditionRow, criteria]); const handleCriteriaChange = (val: string) => { setCriteria(val); @@ -108,6 +202,7 @@ export const ConditionsFormRow = ({ break; } case criterias.not: { + setNotConditionType('simple-condition'); onRuleChange({ not: { rule: '', @@ -121,6 +216,154 @@ export const ConditionsFormRow = ({ } }; + const updateRules = (updatedNestedConditionRow: ConditionsData[]) => { + const existingSimpleCondition = + criteria !== criterias.not + ? ( + conditionRow[criteria as keyof ConditionsData] as Condition[] + )?.filter(con => Object.keys(con).includes('rule')) || [] + : []; + const newCondition = [ + ...existingSimpleCondition, + ...updatedNestedConditionRow, + ]; + + if (criteria === criterias.anyOf) { + onRuleChange({ + [criteria]: [...newCondition], + }); + } + if (criteria === criterias.allOf) { + onRuleChange({ + [criteria]: [...newCondition], + }); + } + if (criteria === criterias.not) { + onRuleChange({ + [criteria]: updatedNestedConditionRow, + }); + } + }; + + const handleNestedConditionCriteriaChange = ( + val: string, + nestedConditionIndex: number, + ) => { + const updatedNestedConditionRow = nestedConditionRow.map((c, index) => { + if (index === nestedConditionIndex) { + return { + [val]: [ + { + rule: '', + resourceType: selPluginResourceType, + params: {}, + }, + ], + }; + } + return c; + }); + + updateRules(updatedNestedConditionRow); + }; + + const handleAddNestedCondition = () => { + const updatedNestedConditionRow = [ + ...nestedConditionRow, + { + [criterias.allOf]: [ + { + rule: '', + resourceType: selPluginResourceType, + params: {}, + }, + ], + }, + ]; + updateRules(updatedNestedConditionRow); + }; + + const handleNotConditionTypeChange = (val: string) => { + setNotConditionType(val as NotConditionType); + if (val === 'nested-condition') { + handleAddNestedCondition(); + } else { + onRuleChange({ + not: { + rule: '', + resourceType: selPluginResourceType, + params: {}, + }, + }); + if (nestedConditionRow.length > 0) { + nestedConditionRow.forEach((_c, index) => { + handleRemoveNestedCondition(index); + }); + } + } + }; + + const handleAddRuleInNestedCondition = ( + nestedConditionCriteria: string, + nestedConditionIndex: number, + ) => { + const updatedNestedConditionRow: ConditionsData[] = []; + + nestedConditionRow.forEach((c, index) => { + if (index === nestedConditionIndex) { + updatedNestedConditionRow.push({ + [nestedConditionCriteria as keyof ConditionsData]: [ + ...((c[ + nestedConditionCriteria as keyof ConditionsData + ] as PermissionCondition[]) || []), + { + rule: '', + resourceType: selPluginResourceType, + params: {}, + }, + ], + }); + } else { + updatedNestedConditionRow.push(c); + } + }); + + updateRules(updatedNestedConditionRow); + }; + + const handleRemoveNestedCondition = (nestedConditionIndex: number) => { + const updatedNestedConditionRow = nestedConditionRow.filter( + (_, index) => index !== nestedConditionIndex, + ); + + updateRules(updatedNestedConditionRow); + }; + + const handleRemoveNestedConditionRule = ( + nestedConditionCriteria: string, + nestedConditionIndex: number, + ruleIndex: number, + ) => { + const updatedNestedConditionRow: ConditionsData[] = []; + + nestedConditionRow.forEach((c, index) => { + if (index === nestedConditionIndex) { + const updatedRules = ( + (c[ + nestedConditionCriteria as keyof ConditionsData + ] as PermissionCondition[]) || [] + ).filter((_r, rindex) => rindex !== ruleIndex); + updatedNestedConditionRow.push({ + [nestedConditionCriteria as keyof ConditionsData]: updatedRules, + }); + } else { + updatedNestedConditionRow.push(c); + } + }); + + updateRules(updatedNestedConditionRow); + }; + const ruleOptionDisabled = ( ruleOption: string, conditions?: PermissionCondition[], @@ -155,84 +398,316 @@ export const ConditionsFormRow = ({ ))} - {(criteria === criterias.allOf || criteria === criterias.anyOf) && ( + {(criteria === criterias.allOf || + criteria === criterias.anyOf || + criteria === criterias.not) && ( {criteria === criterias.allOf && - conditionRow.allOf?.map((c, index) => ( -
- - { - const rules = (conditionRow.allOf || []).filter( - (_r, rindex) => index !== rindex, - ); - onRuleChange({ allOf: rules }); - }} - > - - -
- ))} + conditionRow.allOf?.map((c, index) => { + return ( + (c as PermissionCondition).resourceType && ( +
+ + { + const rules = (conditionRow.allOf || []).filter( + (_r, rindex) => index !== rindex, + ); + onRuleChange({ allOf: rules }); + }} + > + + +
+ ) + ); + })} {criteria === criterias.anyOf && - conditionRow.anyOf?.map((c, index) => ( -
+ conditionRow.anyOf?.map((c, index) => { + return ( + (c as PermissionCondition).resourceType && ( +
+ + { + const rules = (conditionRow.anyOf || []).filter( + (_r, rindex) => index !== rindex, + ); + onRuleChange({ anyOf: rules }); + }} + > + + +
+ ) + ); + })} + {criteria === criterias.not && ( + + handleNotConditionTypeChange(value as NotConditionType) + } + > + } + label="Add rule" + /> + {notConditionType === 'simple-condition' && ( + ruleOptionDisabled( + ruleOption, + conditionRow.not + ? [conditionRow.not as PermissionCondition] + : undefined, + ) + } setRemoveAllClicked={setRemoveAllClicked} /> - { - const rules = (conditionRow.anyOf || []).filter( - (_r, rindex) => index !== rindex, - ); - onRuleChange({ anyOf: rules }); - }} + )} + } + label="Add nested condition" + /> + + )} + {(criteria === criterias.allOf || criteria === criterias.anyOf) && ( + + )} + {(criteria === criterias.allOf || criteria === criterias.anyOf) && ( + + )} + {nestedConditionRow?.length > 0 && + nestedConditionRow.map((nc, nestedConditionIndex) => { + return ( + - - -
- ))} - +
+ + handleNestedConditionCriteriaChange( + newNestedCriteria, + nestedConditionIndex, + ) + } + className={classes.nestedConditioncriteriaButtonGroup} + > + {nestedConditionButtons.map(({ val, label }) => ( + + {label} + + ))} + + + handleRemoveNestedCondition(nestedConditionIndex) + } + > + + +
+ + {Object.keys(nc)[0] === criterias.allOf && + nc.allOf?.map((c, index) => ( +
+ + + handleRemoveNestedConditionRule( + criterias.allOf, + nestedConditionIndex, + index, + ) + } + > + + +
+ ))} + {Object.keys(nc)[0] === criterias.anyOf && + nc.anyOf?.map((c, index) => ( +
+ + + handleRemoveNestedConditionRule( + criterias.anyOf, + nestedConditionIndex, + index, + ) + } + > + + +
+ ))} + {Object.keys(nc)[0] === criterias.not && ( + + ruleOptionDisabled( + ruleOption, + nc.not + ? [nc.not as PermissionCondition] + : undefined, + ) + } + setRemoveAllClicked={setRemoveAllClicked} + /> + )} + {Object.keys(nc)[0] !== criterias.not && ( + + )} +
+
+ ); + })} )} {criteria === criterias.condition && ( @@ -258,29 +733,6 @@ export const ConditionsFormRow = ({ setRemoveAllClicked={setRemoveAllClicked} /> )} - {criteria === criterias.not && ( - - ruleOptionDisabled( - ruleOption, - conditionRow.not ? [conditionRow.not] : undefined, - ) - } - setRemoveAllClicked={setRemoveAllClicked} - /> - )} ); }; diff --git a/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRowFields.tsx b/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRowFields.tsx index 8829f2dfbf..88e0fc9ecb 100644 --- a/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRowFields.tsx +++ b/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRowFields.tsx @@ -11,7 +11,12 @@ import validator from '@rjsf/validator-ajv8'; import { criterias } from './const'; import { CustomArrayField } from './CustomArrayField'; import { RulesDropdownOption } from './RulesDropdownOption'; -import { ConditionsData, RuleParamsErrors, RulesData } from './types'; +import { + Condition, + ConditionsData, + RuleParamsErrors, + RulesData, +} from './types'; const useStyles = makeStyles(theme => ({ bgPaper: { @@ -23,7 +28,7 @@ const useStyles = makeStyles(theme => ({ })); type ConditionFormRowFieldsProps = { - oldCondition: PermissionCondition; + oldCondition: Condition; index?: number; criteria: string; onRuleChange: (newCondition: ConditionsData) => void; @@ -32,6 +37,11 @@ type ConditionFormRowFieldsProps = { setErrors: React.Dispatch>; optionDisabled?: (ruleOption: string) => boolean; setRemoveAllClicked: React.Dispatch>; + nestedConditionRow?: ConditionsData[]; + nestedConditionCriteria?: string; + nestedConditionIndex?: number; + ruleIndex?: number; + updateRules?: (newCondition: ConditionsData[]) => void; }; export const ConditionsFormRowFields = ({ @@ -44,10 +54,16 @@ export const ConditionsFormRowFields = ({ setErrors, optionDisabled, setRemoveAllClicked, + nestedConditionRow, + nestedConditionCriteria, + nestedConditionIndex, + ruleIndex, + updateRules, }: ConditionFormRowFieldsProps) => { const classes = useStyles(); const rules = conditionRulesData?.rules ?? []; - const paramsSchema = conditionRulesData?.[oldCondition.rule]?.schema; + const paramsSchema = + conditionRulesData?.[(oldCondition as PermissionCondition).rule]?.schema; const schema: RJSFSchema = paramsSchema; @@ -88,6 +104,62 @@ export const ConditionsFormRowFields = ({ } }; + const handleNestedConditionChange = ( + updateRules: (conditions: ConditionsData[]) => void, + nestedConditionRow: ConditionsData[], + nestedConditionCriteria: string, + nestedConditionIndex: number, + ruleIndex: number, + newCondition: PermissionCondition, + ) => { + const updatedNestedConditionRow: ConditionsData[] = []; + + nestedConditionRow.forEach((c, index) => { + if (index === nestedConditionIndex) { + const updatedNestedConditionRules = ( + (c[ + nestedConditionCriteria as keyof ConditionsData + ] as PermissionCondition[]) || [] + ).map((_r, rindex) => { + if (rindex === ruleIndex) { + return newCondition; + } + return _r; + }); + + updatedNestedConditionRow.push({ + [nestedConditionCriteria as keyof ConditionsData]: + updatedNestedConditionRules, + }); + } else { + updatedNestedConditionRow.push(c); + } + }); + + updateRules(updatedNestedConditionRow); + }; + + const onConditionChange = (newCondition: PermissionCondition) => { + if ( + nestedConditionRow && + nestedConditionCriteria && + nestedConditionIndex !== undefined && + ruleIndex !== undefined && + updateRules + ) { + handleNestedConditionChange( + updateRules, + nestedConditionRow, + nestedConditionCriteria, + nestedConditionIndex, + ruleIndex, + newCondition, + ); + } else { + handleConditionChange(newCondition as PermissionCondition); + } + }; + return ( optionDisabled ? optionDisabled(option) : false } onChange={(_event, ruleVal?: string | null) => - handleConditionChange({ + onConditionChange({ ...oldCondition, rule: ruleVal ?? '', params: {}, - }) + } as PermissionCondition) } renderOption={option => ( - handleConditionChange({ + onConditionChange({ ...oldCondition, params: data.formData || {}, - }) + } as PermissionCondition) } transformErrors={errors => { setErrors({ [criteria]: errors }); diff --git a/plugins/rbac/src/components/ConditionalAccess/types.ts b/plugins/rbac/src/components/ConditionalAccess/types.ts index eb6afd3fdc..290bef2632 100644 --- a/plugins/rbac/src/components/ConditionalAccess/types.ts +++ b/plugins/rbac/src/components/ConditionalAccess/types.ts @@ -23,12 +23,14 @@ export type ConditionRules = { }; export type ConditionsData = { - allOf?: PermissionCondition[]; - anyOf?: PermissionCondition[]; - not?: PermissionCondition; + allOf?: Condition[]; + anyOf?: Condition[]; + not?: Condition; condition?: PermissionCondition; }; +export type Condition = PermissionCondition | ConditionsData; + export type RuleParamsErrors = { [key: string]: RJSFValidationError[]; };