This is an automated email from the ASF dual-hosted git repository. dhavaljrajpara pushed a commit to branch CDPQE-50726 in repository https://gitbox.apache.org/repos/asf/ranger.git
commit f011d5f236bdc42b079abd2e7fc8143651217b81 Author: Dhaval Rajpara <[email protected]> AuthorDate: Mon Nov 2 08:50:34 2026 +0530 RANGER-5860: --- .../main/webapp/react-webapp/src/utils/XAUtils.js | 112 +- .../NewPolicyForm/CreateNewPolicyForm.jsx | 8 +- .../AccessRulesStep.jsx | 379 ++--- .../PolicyPermissionItemVertical.jsx | 1484 ++++++++++---------- .../views/PolicyListing/PolicyPermissionItem.jsx | 88 +- 5 files changed, 1012 insertions(+), 1059 deletions(-) diff --git a/security-admin/src/main/webapp/react-webapp/src/utils/XAUtils.js b/security-admin/src/main/webapp/react-webapp/src/utils/XAUtils.js index 0ca29858d..d00d358e2 100644 --- a/security-admin/src/main/webapp/react-webapp/src/utils/XAUtils.js +++ b/security-admin/src/main/webapp/react-webapp/src/utils/XAUtils.js @@ -110,9 +110,8 @@ export const isRenderRowFilter = (rowFilterDef) => { : false; }; -export const isRenderConstraint = (constraintDef) => { - return constraintDef && constraintDef.length > 0 ? true : false; -}; +export const isRenderConstraint = (constraintDefs) => + Array.isArray(constraintDefs) && constraintDefs.length > 0; // Returns the policy types (Access / Masking / Row Filter) supported by a service def. // Access is always supported; Masking and Row Filter depend on the service def. @@ -1555,7 +1554,6 @@ export const policyConditionUpdatedJSON = (policyCond) => { }; // Get resources with help of policy type - export const getResourcesDefVal = (serviceDef, policyType) => { let resources = []; if (RangerPolicyType.RANGER_MASKING_POLICY_TYPE.value == policyType) { @@ -1574,6 +1572,7 @@ export const getResourcesDefVal = (serviceDef, policyType) => { return resources; }; +// Get constraint resources export const getConstraintResources = (serviceDef) => { let uniqueResources = uniq(flatMap(serviceDef?.constraintDefs, "resources")); let result = filter(serviceDef?.resources, (item) => @@ -1615,7 +1614,10 @@ export const getConstraintDefByName = (serviceDef, constraintName) => { * Checks the form-value option first (it carries the property from ResourceSelectComp), * then falls back to the service-def resource definition. */ -export const getLeafAccessTypeRestrictions = (formValues, serviceCompDetails) => { +export const getLeafAccessTypeRestrictions = ( + formValues, + serviceCompDetails +) => { if (!formValues || !serviceCompDetails) { return null; } @@ -1867,7 +1869,7 @@ export const getResourceLevelKeysSorted = (serviceCompDetails) => { export const getAccessTypesByResource = ( formValues, serviceCompDetails, - getMatchedConstraint + getMatchedConstraintItems ) => { if (!formValues || !serviceCompDetails) { return []; @@ -1893,8 +1895,10 @@ export const getAccessTypesByResource = ( formValues.policyType ) { srcOp = - getAccessTypesFromMatchedDefs(getMatchedConstraint, serviceCompDetails) || - []; + getAccessTypesFromMatchedDefs( + getMatchedConstraintItems, + serviceCompDetails + ) || []; } else { srcOp = serviceCompDetails.accessTypes; } @@ -1937,78 +1941,6 @@ export const getAccessTypesByResource = ( ); }; -// // get constraint policy access types -// export const getConstraintPolicyAccessTypes = ( -// serviceCompDetails, -// formValues -// ) => { -// const additionalResources = formValues?.additionalResources; -// const constraintDefs = serviceCompDetails?.constraintDefs; -// const allAccessTypes = serviceCompDetails?.accessTypes; - -// if ( -// !Array.isArray(additionalResources) || -// !Array.isArray(constraintDefs) || -// !Array.isArray(allAccessTypes) -// ) { -// return []; -// } - -// // Helper: check if two arrays contain the exact same unique resources -// const isExactResourceMatch = (resA = [], resB = []) => { -// if (resA.length !== resB.length) return false; -// const setB = new Set(resB); -// return resA.every((name) => setB.has(name)); -// }; - -// const matchedAccessTypeNames = new Set(); -// const evaluatedSignatures = new Set(); - -// for (const row of additionalResources) { -// // Extract resource names in level order: ["caiiservicecrn", "namespace", ...] -// const resourceNames = []; -// for (const [key, val] of Object.entries(row)) { -// if (key.startsWith("resourceName-") && val?.name) { -// resourceNames.push(val.name); -// } -// } - -// if (resourceNames.length === 0) continue; - -// // Skip redundant evaluations if the same resource combination exists in another row -// const signature = resourceNames.slice().sort().join(","); -// if (evaluatedSignatures.has(signature)) continue; -// evaluatedSignatures.add(signature); - -// // Find all constraintDefs that match this exact resource combination -// for (const def of constraintDefs) { -// if (isExactResourceMatch(resourceNames, def.resources)) { -// if (Array.isArray(def.accessTypes)) { -// def.accessTypes.forEach((type) => matchedAccessTypeNames.add(type)); -// } -// } -// } -// } - -// // Filter full accessType objects that exist in our matched names -// return allAccessTypes.filter((item) => matchedAccessTypeNames.has(item.name)); -// }; - -// export const getAllConstraintAccessTypes = (serviceCompDetails) => { -// const constraintDefs = serviceCompDetails?.constraintDefs || []; -// const accessTypesList = serviceCompDetails?.accessTypes || []; - -// // 1. Collect all unique accessType strings from every constraintDef -// const targetAccessTypeNames = new Set( -// flatMap(constraintDefs, (def) => def.accessTypes || []) -// ); - -// // 2. Filter the full objects matching those names -// return filter(accessTypesList, (item) => -// targetAccessTypeNames.has(item.name) -// ); -// }; - export const prunePolicyItemAccessesToAllowedTypes = ( form, selectedServiceComponentDef @@ -2204,10 +2136,7 @@ export const serializeConstraintPolicyItemsForApi = ( type: access.type ?? access.value, isAllowed: true })); - } else if ( - key?.accesses?.tableList?.length > 0 && - isTagService - ) { + } else if (key?.accesses?.tableList?.length > 0 && isTagService) { let accessesData = []; for (const data in key.accesses.tableList) { accessesData = [ @@ -2340,14 +2269,11 @@ export const getAccessTypesFromMatchedDefs = ( ) => { const allAccessTypes = serviceCompDetails?.accessTypes || []; - // Extract unique accessType names: ['execute', 'select', ...] - const accessTypeNames = _.chain(matchedDefs) - .flatMap("accessTypes") - .uniq() - .value(); - - // Filter full objects from serviceCompDetails.accessTypes - return _.filter(allAccessTypes, (item) => - _.includes(accessTypeNames, item.name) + // Collect unique accessType names across all matched constraint defs + const accessTypeNames = uniq( + flatMap(matchedDefs, (def) => def.accessTypes || []).filter(Boolean) ); + + // Return matching full accessType objects from the service def + return filter(allAccessTypes, (item) => includes(accessTypeNames, item.name)); }; diff --git a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/CreateNewPolicyForm.jsx b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/CreateNewPolicyForm.jsx index e8497b4ba..d0a11aab9 100644 --- a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/CreateNewPolicyForm.jsx +++ b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/CreateNewPolicyForm.jsx @@ -175,7 +175,10 @@ const CreateNewPolicyForm = () => { const obj = {}; appendAccessesPrincipalsAndConditions(obj, key); - const constraintInfo = buildConstraintInfoForApi(key.constraintInfo); + const constraintInfo = buildConstraintInfoForApi( + key.constraintInfo, + selectedServiceComponentDef + ); if (constraintInfo) { obj.constraintInfo = constraintInfo; } @@ -465,9 +468,6 @@ const CreateNewPolicyForm = () => { localStorage.removeItem("zoneDetails"); } - // eslint-disable-next-line no-console - console.log("Create policy payload:", JSON.stringify(data, null, 2)); - const getPolicyCount = await fetchApi({ url: "plugins/policies/count", method: "GET", diff --git a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/AccessRulesStep.jsx b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/AccessRulesStep.jsx index 4038fd0f7..408a87208 100644 --- a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/AccessRulesStep.jsx +++ b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/AccessRulesStep.jsx @@ -23,6 +23,7 @@ import { Field } from "react-final-form"; import BootstrapSwitchButton from "bootstrap-switch-button-react"; import PolicyPermissionItemVertical from "./PolicyPermissionItemVertical"; import { isEmpty } from "lodash"; +import { RangerPolicyType } from "Utils/XAEnums"; const hasItems = (arr) => Array.isArray(arr) && arr.some((item) => !isItemEmpty(item)); @@ -95,50 +96,59 @@ const AccessRulesStep = ({ const policyPermissionItems = []; const policyType = values.policyType; - // Count items in each category - const allowRulesCount = hasItems(values?.policyItems) - ? values.policyItems.filter((item) => !isEmpty(item)).length - : 0; + if (policyType == RangerPolicyType.RANGER_ACCESS_POLICY_TYPE.value) { + // Count items in each category + const allowRulesCount = hasItems(values?.policyItems) + ? values.policyItems.filter((item) => !isEmpty(item)).length + : 0; - const allowExceptionsCount = hasItems(values?.allowExceptions) - ? values.allowExceptions.filter((item) => !isEmpty(item)).length - : 0; + const allowExceptionsCount = hasItems(values?.allowExceptions) + ? values.allowExceptions.filter((item) => !isEmpty(item)).length + : 0; - const denyRulesCount = hasItems(values?.denyPolicyItems) - ? values.denyPolicyItems.filter((item) => !isEmpty(item)).length - : 0; + const denyRulesCount = hasItems(values?.denyPolicyItems) + ? values.denyPolicyItems.filter((item) => !isEmpty(item)).length + : 0; - const denyExceptionsCount = hasItems(values?.denyExceptions) - ? values.denyExceptions.filter((item) => !isEmpty(item)).length - : 0; + const denyExceptionsCount = hasItems(values?.denyExceptions) + ? values.denyExceptions.filter((item) => !isEmpty(item)).length + : 0; - // Total counts for tab display - const allowTabTotal = allowRulesCount + allowExceptionsCount; - const denyTabTotal = denyRulesCount + denyExceptionsCount; + // Total counts for tab display + const allowTabTotal = allowRulesCount + allowExceptionsCount; + const denyTabTotal = denyRulesCount + denyExceptionsCount; - // Auto-open "Exclude from Allow Rules" accordion item if it has saved values - const hasAllowExceptions = hasItems(values?.allowExceptions); - const allowAccordionDefault = hasAllowExceptions ? ["0", "1"] : ["0"]; + // Auto-open "Exclude from Allow Rules" accordion item if it has saved values + const hasAllowExceptions = hasItems(values?.allowExceptions); + const allowAccordionDefault = hasAllowExceptions ? ["0", "1"] : ["0"]; + } - if (policyType == 1) { + if (policyType == RangerPolicyType.RANGER_MASKING_POLICY_TYPE.value) { policyPermissionItems.push({ attrName: "dataMaskPolicyItems", title: "Mask Rules", msg: "Mask rules grants access to resources when the specific criteria are met." }); - } else if (policyType == 2) { + } else if ( + policyType == RangerPolicyType.RANGER_ROW_FILTER_POLICY_TYPE.value + ) { policyPermissionItems.push({ attrName: "rowFilterPolicyItems", title: "Row Filter Rules", msg: "Row filter rules grants access to resources when the specific criteria are met." }); - } else if (policyType == 4) { + } else if ( + policyType == RangerPolicyType.RANGER_CONSTRAINT_POLICY_TYPE.value + ) { policyPermissionItems.push({ attrName: "constraintPolicyItems", title: "Constraint Rules", msg: "Constraint rules grants access to resources when the specific criteria are met." }); - } else if (policyType == 0 && !enableDenyAndExceptions) { + } else if ( + policyType == RangerPolicyType.RANGER_ACCESS_POLICY_TYPE.value && + !enableDenyAndExceptions + ) { policyPermissionItems.push({ attrName: "policyItems", title: "Allow Rules", @@ -154,174 +164,181 @@ const AccessRulesStep = ({ access to specific users/groups/roles and deny conditions to explicitly block access </p> - {policyType == 0 && isEmpty(policyPermissionItems) && ( - <> - <Tabs defaultActiveKey="allow" transition={false} className="mb-3"> - <Tab - eventKey="allow" - title={ - <span className="d-flex align-items-center gap-2"> - Allow - {allowTabTotal > 0 && ( - <span - className="badge bg-primary text-light" - style={{ - fontSize: "0.75rem", - padding: "0.25rem 0.5rem", - borderRadius: "12px", - margin: "0px" - }} - > - {allowTabTotal} - </span> - )} - </span> - } + {policyType == RangerPolicyType.RANGER_ACCESS_POLICY_TYPE.value && + isEmpty(policyPermissionItems) && ( + <> + <Tabs + defaultActiveKey="allow" + transition={false} + className="mb-3" > - <Accordion - defaultActiveKey={allowAccordionDefault} - alwaysOpen - className="policy-permission-items-accordion" + <Tab + eventKey="allow" + title={ + <span className="d-flex align-items-center gap-2"> + Allow + {allowTabTotal > 0 && ( + <span + className="badge bg-primary text-light" + style={{ + fontSize: "0.75rem", + padding: "0.25rem 0.5rem", + borderRadius: "12px", + margin: "0px" + }} + > + {allowTabTotal} + </span> + )} + </span> + } > - <Accordion.Item eventKey="0"> - <Accordion.Header> - <span className="d-flex align-items-center justify-content-between w-100"> - <span>Allow Rules</span> - </span> - </Accordion.Header> - <Accordion.Body> - <PolicyPermissionItemVertical - serviceCompDetails={selectedServiceComponentDef} - formValues={values} - attrName="policyItems" - msg="Allow rules grants access to resources when the specific criteria are met." - enableResourcePruneDefer={enableResourcePruneDefer} - /> - </Accordion.Body> - </Accordion.Item> + <Accordion + defaultActiveKey={allowAccordionDefault} + alwaysOpen + className="policy-permission-items-accordion" + > + <Accordion.Item eventKey="0"> + <Accordion.Header> + <span className="d-flex align-items-center justify-content-between w-100"> + <span>Allow Rules</span> + </span> + </Accordion.Header> + <Accordion.Body> + <PolicyPermissionItemVertical + serviceCompDetails={selectedServiceComponentDef} + formValues={values} + attrName="policyItems" + msg="Allow rules grants access to resources when the specific criteria are met." + enableResourcePruneDefer={enableResourcePruneDefer} + /> + </Accordion.Body> + </Accordion.Item> - <Accordion.Item eventKey="1"> - <Accordion.Header> - <span className="d-flex align-items-center justify-content-between w-100"> - <span>Exclude from Allow Rules</span> - </span> - </Accordion.Header> - <Accordion.Body> - <PolicyPermissionItemVertical - serviceCompDetails={selectedServiceComponentDef} - formValues={values} - attrName="allowExceptions" - msg="Exclude from Allow Rules grants access to resources when the specific criteria are met." - enableResourcePruneDefer={enableResourcePruneDefer} - /> - </Accordion.Body> - </Accordion.Item> + <Accordion.Item eventKey="1"> + <Accordion.Header> + <span className="d-flex align-items-center justify-content-between w-100"> + <span>Exclude from Allow Rules</span> + </span> + </Accordion.Header> + <Accordion.Body> + <PolicyPermissionItemVertical + serviceCompDetails={selectedServiceComponentDef} + formValues={values} + attrName="allowExceptions" + msg="Exclude from Allow Rules grants access to resources when the specific criteria are met." + enableResourcePruneDefer={enableResourcePruneDefer} + /> + </Accordion.Body> + </Accordion.Item> - <Accordion.Item eventKey="2"> - <Accordion.Header>Deny All Other Accesses</Accordion.Header> - <Accordion.Body> - <p className="stepper-instruction-text mb-3"> - When enabled, this option denies all access types (read, - write, execute, etc.) to every user, group and role - except those explicitly allowed in this policy. Any - user/group/role not listed in the 'Allow' - section will automatically be blocked from accessing the - resource. - </p> + <Accordion.Item eventKey="2"> + <Accordion.Header> + Deny All Other Accesses + </Accordion.Header> + <Accordion.Body> + <p className="stepper-instruction-text mb-3"> + When enabled, this option denies all access types + (read, write, execute, etc.) to every user, group and + role except those explicitly allowed in this policy. + Any user/group/role not listed in the + 'Allow' section will automatically be + blocked from accessing the resource. + </p> - <Field name="isDenyAllElse"> - {({ input }) => ( - <Row> - <Col className="column-1-fixed"> - <span>Deny all other accesses</span> - </Col> - <Col className="resource-toggle-switch"> - <BootstrapSwitchButton - {...input} - checked={input.value || false} - onlabel="True" - onstyle="primary" - offlabel="False" - offstyle="outline-secondary" - size="xs" - width={100} - /> - </Col> - </Row> - )} - </Field> - </Accordion.Body> - </Accordion.Item> - </Accordion> - </Tab> + <Field name="isDenyAllElse"> + {({ input }) => ( + <Row> + <Col className="column-1-fixed"> + <span>Deny all other accesses</span> + </Col> + <Col className="resource-toggle-switch"> + <BootstrapSwitchButton + {...input} + checked={input.value || false} + onlabel="True" + onstyle="primary" + offlabel="False" + offstyle="outline-secondary" + size="xs" + width={100} + /> + </Col> + </Row> + )} + </Field> + </Accordion.Body> + </Accordion.Item> + </Accordion> + </Tab> - <Tab - eventKey="deny" - disabled={isDenyAllElseEnabled} - title={ - <span className="d-flex align-items-center gap-2"> - Deny - {!isDenyAllElseEnabled && denyTabTotal > 0 && ( - <span - className="badge bg-primary text-light" - style={{ - fontSize: "0.75rem", - padding: "0.25rem 0.5rem", - borderRadius: "12px", - margin: "0px" - }} - > - {denyTabTotal} - </span> - )} - </span> - } - > - <Accordion - defaultActiveKey={["0"]} - alwaysOpen - className="policy-permission-items-accordion" + <Tab + eventKey="deny" + disabled={isDenyAllElseEnabled} + title={ + <span className="d-flex align-items-center gap-2"> + Deny + {!isDenyAllElseEnabled && denyTabTotal > 0 && ( + <span + className="badge bg-primary text-light" + style={{ + fontSize: "0.75rem", + padding: "0.25rem 0.5rem", + borderRadius: "12px", + margin: "0px" + }} + > + {denyTabTotal} + </span> + )} + </span> + } > - <Accordion.Item eventKey="0"> - <Accordion.Header> - <span className="d-flex align-items-center justify-content-between w-100"> - <span>Deny Rules</span> - </span> - </Accordion.Header> - <Accordion.Body> - <PolicyPermissionItemVertical - serviceCompDetails={selectedServiceComponentDef} - formValues={values} - attrName="denyPolicyItems" - msg="Deny rules explicitly block access to resources when the specific + <Accordion + defaultActiveKey={["0"]} + alwaysOpen + className="policy-permission-items-accordion" + > + <Accordion.Item eventKey="0"> + <Accordion.Header> + <span className="d-flex align-items-center justify-content-between w-100"> + <span>Deny Rules</span> + </span> + </Accordion.Header> + <Accordion.Body> + <PolicyPermissionItemVertical + serviceCompDetails={selectedServiceComponentDef} + formValues={values} + attrName="denyPolicyItems" + msg="Deny rules explicitly block access to resources when the specific criteria are met." - enableResourcePruneDefer={enableResourcePruneDefer} - /> - </Accordion.Body> - </Accordion.Item> + enableResourcePruneDefer={enableResourcePruneDefer} + /> + </Accordion.Body> + </Accordion.Item> - <Accordion.Item eventKey="1"> - <Accordion.Header> - <span className="d-flex align-items-center justify-content-between w-100"> - <span>Exclude from Deny Rules</span> - </span> - </Accordion.Header> - <Accordion.Body> - <PolicyPermissionItemVertical - serviceCompDetails={selectedServiceComponentDef} - formValues={values} - attrName="denyExceptions" - msg="Exclude from Deny Rules explicitly block access to resources when the specific + <Accordion.Item eventKey="1"> + <Accordion.Header> + <span className="d-flex align-items-center justify-content-between w-100"> + <span>Exclude from Deny Rules</span> + </span> + </Accordion.Header> + <Accordion.Body> + <PolicyPermissionItemVertical + serviceCompDetails={selectedServiceComponentDef} + formValues={values} + attrName="denyExceptions" + msg="Exclude from Deny Rules explicitly block access to resources when the specific criteria are met." - enableResourcePruneDefer={enableResourcePruneDefer} - /> - </Accordion.Body> - </Accordion.Item> - </Accordion> - </Tab> - </Tabs> - </> - )} + enableResourcePruneDefer={enableResourcePruneDefer} + /> + </Accordion.Body> + </Accordion.Item> + </Accordion> + </Tab> + </Tabs> + </> + )} {!isEmpty(policyPermissionItems) && ( <> {policyPermissionItems.map((item) => ( diff --git a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/PolicyPermissionItemVertical.jsx b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/PolicyPermissionItemVertical.jsx index 09d052d64..86895245b 100644 --- a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/PolicyPermissionItemVertical.jsx +++ b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/PolicyPermissionItemVertical.jsx @@ -62,7 +62,7 @@ export default function PolicyPermissionItemVertical({ const [showTagPermissionModalIndex, setShowTagPermissionModalIndex] = useState(null); const form = useForm(); // Get form instance - let getMatchedConstraint = []; + let getMatchedConstraintItems = []; const { conditionDefVal, actionReqsMap, leafResourceTypes } = usePolicyPermissionConditionContext({ @@ -76,7 +76,7 @@ export default function PolicyPermissionItemVertical({ RangerPolicyType.RANGER_CONSTRAINT_POLICY_TYPE.value == formValues.policyType ) { - getMatchedConstraint = getMatchedConstraintDefs( + getMatchedConstraintItems = getMatchedConstraintDefs( serviceCompDetails, formValues ); @@ -84,19 +84,22 @@ export default function PolicyPermissionItemVertical({ const accessTypeOptions = getAccessTypesByResource( formValues, serviceCompDetails, - getMatchedConstraint + getMatchedConstraintItems ); // For constraint policy: leaf resource accessTypeRestrictions (computed once, reused per row) const leafRestrictions = - RangerPolicyType.RANGER_CONSTRAINT_POLICY_TYPE.value == formValues.policyType + RangerPolicyType.RANGER_CONSTRAINT_POLICY_TYPE.value == + formValues.policyType ? getLeafAccessTypeRestrictions(formValues, serviceCompDetails) : null; - const constraintSelectOptions = getMatchedConstraint.map((constraint) => ({ - label: constraint.label || constraint.name, - value: constraint.name - })); + const constraintSelectOptions = getMatchedConstraintItems?.map( + (constraint) => ({ + label: constraint.label || constraint.name, + value: constraint.name + }) + ); const getMaskingAccessTypeOptions = (index) => { if (serviceCompDetails?.dataMaskDef?.maskTypes?.length > 0) { @@ -242,169 +245,306 @@ export default function PolicyPermissionItemVertical({ ? rowAccessTypeOptions : accessTypeOptions; return ( - <React.Fragment key={name}> - <Row key={name} className="py-3"> - {/* Rule Header */} - <Col sm={11}> - {RangerPolicyType.RANGER_CONSTRAINT_POLICY_TYPE.value == - formValues?.policyType && ( + <React.Fragment key={name}> + <Row key={name} className="py-3"> + {/* Rule Header */} + <Col sm={11}> + {RangerPolicyType.RANGER_CONSTRAINT_POLICY_TYPE.value == + formValues?.policyType && ( + <Col xs={12} className="mb-4"> + <Row> + <Col className="column-1-fixed text-end"> + <FormB.Label>Constraint Name</FormB.Label> + </Col> + <> + <Col> + <Field + name={`${name}.constraintInfo.constraintName`} + validate={required} + > + {({ input, meta }) => ( + <> + <Select + inputId={`${name}-constraint-name`} + options={constraintSelectOptions} + value={input.value} + onChange={(option) => + handleConstraintNameChange( + input, + name, + option + ) + } + onBlur={input.onBlur} + placeholder="Select constraint" + tabSelectsValue={false} + isClearable + isDisabled={ + constraintSelectOptions.length === 0 + } + /> + {meta.touched && meta.error && ( + <span className="text-danger d-block mt-1"> + {meta.error} + </span> + )} + </> + )} + </Field> + </Col> + <Col className="column-3-fixed"></Col> + </> + </Row> + </Col> + )} + {/* Permissions Section */} <Col xs={12} className="mb-4"> <Row> <Col className="column-1-fixed text-end"> - <FormB.Label>Constraint Name</FormB.Label> + <FormB.Label>Permissions</FormB.Label> </Col> - <> - <Col> - <Field - name={`${name}.constraintInfo.constraintName`} - validate={required} - > + <Col> + {isConstraintPolicy && !rowConstraintName ? ( + <p className="text-muted mb-0"> + {constraintSelectFirstMsg} + </p> + ) : serviceCompDetails?.name === "tag" ? ( + /* Tag-based policy - use modal with table layout */ + <Field name={`${name}.accesses`}> {({ input, meta }) => ( <> - <Select - inputId={`${name}-constraint-name`} - options={constraintSelectOptions} - value={input.value} - onChange={(option) => - handleConstraintNameChange( - input, - name, - option - ) - } - onBlur={input.onBlur} - placeholder="Select constraint" - tabSelectsValue={false} - isClearable - isDisabled={ - constraintSelectOptions.length === 0 - } - /> - {meta.touched && meta.error && ( - <span className="text-danger d-block mt-1"> + {/* Container with flex layout for table + button side by side */} + {input.value?.tableList?.length > 0 ? ( + <Container fluid className="p-0"> + <Row> + <Col> + <div style={{ flex: 1 }}> + <Table + bordered + size="sm" + className="condition-group-table" + > + <thead> + <tr> + <th className="text-center"> + Service + </th> + <th className="text-center"> + Permissions + </th> + </tr> + </thead> + <tbody> + {input.value.tableList.map( + (tableItem, idx) => ( + <tr + key={`${tableItem.serviceName}-${idx}`} + > + <td className="white-space-nowrap"> + {toUpper( + tableItem.serviceName + )} + </td> + <td className="text-word-break"> + {displayTagPermissionItem( + tableItem.permission + ).join(", ")} + </td> + </tr> + ) + )} + </tbody> + </Table> + </div> + </Col> + <Col className="column-3-fixed"> + <Button + variant="link" + onClick={() => + setShowTagPermissionModalIndex( + index + ) + } + className="text-primary" + style={{ whiteSpace: "nowrap" }} + > + Edit Permissions + </Button> + </Col> + </Row> + </Container> + ) : ( + <Col> + <Button + variant="link" + onClick={() => + setShowTagPermissionModalIndex( + index + ) + } + className="text-primary" + style={{ whiteSpace: "nowrap" }} + > + Add Permissions + </Button> + </Col> + )} + {/* Tag Permission Modal */} + {showTagPermissionModalIndex === index && ( + <TagBasePermissionItem + options={permissionAccessOptions} + inputVal={{ + ...input, + onChange: (value) => { + input.onChange(value); + // Don't close here, let the modal handle it via onClose + } + }} + formValues={formValues} + dataMaskIndex={index} + serviceCompDetails={serviceCompDetails} + attrName={attrName} + showModal={true} + handleCloseModal={() => + setShowTagPermissionModalIndex(null) + } + /> + )} + {meta.error && ( + <div className="text-danger small mt-1"> + <i className="fa fa-exclamation-circle me-1" /> {meta.error} - </span> + </div> )} </> )} </Field> - </Col> - <Col className="column-3-fixed"></Col> - </> + ) : ( + /* Non-tag policy - use checkboxes */ + <Field name={`${name}.accesses`}> + {({ input, meta }) => { + const currentValue = input.value || []; + const allSelected = + permissionAccessOptions.length > 0 && + permissionAccessOptions.every((access) => + currentValue.some( + (a) => a.type === access.value + ) + ); + + const handleSelectAll = () => { + if (allSelected) { + // Deselect all + input.onChange([]); + } else { + // Select all + const allAccesses = + permissionAccessOptions.map((a) => ({ + type: a.value, + isAllowed: true, + label: a.label + })); + input.onChange(allAccesses); + } + }; + + return ( + <div> + <Container fluid className="p-0"> + <div className="gx-3 gy-1 permissions-checkboxes"> + {/* Select All Link - Only show if more than 1 option */} + {permissionAccessOptions.length > + 1 && ( + <div className="mb-1"> + <FormB.Check + type="checkbox" + id={`${name}-selectAll`} + label="Select All" + checked={allSelected} + onChange={handleSelectAll} + className="text-nowrap text-primary" + /> + </div> + )} + {permissionAccessOptions.map( + (access) => { + const isChecked = + Array.isArray(input.value) && + input.value.some( + (a) => a.type === access.value + ); + + return ( + <div + className="mb-1" + key={access.value} + > + <FormB.Check + type="checkbox" + id={`${name}-${access.value}`} + label={access.label} + checked={isChecked} + onChange={(e) => { + const currentValue = + input.value || []; + if (e.target.checked) { + input.onChange([ + ...currentValue, + { + type: access.value, + isAllowed: true, + label: access.label + } + ]); + } else { + input.onChange( + currentValue.filter( + (a) => + a.type !== + access.value + ) + ); + } + }} + className="text-nowrap" + /> + </div> + ); + } + )} + </div> + </Container> + {meta.error && ( + <div className="text-danger small mt-1"> + <i className="fa fa-exclamation-circle me-1" /> + {meta.error} + </div> + )} + </div> + ); + }} + </Field> + )} + </Col> </Row> </Col> - )} - {/* Permissions Section */} - <Col xs={12} className="mb-4"> - <Row> - <Col className="column-1-fixed text-end"> - <FormB.Label>Permissions</FormB.Label> - </Col> - <Col> - {isConstraintPolicy && !rowConstraintName ? ( - <p className="text-muted mb-0"> - {constraintSelectFirstMsg} - </p> - ) : serviceCompDetails?.name === "tag" ? ( - /* Tag-based policy - use modal with table layout */ - <Field name={`${name}.accesses`}> + + {/* Users, Groups, Roles Selection */} + <Col xl={12} className="mb-4"> + <Row> + <Col className="column-1-fixed text-end"> + <FormB.Label>Users, Groups, Roles</FormB.Label> + </Col> + <Col> + <Field name={`${name}.users`}> {({ input, meta }) => ( <> - {/* Container with flex layout for table + button side by side */} - {input.value?.tableList?.length > 0 ? ( - <Container fluid className="p-0"> - <Row> - <Col> - <div style={{ flex: 1 }}> - <Table - bordered - size="sm" - className="condition-group-table" - > - <thead> - <tr> - <th className="text-center"> - Service - </th> - <th className="text-center"> - Permissions - </th> - </tr> - </thead> - <tbody> - {input.value.tableList.map( - (tableItem, idx) => ( - <tr - key={`${tableItem.serviceName}-${idx}`} - > - <td className="white-space-nowrap"> - {toUpper( - tableItem.serviceName - )} - </td> - <td className="text-word-break"> - {displayTagPermissionItem( - tableItem.permission - ).join(", ")} - </td> - </tr> - ) - )} - </tbody> - </Table> - </div> - </Col> - <Col className="column-3-fixed"> - <Button - variant="link" - onClick={() => - setShowTagPermissionModalIndex( - index - ) - } - className="text-primary" - style={{ whiteSpace: "nowrap" }} - > - Edit Permissions - </Button> - </Col> - </Row> - </Container> - ) : ( - <Col> - <Button - variant="link" - onClick={() => - setShowTagPermissionModalIndex(index) - } - className="text-primary" - style={{ whiteSpace: "nowrap" }} - > - Add Permissions - </Button> - </Col> - )} - {/* Tag Permission Modal */} - {showTagPermissionModalIndex === index && ( - <TagBasePermissionItem - options={permissionAccessOptions} - inputVal={{ - ...input, - onChange: (value) => { - input.onChange(value); - // Don't close here, let the modal handle it via onClose - } - }} - formValues={formValues} - dataMaskIndex={index} - serviceCompDetails={serviceCompDetails} - attrName={attrName} - showModal={true} - handleCloseModal={() => - setShowTagPermissionModalIndex(null) - } - /> - )} + <UsersGroupsAndRolesSelectionComponent + value={input.value} + onChange={input.onChange} + name={`${name}.users`} + placeholder="Select users, groups, roles..." + /> {meta.error && ( <div className="text-danger small mt-1"> <i className="fa fa-exclamation-circle me-1" /> @@ -414,218 +554,100 @@ export default function PolicyPermissionItemVertical({ </> )} </Field> - ) : ( - /* Non-tag policy - use checkboxes */ - <Field name={`${name}.accesses`}> - {({ input, meta }) => { - const currentValue = input.value || []; - const allSelected = - permissionAccessOptions.length > 0 && - permissionAccessOptions.every((access) => - currentValue.some( - (a) => a.type === access.value - ) - ); + </Col> + </Row> + </Col> - const handleSelectAll = () => { - if (allSelected) { - // Deselect all - input.onChange([]); - } else { - // Select all - const allAccesses = - permissionAccessOptions.map((a) => ({ - type: a.value, - isAllowed: true, - label: a.label - })); - input.onChange(allAccesses); - } - }; + {/* Rule Conditions */} + {serviceCompDetails?.policyConditions?.length > 0 && ( + <Col xs={12} className="mb-4"> + <Row> + <Col className="column-1-fixed text-end"> + <FormB.Label>Rule Conditions</FormB.Label> + </Col> + {fields.value[index]?.conditions && + !isEmpty(fields.value[index].conditions) ? ( + <> + <Col> + <div style={{ flex: 1 }}> + <Table + bordered + size="sm" + className="condition-group-table" + > + <thead> + <tr> + <th className="text-center"> + Condition + </th> + <th className="text-center">Value</th> + </tr> + </thead> + <tbody> + {Object.keys( + fields.value[index].conditions + ).map((keyName) => { + const conditionValue = + fields.value[index].conditions[ + keyName + ]; - return ( - <div> - <Container fluid className="p-0"> - <div className="gx-3 gy-1 permissions-checkboxes"> - {/* Select All Link - Only show if more than 1 option */} - {permissionAccessOptions.length > 1 && ( - <div className="mb-1"> - <FormB.Check - type="checkbox" - id={`${name}-selectAll`} - label="Select All" - checked={allSelected} - onChange={handleSelectAll} - className="text-nowrap text-primary" - /> - </div> - )} - {permissionAccessOptions.map( - (access) => { - const isChecked = - Array.isArray(input.value) && - input.value.some( - (a) => a.type === access.value + if ( + conditionValue != "" && + conditionValue != null + ) { + let conditionObj = find( + serviceCompDetails?.policyConditions, + function (m) { + return m.name == keyName; + } ); - return ( - <div - className="mb-1" - key={access.value} - > - <FormB.Check - type="checkbox" - id={`${name}-${access.value}`} - label={access.label} - checked={isChecked} - onChange={(e) => { - const currentValue = - input.value || []; - if (e.target.checked) { - input.onChange([ - ...currentValue, - { - type: access.value, - isAllowed: true, - label: access.label - } - ]); - } else { - input.onChange( - currentValue.filter( - (a) => - a.type !== - access.value - ) - ); - } - }} - className="text-nowrap" - /> - </div> - ); - } - )} - </div> - </Container> - {meta.error && ( - <div className="text-danger small mt-1"> - <i className="fa fa-exclamation-circle me-1" /> - {meta.error} - </div> - )} - </div> - ); - }} - </Field> - )} - </Col> - </Row> - </Col> - - {/* Users, Groups, Roles Selection */} - <Col xl={12} className="mb-4"> - <Row> - <Col className="column-1-fixed text-end"> - <FormB.Label>Users, Groups, Roles</FormB.Label> - </Col> - <Col> - <Field name={`${name}.users`}> - {({ input, meta }) => ( - <> - <UsersGroupsAndRolesSelectionComponent - value={input.value} - onChange={input.onChange} - name={`${name}.users`} - placeholder="Select users, groups, roles..." - /> - {meta.error && ( - <div className="text-danger small mt-1"> - <i className="fa fa-exclamation-circle me-1" /> - {meta.error} + return ( + <tr key={keyName}> + <td className="white-space-nowrap"> + {getPolicyConditionDisplayLbl( + conditionObj?.label + )} + </td> + <td className="text-word-break"> + {isArray(conditionValue) + ? conditionValue + .map((m) => + typeof m === + "object" && + m?.value != null + ? m.value + : m + ) + .join(", ") + : conditionValue} + </td> + </tr> + ); + } + return null; + })} + </tbody> + </Table> </div> - )} + </Col> + <Col className="column-3-fixed"> + {/* Edit/Add Button - Always visible on the right */} + <Button + variant="link" + onClick={() => + setShowConditionsModalIndex(index) + } + className="text-primary" + style={{ whiteSpace: "nowrap" }} + > + Edit Conditions + </Button> + </Col> </> - )} - </Field> - </Col> - </Row> - </Col> - - {/* Rule Conditions */} - {serviceCompDetails?.policyConditions?.length > 0 && ( - <Col xs={12} className="mb-4"> - <Row> - <Col className="column-1-fixed text-end"> - <FormB.Label>Rule Conditions</FormB.Label> - </Col> - {fields.value[index]?.conditions && - !isEmpty(fields.value[index].conditions) ? ( - <> + ) : ( <Col> - <div style={{ flex: 1 }}> - <Table - bordered - size="sm" - className="condition-group-table" - > - <thead> - <tr> - <th className="text-center"> - Condition - </th> - <th className="text-center">Value</th> - </tr> - </thead> - <tbody> - {Object.keys( - fields.value[index].conditions - ).map((keyName) => { - const conditionValue = - fields.value[index].conditions[ - keyName - ]; - - if ( - conditionValue != "" && - conditionValue != null - ) { - let conditionObj = find( - serviceCompDetails?.policyConditions, - function (m) { - return m.name == keyName; - } - ); - - return ( - <tr key={keyName}> - <td className="white-space-nowrap"> - {getPolicyConditionDisplayLbl( - conditionObj?.label - )} - </td> - <td className="text-word-break"> - {isArray(conditionValue) - ? conditionValue - .map((m) => - typeof m === "object" && - m?.value != null - ? m.value - : m - ) - .join(", ") - : conditionValue} - </td> - </tr> - ); - } - return null; - })} - </tbody> - </Table> - </div> - </Col> - <Col className="column-3-fixed"> {/* Edit/Add Button - Always visible on the right */} <Button variant="link" @@ -635,366 +657,298 @@ export default function PolicyPermissionItemVertical({ className="text-primary" style={{ whiteSpace: "nowrap" }} > - Edit Conditions + Add Conditions </Button> </Col> - </> - ) : ( - <Col> - {/* Edit/Add Button - Always visible on the right */} - <Button - variant="link" - onClick={() => - setShowConditionsModalIndex(index) - } - className="text-primary" - style={{ whiteSpace: "nowrap" }} - > - Add Conditions - </Button> - </Col> - )} - {showConditionsModalIndex === index && ( - <PolicyConditionsComp - // Visibility & Control - showModal={true} - modalHeader="Rule Conditions" - handleCloseModal={() => - setShowConditionsModalIndex(null) - } - // Data & Value Binding - scope="policyItem" - policyConditionDetails={conditionDefVal} - inputVal={{ - value: fields.value[index]?.conditions || {}, - onChange: (newValue) => { - form.change(`${name}.conditions`, newValue); + )} + {showConditionsModalIndex === index && ( + <PolicyConditionsComp + // Visibility & Control + showModal={true} + modalHeader="Rule Conditions" + handleCloseModal={() => + setShowConditionsModalIndex(null) } - }} - // Service & Actions Metadata - servicedefName={serviceCompDetails?.name} - serviceDefOptions={serviceCompDetails?.options} - actionReqsMap={actionReqsMap} - // Filter Context Evaluation - actionFilterContext={{ - leafResourceTypes, - accessTypeDefs: serviceCompDetails?.accessTypes, - selectedAccessTypes: - getSelectedAccessTypesForRow( - formValues, - attrName, - index - ) - }} + // Data & Value Binding + scope="policyItem" + policyConditionDetails={conditionDefVal} + inputVal={{ + value: fields.value[index]?.conditions || {}, + onChange: (newValue) => { + form.change(`${name}.conditions`, newValue); + } + }} + // Service & Actions Metadata + servicedefName={serviceCompDetails?.name} + serviceDefOptions={serviceCompDetails?.options} + actionReqsMap={actionReqsMap} + // Filter Context Evaluation + actionFilterContext={{ + leafResourceTypes, + accessTypeDefs: + serviceCompDetails?.accessTypes, + selectedAccessTypes: + getSelectedAccessTypesForRow( + formValues, + attrName, + index + ) + }} + /> + )} + </Row> + </Col> + )} + + {isConstraintPolicy && ( + <Col xs={12} className="mb-4"> + {!rowConstraintName ? ( + <Row> + <Col className="column-1-fixed text-end"> + <FormB.Label>Parameters</FormB.Label> + </Col> + <> + <Col> + <p className="text-muted mb-0"> + {constraintSelectFirstMsg} + </p> + </Col> + <Col className="column-3-fixed"></Col> + </> + </Row> + ) : ( + <ConstraintPolicyParameterFields + parameters={rowConstraintDef?.parameters || []} + fieldNamePrefix={`${name}.constraintInfo.parameters`} + serviceCompDetails={serviceCompDetails} /> )} - </Row> - </Col> - )} + </Col> + )} - {isConstraintPolicy && ( - <Col xs={12} className="mb-4"> - {!rowConstraintName ? ( + {/* Data Masking Option (for Masking policies) */} + {RangerPolicyType.RANGER_MASKING_POLICY_TYPE.value == + formValues?.policyType && ( + <Col xs={12} className="mb-4"> <Row> <Col className="column-1-fixed text-end"> - <FormB.Label>Parameters</FormB.Label> + <FormB.Label>Select Masking Option</FormB.Label> </Col> - <> - <Col> - <p className="text-muted mb-0"> - {constraintSelectFirstMsg} - </p> - </Col> - <Col className="column-3-fixed"></Col> - </> - </Row> - ) : ( - <ConstraintPolicyParameterFields - parameters={rowConstraintDef?.parameters || []} - fieldNamePrefix={`${name}.constraintInfo.parameters`} - serviceCompDetails={serviceCompDetails} - /> - )} - </Col> - )} - - {/* Data Masking Option (for Masking policies) */} - {RangerPolicyType.RANGER_MASKING_POLICY_TYPE.value == - formValues?.policyType && ( - <Col xs={12} className="mb-4"> - <Row> - <Col className="column-1-fixed text-end"> - <FormB.Label>Select Masking Option</FormB.Label> - </Col> - <Col> - {serviceCompDetails?.name !== "tag" ? ( - <> - {" "} - <Field name={`${name}.dataMaskInfo`}> - {({ input, meta }) => { - const maskingOptions = - getMaskingAccessTypeOptions(index); - return ( - <> - <Container fluid className="p-0"> - <Row> - {/* Masking Type Radio Buttons */} - {maskingOptions.map((maskType) => ( - <Col - xxl={3} - xl={4} - md={6} - sm={12} - className="mb-3" - key={maskType.value} - > - <FormB.Check - type="radio" - id={`${name}-mask-${maskType.value}`} - name={`${name}.dataMaskInfo`} - label={maskType.label} - checked={ - input.value?.value === - maskType.value - } - onChange={() => - input.onChange({ - label: maskType.label, - value: maskType.value - }) - } - className="mb-2" - /> - </Col> - ))} - </Row> - </Container> - {/* Custom Value Expression (shown when "Custom" is selected) */} - {input.value?.label === "Custom" && ( - <div className="mt-2"> - <Field - name={`${name}.dataMaskInfo.valueExpr`} - validate={required} - > - {({ - input: exprInput, - meta: exprMeta - }) => ( - <div> - <FormB.Control - {...exprInput} - type="text" - placeholder="Enter masked value or expression..." - isInvalid={ - exprMeta.touched && - exprMeta.error - } - /> - {exprMeta.touched && - exprMeta.error && ( - <FormB.Control.Feedback type="invalid"> - {exprMeta.error} - </FormB.Control.Feedback> - )} - </div> + <Col> + {serviceCompDetails?.name !== "tag" ? ( + <> + {" "} + <Field name={`${name}.dataMaskInfo`}> + {({ input, meta }) => { + const maskingOptions = + getMaskingAccessTypeOptions(index); + return ( + <> + <Container fluid className="p-0"> + <Row> + {/* Masking Type Radio Buttons */} + {maskingOptions.map( + (maskType) => ( + <Col + xxl={3} + xl={4} + md={6} + sm={12} + className="mb-3" + key={maskType.value} + > + <FormB.Check + type="radio" + id={`${name}-mask-${maskType.value}`} + name={`${name}.dataMaskInfo`} + label={maskType.label} + checked={ + input.value?.value === + maskType.value + } + onChange={() => + input.onChange({ + label: maskType.label, + value: maskType.value + }) + } + className="mb-2" + /> + </Col> + ) )} - </Field> - </div> - )} - {/* Form-level validation error - only render if string (nested field errors bubble up as objects) */} - {typeof meta.error === "string" && ( - <div className="text-danger small mt-1"> - <i className="fa fa-exclamation-circle me-1" /> - {meta.error} - </div> - )} - </> - ); - }} - </Field> - </> - ) : formValues?.dataMaskPolicyItems?.[index] - ?.accesses?.tableList?.length > 0 ? ( - <> - {" "} - <Field name={`${name}.dataMaskInfo`}> - {({ input, meta }) => { - const maskingOptions = - getMaskingAccessTypeOptions(index); - return ( - <> - <Container fluid className="p-0"> - <Row> - {/* Masking Type Radio Buttons */} - {maskingOptions.map((maskType) => ( - <Col - xxl={3} - xl={4} - md={6} - sm={12} - className="mb-3" - key={maskType.value} + </Row> + </Container> + {/* Custom Value Expression (shown when "Custom" is selected) */} + {input.value?.label === "Custom" && ( + <div className="mt-2"> + <Field + name={`${name}.dataMaskInfo.valueExpr`} + validate={required} > - <FormB.Check - type="radio" - id={`${name}-mask-${maskType.value}`} - name={`${name}.dataMaskInfo`} - label={maskType.label} - checked={ - input.value?.value === - maskType.value - } - onChange={() => - input.onChange({ - label: maskType.label, - value: maskType.value - }) - } - className="mb-2" - /> - </Col> - ))} - </Row> - </Container> - {/* Custom Value Expression (shown when "Custom" is selected) */} - {input.value?.label === "Custom" && ( - <div className="mt-2"> - <Field - name={`${name}.dataMaskInfo.valueExpr`} - validate={required} - > - {({ - input: exprInput, - meta: exprMeta - }) => ( - <div> - <FormB.Control - {...exprInput} - type="text" - placeholder="Enter masked value or expression..." - isInvalid={ - exprMeta.touched && - exprMeta.error - } - /> - {exprMeta.touched && - exprMeta.error && ( - <FormB.Control.Feedback type="invalid"> - {exprMeta.error} - </FormB.Control.Feedback> - )} - </div> + {({ + input: exprInput, + meta: exprMeta + }) => ( + <div> + <FormB.Control + {...exprInput} + type="text" + placeholder="Enter masked value or expression..." + isInvalid={ + exprMeta.touched && + exprMeta.error + } + /> + {exprMeta.touched && + exprMeta.error && ( + <FormB.Control.Feedback type="invalid"> + {exprMeta.error} + </FormB.Control.Feedback> + )} + </div> + )} + </Field> + </div> + )} + {/* Form-level validation error - only render if string (nested field errors bubble up as objects) */} + {typeof meta.error === "string" && ( + <div className="text-danger small mt-1"> + <i className="fa fa-exclamation-circle me-1" /> + {meta.error} + </div> + )} + </> + ); + }} + </Field> + </> + ) : formValues?.dataMaskPolicyItems?.[index] + ?.accesses?.tableList?.length > 0 ? ( + <> + {" "} + <Field name={`${name}.dataMaskInfo`}> + {({ input, meta }) => { + const maskingOptions = + getMaskingAccessTypeOptions(index); + return ( + <> + <Container fluid className="p-0"> + <Row> + {/* Masking Type Radio Buttons */} + {maskingOptions.map( + (maskType) => ( + <Col + xxl={3} + xl={4} + md={6} + sm={12} + className="mb-3" + key={maskType.value} + > + <FormB.Check + type="radio" + id={`${name}-mask-${maskType.value}`} + name={`${name}.dataMaskInfo`} + label={maskType.label} + checked={ + input.value?.value === + maskType.value + } + onChange={() => + input.onChange({ + label: maskType.label, + value: maskType.value + }) + } + className="mb-2" + /> + </Col> + ) )} - </Field> - </div> - )} - {/* Form-level validation error - only render if string (nested field errors bubble up as objects) */} - {typeof meta.error === "string" && ( - <div className="text-danger small mt-1"> - <i className="fa fa-exclamation-circle me-1" /> - {meta.error} - </div> - )} - </> - ); - }} - </Field> - </> - ) : ( - <Col> - <div className="text-muted fst-italic"> - Please add permissions to select masking - option. - </div> - </Col> - )} - </Col> - </Row> - </Col> - )} - - {/* Row Level Filter (for Row Filter policies) */} - {RangerPolicyType.RANGER_ROW_FILTER_POLICY_TYPE.value == - formValues?.policyType && ( - <Col xs={12} className="mb-4"> - <Row> - <Col className="column-1-fixed text-end"> - <FormB.Label>Row Level Filter</FormB.Label> - </Col> - <Col> - <Field name={`${name}.rowFilterInfo`}> - {({ input, meta }) => ( - <div> - <FormB.Control - {...input} - as="textarea" - rows={3} - placeholder="Enter filter expression (e.g., country = 'US')" - isInvalid={!!meta.error} - /> - {meta.error && ( - <FormB.Control.Feedback type="invalid"> - {meta.error} - </FormB.Control.Feedback> - )} - </div> + </Row> + </Container> + {/* Custom Value Expression (shown when "Custom" is selected) */} + {input.value?.label === "Custom" && ( + <div className="mt-2"> + <Field + name={`${name}.dataMaskInfo.valueExpr`} + validate={required} + > + {({ + input: exprInput, + meta: exprMeta + }) => ( + <div> + <FormB.Control + {...exprInput} + type="text" + placeholder="Enter masked value or expression..." + isInvalid={ + exprMeta.touched && + exprMeta.error + } + /> + {exprMeta.touched && + exprMeta.error && ( + <FormB.Control.Feedback type="invalid"> + {exprMeta.error} + </FormB.Control.Feedback> + )} + </div> + )} + </Field> + </div> + )} + {/* Form-level validation error - only render if string (nested field errors bubble up as objects) */} + {typeof meta.error === "string" && ( + <div className="text-danger small mt-1"> + <i className="fa fa-exclamation-circle me-1" /> + {meta.error} + </div> + )} + </> + ); + }} + </Field> + </> + ) : ( + <Col> + <div className="text-muted fst-italic"> + Please add permissions to select masking + option. + </div> + </Col> )} - </Field> - </Col> - </Row> - </Col> - )} + </Col> + </Row> + </Col> + )} - {/* Delegate Admin (for Access policies only, non-tag) */} - {RangerPolicyType.RANGER_ACCESS_POLICY_TYPE.value == - formValues?.policyType && - serviceCompDetails?.name !== "tag" && ( - <Col xs={12} className="mb-0"> + {/* Row Level Filter (for Row Filter policies) */} + {RangerPolicyType.RANGER_ROW_FILTER_POLICY_TYPE.value == + formValues?.policyType && ( + <Col xs={12} className="mb-4"> <Row> <Col className="column-1-fixed text-end"> - <FormB.Label htmlFor={`${name}.delegateAdmin`}> - Delegate Admin - <span className="ps-2"> - <CustomTooltip - placement="right" - content={ - <p - className="pd-5" - style={{ fontSize: "small" }} - > - Delegate Admin grants users or groups or - roles the authority to create and manage - policies, but strictly within the - specific resource boundaries defined by - the original policy. - </p> - } - icon="fa-fw fa fa-info-circle" - /> - </span> - </FormB.Label> + <FormB.Label>Row Level Filter</FormB.Label> </Col> <Col> - <Field - className="form-control" - name={`${name}.delegateAdmin`} - data-js="delegatedAdmin" - data-cy="delegatedAdmin" - type="checkbox" - > - {({ input }) => ( - <div - className="d-flex align-items-center" - style={{ height: "21px" }} - > - <input + <Field name={`${name}.rowFilterInfo`}> + {({ input, meta }) => ( + <div> + <FormB.Control {...input} - type="checkbox" - onChange={handleDelegateAdminChange( - input, - index - )} - />{" "} + as="textarea" + rows={3} + placeholder="Enter filter expression (e.g., country = 'US')" + isInvalid={!!meta.error} + /> + {meta.error && ( + <FormB.Control.Feedback type="invalid"> + {meta.error} + </FormB.Control.Feedback> + )} </div> )} </Field> @@ -1002,40 +956,98 @@ export default function PolicyPermissionItemVertical({ </Row> </Col> )} - </Col> - <Col - sm={1} - className="mb-4 justify-content-end d-flex align-items-start" - > - {fields.length > 1 ? ( - <Button - variant="danger" - size="sm" - title="Remove" - onClick={() => fields.remove(index)} - data-action="delete" - data-cy="delete" - > - <i className="fa-fw fa fa-remove"></i> - </Button> - ) : ( - // Show clear button only when there's one item and it has data - <Button - variant="danger" - size="sm" - title="Clear item data" - onClick={() => clearPermissionItem(index)} - data-action="clear" - data-cy="clear" - > - <i className="fa-fw fa fa-remove"></i> - </Button> - )} - </Col> - </Row> - <hr /> - </React.Fragment> - ); + + {/* Delegate Admin (for Access policies only, non-tag) */} + {RangerPolicyType.RANGER_ACCESS_POLICY_TYPE.value == + formValues?.policyType && + serviceCompDetails?.name !== "tag" && ( + <Col xs={12} className="mb-0"> + <Row> + <Col className="column-1-fixed text-end"> + <FormB.Label htmlFor={`${name}.delegateAdmin`}> + Delegate Admin + <span className="ps-2"> + <CustomTooltip + placement="right" + content={ + <p + className="pd-5" + style={{ fontSize: "small" }} + > + Delegate Admin grants users or groups + or roles the authority to create and + manage policies, but strictly within + the specific resource boundaries + defined by the original policy. + </p> + } + icon="fa-fw fa fa-info-circle" + /> + </span> + </FormB.Label> + </Col> + <Col> + <Field + className="form-control" + name={`${name}.delegateAdmin`} + data-js="delegatedAdmin" + data-cy="delegatedAdmin" + type="checkbox" + > + {({ input }) => ( + <div + className="d-flex align-items-center" + style={{ height: "21px" }} + > + <input + {...input} + type="checkbox" + onChange={handleDelegateAdminChange( + input, + index + )} + />{" "} + </div> + )} + </Field> + </Col> + </Row> + </Col> + )} + </Col> + <Col + sm={1} + className="mb-4 justify-content-end d-flex align-items-start" + > + {fields.length > 1 ? ( + <Button + variant="danger" + size="sm" + title="Remove" + onClick={() => fields.remove(index)} + data-action="delete" + data-cy="delete" + > + <i className="fa-fw fa fa-remove"></i> + </Button> + ) : ( + // Show clear button only when there's one item and it has data + <Button + variant="danger" + size="sm" + title="Clear item data" + onClick={() => clearPermissionItem(index)} + data-action="clear" + data-cy="clear" + > + <i className="fa-fw fa fa-remove"></i> + </Button> + )} + </Col> + </Row> + <hr /> + </React.Fragment> + ); })} {/* Add More Rules Button */} diff --git a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyPermissionItem.jsx b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyPermissionItem.jsx index 7e9c36998..425fd4cc2 100644 --- a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyPermissionItem.jsx +++ b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyPermissionItem.jsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useRef, useState } from "react"; +import React, { useRef, useState, useEffect } from "react"; import { Table, Button, Form } from "react-bootstrap"; import { FieldArray } from "react-final-form-arrays"; import { Col } from "react-bootstrap"; @@ -113,6 +113,27 @@ export default function PolicyPermissionItem(props) { })); const constraintSelectFirstMsg = "Please select constraint name first"; + // When resources change, clear any constraint name that is no longer valid for + // the selected resources (avoids stale state without a render-time side-effect). + useEffect(() => { + if (!isConstraintPolicy || !changePolicyItemPermissions) { + return; + } + const allowedNames = new Set(constraintSelectOptions.map((o) => o.value)); + const items = formValues?.[attrName] || []; + items.forEach((item, index) => { + const rowConstraintName = getConstraintNameFromField( + item?.constraintInfo?.constraintName + ); + if (rowConstraintName && !allowedNames.has(rowConstraintName)) { + form.change(`${attrName}[${index}].constraintInfo.constraintName`, null); + form.change(`${attrName}[${index}].constraintInfo.parameters`, undefined); + form.change(`${attrName}[${index}].accesses`, undefined); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [constraintSelectOptions.map((o) => o.value).join(","), isConstraintPolicy, changePolicyItemPermissions]); + if (serviceCompDetails?.policyConditions?.length > 0) { permList.push("Rule Conditions"); } @@ -689,54 +710,31 @@ export default function PolicyPermissionItem(props) { ); } if (colName == "Constraint Name") { - const allowedNames = new Set( - constraintSelectOptions.map((o) => o.value) - ); - const rowConstraintName = getConstraintNameFromField( - fields.value[index]?.constraintInfo?.constraintName - ); return ( <td key={colName} className="align-middle"> <Field name={`${name}.constraintInfo.constraintName`} - render={({ input }) => { - if ( - changePolicyItemPermissions && - rowConstraintName && - !allowedNames.has(rowConstraintName) - ) { - form.change( - `${name}.constraintInfo.constraintName`, - null - ); - form.change( - `${name}.constraintInfo.parameters`, - undefined - ); - form.change(`${name}.accesses`, undefined); - } - return ( - <Select - inputId={`${name}-constraint-name`} - options={constraintSelectOptions} - value={input.value} - onChange={(option) => - handleConstraintNameChange( - input, - name, - option - ) - } - onBlur={input.onBlur} - placeholder="Select constraint" - isClearable - isDisabled={ - constraintSelectOptions.length === 0 - } - styles={selectInputCustomStyles} - /> - ); - }} + render={({ input }) => ( + <Select + inputId={`${name}-constraint-name`} + options={constraintSelectOptions} + value={input.value} + onChange={(option) => + handleConstraintNameChange( + input, + name, + option + ) + } + onBlur={input.onBlur} + placeholder="Select constraint" + isClearable + isDisabled={ + constraintSelectOptions.length === 0 + } + styles={selectInputCustomStyles} + /> + )} /> </td> );
