This is an automated email from the ASF dual-hosted git repository.
fimugdha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/master by this push:
new a93c43f6b RANGER-5685 : Follow-on UI side review changes for
RANGER-5628 (#1127)
a93c43f6b is described below
commit a93c43f6b084874cab0ea70fa86509fb77e2a014
Author: Mugdha Varadkar <[email protected]>
AuthorDate: Wed Aug 5 17:39:03 2026 +0530
RANGER-5685 : Follow-on UI side review changes for RANGER-5628 (#1127)
---
.../src/components/CommonComponents.jsx | 6 +-
.../react-webapp/src/components/Editable.jsx | 75 ++++----
.../hooks/usePolicyPermissionConditionContext.js | 88 +++++++++
.../src/hooks/usePruneStaleConditions.js | 148 +++++++--------
.../react-webapp/src/utils/policyConditionUtils.js | 198 ++++++++++++++++++---
.../views/PolicyListing/AddUpdatePolicyForm.jsx | 16 +-
.../EditNewPolicyForm/EditPolicyForm.jsx | 1 +
.../AccessRulesStep.jsx | 11 +-
.../PolicyDetailsStep.jsx | 10 +-
.../PolicyPermissionItemVertical.jsx | 69 +++----
.../views/PolicyListing/PolicyConditionsComp.jsx | 29 +--
.../views/PolicyListing/PolicyPermissionItem.jsx | 115 ++++--------
12 files changed, 479 insertions(+), 287 deletions(-)
diff --git
a/security-admin/src/main/webapp/react-webapp/src/components/CommonComponents.jsx
b/security-admin/src/main/webapp/react-webapp/src/components/CommonComponents.jsx
index 64aed3515..b771668f0 100644
---
a/security-admin/src/main/webapp/react-webapp/src/components/CommonComponents.jsx
+++
b/security-admin/src/main/webapp/react-webapp/src/components/CommonComponents.jsx
@@ -499,10 +499,6 @@ export const scrollToError = (selector) => {
};
export const selectInputCustomStyles = {
- multiValue: (base) => ({
- ...base,
- maxWidth: "100%"
- }),
option: (base) => ({
...base,
textOverflow: "unset",
@@ -523,7 +519,7 @@ export const selectInputCustomStyles = {
})
};
-/** Multi-value select styles that wrap chips (e.g. action-matches in
permission rows). */
+// Multi-value select styles that wrap chips (e.g. action-matches in
permission rows).
export const selectInputWrappingCustomStyles = {
...selectInputCustomStyles,
control: (base) => ({
diff --git
a/security-admin/src/main/webapp/react-webapp/src/components/Editable.jsx
b/security-admin/src/main/webapp/react-webapp/src/components/Editable.jsx
index f05c790d5..807764972 100644
--- a/security-admin/src/main/webapp/react-webapp/src/components/Editable.jsx
+++ b/security-admin/src/main/webapp/react-webapp/src/components/Editable.jsx
@@ -59,8 +59,7 @@ const TYPE_CUSTOM = "custom";
* Callers may pass selectProps to merge or override (PolicyPermissionItem
does).
*/
const CONDITION_POPOVER_SELECT_PROPS = {
- menuPortalTarget:
- typeof document !== "undefined" ? document.body : undefined,
+ menuPortalTarget: typeof document !== "undefined" ? document.body :
undefined,
menuPosition: "fixed",
menuPlacement: "auto",
menuShouldScrollIntoView: false
@@ -243,15 +242,9 @@ const ConditionRow = ({
return mOp;
}
});
- setCondSelect(
- !isEmpty(e) ? (e?.length > 1 ? filterVal : e) : null
- );
+ setCondSelect(!isEmpty(e) ? (e?.length > 1 ? filterVal : e) : null);
tagAccessData(
- !isEmpty(e)
- ? e?.length > 1
- ? filterVal[0].value
- : e[0].value
- : null,
+ !isEmpty(e) ? (e?.length > 1 ? filterVal[0].value : e[0].value) : null,
name
);
};
@@ -321,9 +314,7 @@ const ConditionRow = ({
isInvalid={validExpression.state}
/>
{validExpression.state && (
- <div className="text-danger">
- {validExpression.errorMSG}
- </div>
+ <div className="text-danger">{validExpression.errorMSG}</div>
)}
</Col>
</Row>
@@ -529,6 +520,23 @@ const Editable = (props) => {
};
}, []);
+ useEffect(() => {
+ if (!initialLoad.current) {
+ onChange(editableValue);
+ dispatch({
+ type: "SET_VALUE",
+ value: editableValue,
+ show: false,
+ target: null
+ });
+ } else {
+ initialLoad.current = false;
+ }
+ type === TYPE_CUSTOM
+ ? (selectValRef.current = { ...editableValue })
+ : (selectValRef.current = editableValue);
+ }, [editableValue]);
+
const displayValue = () => {
let val = "--";
let selectVal = value;
@@ -759,23 +767,6 @@ const Editable = (props) => {
return val;
};
- useEffect(() => {
- if (!initialLoad.current) {
- onChange(editableValue);
- dispatch({
- type: "SET_VALUE",
- value: editableValue,
- show: false,
- target: null
- });
- } else {
- initialLoad.current = false;
- }
- type === TYPE_CUSTOM
- ? (selectValRef.current = { ...editableValue })
- : (selectValRef.current = editableValue);
- }, [editableValue]);
-
const handleApply = () => {
let errors, uiHintVal;
if (selectValRef?.current) {
@@ -795,7 +786,7 @@ const Editable = (props) => {
actionFilterContext?.selectedAccessTypes?.length > 0
) {
const current = selectValRef.current[conditionObj.name];
-
+
if (Array.isArray(current)) {
const { prunedSelection } = getAllowedActionMatchesForCondition({
conditionName: conditionObj.name,
@@ -805,7 +796,7 @@ const Editable = (props) => {
uiHintAttb: uiHintVal,
currentSelection: current
});
-
+
if (prunedSelection && prunedSelection.length > 0) {
selectValRef.current[conditionObj.name] = prunedSelection;
} else {
@@ -842,6 +833,16 @@ const Editable = (props) => {
}
};
+ const handleClick = (e) => {
+ setValidated({ state: false, errorMSG: "" });
+ let display = !show;
+ dispatch({
+ type: "SET_POPOVER",
+ show: display,
+ target: e.target
+ });
+ };
+
const handleClose = () => {
setValidated({ state: false, errorMSG: "" });
dispatch({
@@ -906,16 +907,6 @@ const Editable = (props) => {
</Popover>
);
- const handleClick = (e) => {
- setValidated({ state: false, errorMSG: "" });
- let display = !show;
- dispatch({
- type: "SET_POPOVER",
- show: display,
- target: e.target
- });
- };
-
return (
<div ref={popoverRef}>
<OverlayTrigger
diff --git
a/security-admin/src/main/webapp/react-webapp/src/hooks/usePolicyPermissionConditionContext.js
b/security-admin/src/main/webapp/react-webapp/src/hooks/usePolicyPermissionConditionContext.js
new file mode 100644
index 000000000..c91925eb4
--- /dev/null
+++
b/security-admin/src/main/webapp/react-webapp/src/hooks/usePolicyPermissionConditionContext.js
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useMemo } from "react";
+import { groupBy } from "lodash";
+import { policyConditionUpdatedJSON } from "Utils/XAUtils";
+import {
+ buildActionReqsMapFromConditionDef,
+ getResourceSelectionSignature,
+ getSelectedLeafResourceTypes
+} from "Utils/policyConditionUtils";
+import { usePruneStaleConditions } from "Hooks/usePruneStaleConditions";
+
+/**
+ * Shared condition/action-matches setup for permission row components.
+ * Memoizes condition defs, action requirements, and leaf resource types,
+ * then runs background action-matches prune sync.
+ */
+export const usePolicyPermissionConditionContext = ({
+ serviceCompDetails,
+ formValues,
+ attrName,
+ form,
+ enableResourcePruneDefer = false
+}) => {
+ const conditionDefVal = useMemo(
+ () => policyConditionUpdatedJSON(serviceCompDetails?.policyConditions),
+ [serviceCompDetails?.policyConditions]
+ );
+
+ const actionReqsMap = useMemo(
+ () => buildActionReqsMapFromConditionDef(conditionDefVal),
+ [conditionDefVal]
+ );
+
+ const grpResourcesKeys = useMemo(() => {
+ const { resources = [] } = serviceCompDetails || {};
+ const grpResources = groupBy(resources, "level");
+ let keys = [];
+ for (const resourceKey in grpResources) {
+ keys.push(+resourceKey);
+ }
+ return keys.sort((a, b) => a - b);
+ }, [serviceCompDetails?.resources]);
+
+ const resourceSelectionSignature = getResourceSelectionSignature(
+ grpResourcesKeys,
+ formValues
+ );
+
+ const leafResourceTypes = useMemo(() => {
+ return getSelectedLeafResourceTypes(serviceCompDetails, formValues);
+ }, [serviceCompDetails, resourceSelectionSignature]);
+
+ usePruneStaleConditions({
+ formValues,
+ attrName,
+ form,
+ leafResourceTypes,
+ serviceCompDetails,
+ conditionDefVal,
+ actionReqsMap,
+ enableResourcePruneDefer
+ });
+
+ return {
+ conditionDefVal,
+ actionReqsMap,
+ leafResourceTypes,
+ grpResourcesKeys
+ };
+};
diff --git
a/security-admin/src/main/webapp/react-webapp/src/hooks/usePruneStaleConditions.js
b/security-admin/src/main/webapp/react-webapp/src/hooks/usePruneStaleConditions.js
index 6cb1cb0ed..9d14e4d15 100644
---
a/security-admin/src/main/webapp/react-webapp/src/hooks/usePruneStaleConditions.js
+++
b/security-admin/src/main/webapp/react-webapp/src/hooks/usePruneStaleConditions.js
@@ -23,15 +23,18 @@
* or other per-row conditions.
*/
-import { useEffect } from "react";
+import { useEffect, useMemo } from "react";
import { isArray, find, isEqual } from "lodash";
import {
getSelectedAccessTypesForRow,
getAllowedActionMatchesForCondition,
- isPerRowCondition,
+ shouldDeferActionMatchesPrune,
+ isActionMatcherEnabled,
parseConditionUiHint
} from "Utils/policyConditionUtils";
+const ACTION_MATCHES = "action-matches";
+
/**
* Stable dependency string: only permission accesses and per-row condition
values.
* Avoids re-running when unrelated form fields (user/group names, etc.)
change.
@@ -44,7 +47,7 @@ const serializePruneDeps = (formValues, attrName) => {
return JSON.stringify(
items.map((item, index) => ({
accesses: getSelectedAccessTypesForRow(formValues, attrName, index),
- actionMatches: item?.conditions?.["action-matches"] ?? null
+ actionMatches: item?.conditions?.[ACTION_MATCHES] ?? null
}))
);
};
@@ -56,98 +59,103 @@ export const usePruneStaleConditions = ({
leafResourceTypes,
serviceCompDetails,
conditionDefVal,
- actionReqsMap
+ actionReqsMap,
+ // When true, skip resource-driven prune while multi-resource rows are empty
or mid-edit.
+ enableResourcePruneDefer = false
}) => {
+ const actionMatcherEnabled = isActionMatcherEnabled(
+ serviceCompDetails?.options
+ );
const serializedPruneDeps = serializePruneDeps(formValues, attrName);
+ const serializedLeafTypes = leafResourceTypes
+ ? [...leafResourceTypes].sort().join(",")
+ : "";
+
+ const actionMatchesUiHint = useMemo(() => {
+ const actionMatchesDef = find(conditionDefVal, { name: ACTION_MATCHES });
+ return actionMatchesDef
+ ? parseConditionUiHint(actionMatchesDef.uiHint)
+ : null;
+ }, [conditionDefVal]);
useEffect(() => {
+ if (!actionMatcherEnabled) {
+ return;
+ }
+
const items = formValues?.[attrName];
if (!items || !isArray(items)) {
return;
}
- let hasChanges = false;
- const newItems = [...items];
+ const deferResourcePrune =
+ enableResourcePruneDefer &&
+ shouldDeferActionMatchesPrune(serviceCompDetails, formValues);
- newItems.forEach((item, index) => {
+ const baseActionFilterContext = {
+ leafResourceTypes,
+ accessTypeDefs: serviceCompDetails?.accessTypes
+ };
+
+ const changedRows = [];
+
+ items.forEach((item, index) => {
if (!item?.conditions) {
return;
}
- const accesses = getSelectedAccessTypesForRow(formValues, attrName,
index);
+ const accesses = getSelectedAccessTypesForRow(
+ formValues,
+ attrName,
+ index
+ );
+
+ // Always sync when permissions are cleared — defer must not block this.
if (accesses.length === 0) {
- const actionMatches = item.conditions?.["action-matches"];
+ const actionMatches = item.conditions[ACTION_MATCHES];
if (Array.isArray(actionMatches) && actionMatches.length > 0) {
const newConditions = { ...item.conditions };
- delete newConditions["action-matches"];
- newItems[index] = { ...item, conditions: newConditions };
- hasChanges = true;
+ delete newConditions[ACTION_MATCHES];
+ changedRows.push({ index, conditions: newConditions });
}
return;
}
- const actionFilterContext = {
- selectedAccessTypes: accesses,
- leafResourceTypes,
- accessTypeDefs: serviceCompDetails?.accessTypes
- };
-
- let itemChanged = false;
- const newConditions = { ...item.conditions };
-
- for (const conditionName in newConditions) {
- if (!isPerRowCondition(conditionName)) {
- continue;
- }
-
- const conditionDef = find(conditionDefVal, { name: conditionName });
- if (!conditionDef) {
- continue;
- }
+ // Defer only resource-driven prune while a multi-resource row is empty
or mid-edit.
+ if (deferResourcePrune) {
+ return;
+ }
- const uiHintVal = parseConditionUiHint(conditionDef.uiHint);
-
- if (
- uiHintVal?.isMultiValue &&
- Array.isArray(newConditions[conditionName])
- ) {
- const current = newConditions[conditionName];
- const { prunedSelection } = getAllowedActionMatchesForCondition({
- conditionName,
- actionFilterContext,
- actionReqsMap,
- servicedefName: serviceCompDetails?.name,
- uiHintAttb: uiHintVal,
- currentSelection: current
- });
-
- if (!isEqual(current, prunedSelection)) {
- if (prunedSelection && prunedSelection.length > 0) {
- newConditions[conditionName] = prunedSelection;
- } else {
- delete newConditions[conditionName];
- }
- itemChanged = true;
- hasChanges = true;
- }
- }
+ const current = item.conditions[ACTION_MATCHES];
+ if (!Array.isArray(current) || current.length === 0) {
+ return;
}
- if (itemChanged) {
- newItems[index] = { ...item, conditions: newConditions };
+ const { prunedSelection } = getAllowedActionMatchesForCondition({
+ conditionName: ACTION_MATCHES,
+ actionFilterContext: {
+ ...baseActionFilterContext,
+ selectedAccessTypes: accesses
+ },
+ actionReqsMap,
+ servicedefName: serviceCompDetails?.name,
+ uiHintAttb: actionMatchesUiHint,
+ currentSelection: current
+ });
+
+ if (!isEqual(current, prunedSelection)) {
+ const newConditions = { ...item.conditions };
+ if (prunedSelection && prunedSelection.length > 0) {
+ newConditions[ACTION_MATCHES] = prunedSelection;
+ } else {
+ delete newConditions[ACTION_MATCHES];
+ }
+ changedRows.push({ index, conditions: newConditions });
}
});
- if (hasChanges) {
- form.change(attrName, newItems);
- }
- }, [
- serializedPruneDeps,
- leafResourceTypes,
- actionReqsMap,
- attrName,
- form,
- conditionDefVal,
- serviceCompDetails
- ]);
+ changedRows.forEach(({ index, conditions }) => {
+ form.change(`${attrName}[${index}].conditions`, conditions);
+ });
+ }, [serializedPruneDeps, serializedLeafTypes, attrName]);
};
diff --git
a/security-admin/src/main/webapp/react-webapp/src/utils/policyConditionUtils.js
b/security-admin/src/main/webapp/react-webapp/src/utils/policyConditionUtils.js
index 3397769c3..e61259a51 100644
---
a/security-admin/src/main/webapp/react-webapp/src/utils/policyConditionUtils.js
+++
b/security-admin/src/main/webapp/react-webapp/src/utils/policyConditionUtils.js
@@ -39,7 +39,7 @@
* (ozone.json keyed by volume | bucket | key) and the row's selected access
types.
*/
-import { find, isEqual, isArray } from "lodash";
+import { isEmpty, isEqual, isArray, find } from "lodash";
import { actionRequirementsRegistry } from "./actionRequirements/registry";
/**
@@ -188,17 +188,27 @@ export const getSelectedAccessTypesForRow = (formValues,
attrName, index) => {
*/
export const isPerRowCondition = (name) => name === "action-matches";
+export const isActionMatcherEnabled = (serviceDefOptions) => {
+ const value = serviceDefOptions?.enableActionMatcherInPoliciesCondition;
+ return value === "true" || value === true;
+};
+
/**
* Safely parses a condition definition uiHint JSON string.
* @returns {object|null} Parsed uiHint object, or null if missing or invalid.
*/
export const parseConditionUiHint = (uiHint) => {
- if (uiHint == null || uiHint === "") {
+ if (isEmpty(uiHint)) {
return null;
}
+
try {
const parsed = JSON.parse(uiHint);
- return parsed && typeof parsed === "object" ? parsed : null;
+ return parsed !== null &&
+ typeof parsed === "object" &&
+ !Array.isArray(parsed)
+ ? parsed
+ : null;
} catch (e) {
return null;
}
@@ -231,6 +241,135 @@ export const sortPolicyConditions = (conditions) => {
});
};
+const getResourceLevels = (resources) => {
+ if (!Array.isArray(resources)) {
+ return [];
+ }
+ return [
+ ...new Set(resources.map((r) => r?.level).filter(Number.isFinite))
+ ].sort((a, b) => a - b);
+};
+
+const getResourceBlocks = (formValues) => {
+ if (Array.isArray(formValues?.additionalResources)) {
+ return formValues.additionalResources;
+ }
+ return [formValues];
+};
+
+const hasResourcePathValue = (block, level) => {
+ const valueField = block?.[`value-${level}`];
+ if (Array.isArray(valueField)) {
+ return valueField.length > 0;
+ }
+ return valueField != null && valueField !== "";
+};
+
+/**
+ * A resource block is complete when the deepest selected level has a path
value
+ * or an explicit "None" (e.g. all keys under a bucket). Auto-populated child
+ * levels without user input stay incomplete until resolved.
+ */
+const isResourceBlockComplete = (block, levels) => {
+ if (!block || typeof block !== "object") {
+ return false;
+ }
+
+ let deepestLevel = null;
+ for (const level of levels) {
+ const sel = block[`resourceName-${level}`];
+ if (sel && sel.name) {
+ deepestLevel = level;
+ }
+ }
+
+ if (deepestLevel == null) {
+ return false;
+ }
+
+ const deepestSel = block[`resourceName-${deepestLevel}`];
+ if (deepestSel?.value === NONE_RESOURCE_VALUE) {
+ return true;
+ }
+
+ return hasResourcePathValue(block, deepestLevel);
+};
+
+/** Defer resource-driven background prune while a multi-resource row is empty
or mid-edit.
+ * Does not block clearing action-matches when row permissions are removed. */
+export const shouldDeferActionMatchesPrune = (
+ serviceCompDetails,
+ formValues
+) => {
+ if (!Array.isArray(formValues?.additionalResources)) {
+ return false;
+ }
+
+ const levels = getResourceLevels(serviceCompDetails?.resources);
+ if (levels.length === 0) {
+ return false;
+ }
+
+ return formValues.additionalResources.some((block) => {
+ if (!block || typeof block !== "object") {
+ return false;
+ }
+
+ const hasSelection = levels.some((level) => {
+ const sel = block[`resourceName-${level}`];
+ return sel && sel.name;
+ });
+
+ // Empty "Add Resource" row
+ if (!hasSelection) {
+ return true;
+ }
+
+ // Row started but deepest level not committed yet (e.g. key not rendered)
+ return !isResourceBlockComplete(block, levels);
+ });
+};
+
+const resourceSelectionPart = (sel) => {
+ if (!sel) {
+ return "";
+ }
+ return `${sel.name ?? ""}:${sel.value ?? ""}`;
+};
+
+/**
+ * Stable string signature of resource selection fields only.
+ * Multi-resource: additionalResources blocks only.
+ * Single-resource: root formValues resourceName-* fields.
+ *
+ * @param {number[]} resourceLevels - Sorted resource level keys
(grpResourcesKeys).
+ * @param {object} formValues - Policy form state.
+ * @returns {string} Signature used to memoize leafResourceTypes.
+ */
+export const getResourceSelectionSignature = (resourceLevels, formValues) => {
+ if (!isArray(resourceLevels) || resourceLevels.length === 0) {
+ return "";
+ }
+
+ if (isArray(formValues?.additionalResources)) {
+ return formValues.additionalResources
+ .map((block) =>
+ resourceLevels
+ .map((level) =>
+ resourceSelectionPart(block?.[`resourceName-${level}`])
+ )
+ .join("|")
+ )
+ .join(";");
+ }
+
+ return resourceLevels
+ .map((level) =>
+ resourceSelectionPart(formValues?.[`resourceName-${level}`])
+ )
+ .join("|");
+};
+
/**
* Determines the deepest (leaf) resource types currently selected in the
policy form.
* For Ozone, this might return {"key"} or {"bucket"} depending on which
resource
@@ -254,13 +393,9 @@ export const getSelectedLeafResourceTypes = (
return result;
}
- const levels = [
- ...new Set(resources.map((r) => r?.level).filter(Number.isFinite))
- ].sort((a, b) => a - b);
+ const levels = getResourceLevels(resources);
- const blocks = Array.isArray(formValues?.additionalResources)
- ? formValues.additionalResources
- : [formValues];
+ const blocks = getResourceBlocks(formValues);
for (const block of blocks) {
if (!block) {
@@ -723,35 +858,42 @@ export const getAllowedActionMatchesForCondition = ({
};
/**
- * Pre-builds the action requirements map from the condition definitions.
- * Scans each condition's uiHint for an `actionRequirementsFile` reference
- * (which points to a JSON file in the actionRequirements registry, e.g.
"ozone")
- * or an inline `actionRequirements` object.
+ * Pre-builds action requirements for the action-matches condition.
+ * Only per-row conditions (today: action-matches) may declare
+ * actionRequirementsFile (registry) or inline actionRequirements in uiHint.
*
- * This is memoized in PolicyPermissionItem and Editable so we don't re-parse
- * uiHint JSON on every render.
+ * Memoized in PolicyPermissionItem and Editable so uiHint JSON is not
+ * re-parsed on every render.
*
* @param {Array<{name: string, uiHint: string}>} conditionDefVal - Condition
definitions from service def.
- * @returns {object} Map of condition name → action requirements object.
+ * @returns {object} Map with at most one entry: { "action-matches":
requirementsObject }.
*/
export const buildActionReqsMapFromConditionDef = (conditionDefVal) => {
const map = {};
- if (!Array.isArray(conditionDefVal) || conditionDefVal.length === 0) {
+
+ if (!isArray(conditionDefVal) || isEmpty(conditionDefVal)) {
return map;
}
- conditionDefVal.forEach((m) => {
- const uiHintAttb = parseConditionUiHint(m.uiHint);
+ const actionMatchesDef = find(conditionDefVal, (def) =>
+ isPerRowCondition(def?.name)
+ );
- if (uiHintAttb) {
- const fileToLoad = uiHintAttb?.actionRequirementsFile;
- if (fileToLoad && actionRequirementsRegistry[fileToLoad]) {
- map[m.name] = actionRequirementsRegistry[fileToLoad];
- } else if (uiHintAttb?.actionRequirements) {
- map[m.name] = uiHintAttb.actionRequirements;
- }
- }
- });
+ if (!actionMatchesDef) {
+ return map;
+ }
+
+ const uiHintAttb = parseConditionUiHint(actionMatchesDef.uiHint);
+ if (!uiHintAttb) {
+ return map;
+ }
+
+ const fileToLoad = uiHintAttb.actionRequirementsFile;
+ if (fileToLoad && actionRequirementsRegistry[fileToLoad]) {
+ map[actionMatchesDef.name] = actionRequirementsRegistry[fileToLoad];
+ } else if (uiHintAttb.actionRequirements) {
+ map[actionMatchesDef.name] = uiHintAttb.actionRequirements;
+ }
return map;
};
diff --git
a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/AddUpdatePolicyForm.jsx
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/AddUpdatePolicyForm.jsx
index 5c0da101e..2c6f2a7b2 100644
---
a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/AddUpdatePolicyForm.jsx
+++
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/AddUpdatePolicyForm.jsx
@@ -66,7 +66,6 @@ import {
} from "react-router-dom";
import PolicyValidityPeriodComp from "./PolicyValidityPeriodComp";
import PolicyConditionsComp from "./PolicyConditionsComp";
-import { isPerRowCondition } from "Utils/policyConditionUtils";
import moment from "moment";
import {
InfoIcon,
@@ -1362,15 +1361,17 @@ export default function AddUpdatePolicyForm() {
name="conditions"
render={({ input }) => (
<PolicyConditionsComp
-
policyConditionDetails={policyConditionUpdatedJSON(
- serviceCompDetails.policyConditions
- )}
- inputVal={input}
+ // Visibility & Control
showModal={showModal}
+ modalHeader="Policy Conditions"
handleCloseModal={
policyConditionState
}
- modalHeader="Policy Conditions"
+ // Data & Value Binding
+ inputVal={input}
+
policyConditionDetails={policyConditionUpdatedJSON(
+ serviceCompDetails.policyConditions
+ )}
/>
)}
/>
@@ -1394,9 +1395,6 @@ export default function AddUpdatePolicyForm() {
!isEmpty(values.conditions) ? (
Object.keys(values.conditions).map(
(keyName) => {
- if (isPerRowCondition(keyName)) {
- return null;
- }
if (
values.conditions[keyName] != "" &&
values.conditions[keyName] != null
diff --git
a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/EditNewPolicyForm/EditPolicyForm.jsx
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/EditNewPolicyForm/EditPolicyForm.jsx
index f5fa0c9ea..d3de2c8d2 100644
---
a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/EditNewPolicyForm/EditPolicyForm.jsx
+++
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/EditNewPolicyForm/EditPolicyForm.jsx
@@ -841,6 +841,7 @@ export default function EditPolicyForm() {
values={values}
form={form}
selectedServiceComponentDef={serviceCompDef}
+ enableResourcePruneDefer
/>
</Accordion.Body>
</Accordion.Item>
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 686ca47ac..448077256 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
@@ -83,7 +83,11 @@ const isItemEmpty = (item) => {
return isEmpty;
};
-const AccessRulesStep = ({ values, selectedServiceComponentDef }) => {
+const AccessRulesStep = ({
+ values,
+ selectedServiceComponentDef,
+ enableResourcePruneDefer = false
+}) => {
const isDenyAllElseEnabled = values?.isDenyAllElse === true;
const enableDenyAndExceptions =
selectedServiceComponentDef?.options?.enableDenyAndExceptionsInPolicies ===
@@ -185,6 +189,7 @@ const AccessRulesStep = ({ values,
selectedServiceComponentDef }) => {
formValues={values}
attrName="policyItems"
msg="Allow rules grants access to resources when the
specific criteria are met."
+ enableResourcePruneDefer={enableResourcePruneDefer}
/>
</Accordion.Body>
</Accordion.Item>
@@ -201,6 +206,7 @@ const AccessRulesStep = ({ values,
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>
@@ -283,6 +289,7 @@ const AccessRulesStep = ({ values,
selectedServiceComponentDef }) => {
attrName="denyPolicyItems"
msg="Deny rules explicitly block access to resources
when the specific
criteria are met."
+ enableResourcePruneDefer={enableResourcePruneDefer}
/>
</Accordion.Body>
</Accordion.Item>
@@ -300,6 +307,7 @@ const AccessRulesStep = ({ values,
selectedServiceComponentDef }) => {
attrName="denyExceptions"
msg="Exclude from Deny Rules explicitly block access
to resources when the specific
criteria are met."
+ enableResourcePruneDefer={enableResourcePruneDefer}
/>
</Accordion.Body>
</Accordion.Item>
@@ -317,6 +325,7 @@ const AccessRulesStep = ({ values,
selectedServiceComponentDef }) => {
formValues={values}
attrName={item.attrName}
msg={item.msg}
+ enableResourcePruneDefer={enableResourcePruneDefer}
/>
))}
</>
diff --git
a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/PolicyDetailsStep.jsx
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/PolicyDetailsStep.jsx
index 36e97981b..58ec8906d 100644
---
a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/PolicyDetailsStep.jsx
+++
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/PolicyDetailsStep.jsx
@@ -459,6 +459,12 @@ const PolicyDetailsStep = ({ values, form,
selectedServiceComponentDef }) => {
{/* Policy Conditions Modal */}
{showConditionsModal && (
<PolicyConditionsComp
+ // Visibility & Control
+ showModal={showConditionsModal}
+ handleCloseModal={() =>
+ setShowConditionsModal(false)
+ }
+ // Data & Value Binding
policyConditionDetails={policyConditionUpdatedJSON(
selectedServiceComponentDef.policyConditions
)}
@@ -468,10 +474,6 @@ const PolicyDetailsStep = ({ values, form,
selectedServiceComponentDef }) => {
form.change("conditions", newValue);
}
}}
- showModal={showConditionsModal}
- handleCloseModal={() =>
- setShowConditionsModal(false)
- }
/>
)}
</Col>
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 f961df92a..0ac3fd996 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
@@ -17,7 +17,7 @@
* under the License.
*/
-import React, { useMemo, useState } from "react";
+import React, { useState } from "react";
import { FieldArray } from "react-final-form-arrays";
import {
Row,
@@ -31,7 +31,6 @@ import { Field, useForm } from "react-final-form";
import { RangerPolicyType } from "Utils/XAEnums";
import { find, isEmpty, isArray, toUpper, map } from "lodash";
import PolicyConditionsComp from "../../PolicyConditionsComp";
-import { policyConditionUpdatedJSON } from "Utils/XAUtils";
import UsersGroupsAndRolesSelectionComponent from
"./UsersGroupsAndRolesSelectionComponent";
import TagBasePermissionItem from "../../TagBasePermissionItem";
import {
@@ -39,17 +38,14 @@ import {
getPolicyConditionDisplayLbl
} from "Utils/XAUtils";
import { CustomTooltip } from "Components/CommonComponents";
-import {
- getSelectedAccessTypesForRow,
- getSelectedLeafResourceTypes,
- buildActionReqsMapFromConditionDef
-} from "Utils/policyConditionUtils";
-import { usePruneStaleConditions } from
"../../../../hooks/usePruneStaleConditions";
+import { getSelectedAccessTypesForRow } from "Utils/policyConditionUtils";
+import { usePolicyPermissionConditionContext } from
"Hooks/usePolicyPermissionConditionContext";
export default function PolicyPermissionItemVertical({
attrName,
serviceCompDetails,
- formValues
+ formValues,
+ enableResourcePruneDefer = false
}) {
const [showConditionsModalIndex, setShowConditionsModalIndex] =
useState(null);
@@ -57,30 +53,14 @@ export default function PolicyPermissionItemVertical({
useState(null);
const form = useForm(); // Get form instance
- const conditionDefVal = useMemo(
- () => policyConditionUpdatedJSON(serviceCompDetails?.policyConditions),
- [serviceCompDetails?.policyConditions]
- );
-
- const actionReqsMap = useMemo(
- () => buildActionReqsMapFromConditionDef(conditionDefVal),
- [conditionDefVal]
- );
-
- const leafResourceTypes = useMemo(
- () => getSelectedLeafResourceTypes(serviceCompDetails, formValues),
- [serviceCompDetails, formValues]
- );
-
- usePruneStaleConditions({
- formValues,
- attrName,
- form,
- leafResourceTypes,
- serviceCompDetails,
- conditionDefVal,
- actionReqsMap
- });
+ const { conditionDefVal, actionReqsMap, leafResourceTypes } =
+ usePolicyPermissionConditionContext({
+ serviceCompDetails,
+ formValues,
+ attrName,
+ form,
+ enableResourcePruneDefer
+ });
const accessTypeOptions = getAccessTypesByResource(
formValues,
@@ -553,6 +533,14 @@ export default function PolicyPermissionItemVertical({
)}
{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 || {},
@@ -560,23 +548,20 @@ export default function PolicyPermissionItemVertical({
form.change(`${name}.conditions`, newValue);
}
}}
- showModal={true}
- handleCloseModal={() =>
- setShowConditionsModalIndex(null)
- }
- modalHeader="Rule Conditions"
- scope="policyItem"
+ // Service & Actions Metadata
servicedefName={serviceCompDetails?.name}
+ serviceDefOptions={serviceCompDetails?.options}
actionReqsMap={actionReqsMap}
+ // Filter Context Evaluation
actionFilterContext={{
+ leafResourceTypes,
+ accessTypeDefs:
serviceCompDetails?.accessTypes,
selectedAccessTypes:
getSelectedAccessTypesForRow(
formValues,
attrName,
index
- ),
- leafResourceTypes,
- accessTypeDefs: serviceCompDetails?.accessTypes
+ )
}}
/>
)}
diff --git
a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyConditionsComp.jsx
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyConditionsComp.jsx
index 60ed5e3e6..34b65c886 100644
---
a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyConditionsComp.jsx
+++
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyConditionsComp.jsx
@@ -30,6 +30,7 @@ import {
trimInputValue
} from "Components/CommonComponents";
import {
+ isActionMatcherEnabled,
isPerRowCondition,
getCleanConditions,
parseConditionUiHint,
@@ -109,22 +110,31 @@ const pruneActionMatchesOnSubmit = ({
export default function PolicyConditionsComp(props) {
const {
- policyConditionDetails,
- inputVal,
+ // Visibility & Control
showModal,
- handleCloseModal,
modalHeader,
+ handleCloseModal,
+
+ // Data & Value Binding
+ inputVal,
+ policyConditionDetails,
scope = POLICY_SCOPE,
+
+ // Service & Actions Metadata
servicedefName,
- actionFilterContext,
- actionReqsMap: actionReqsMapProp
+ serviceDefOptions,
+ actionReqsMap: actionReqsMapProp,
+
+ // Filter Context Evaluation
+ actionFilterContext
} = props;
const isPolicyItemScope = scope === POLICY_ITEM_SCOPE;
+ const hasActionMatcherEnabled = isActionMatcherEnabled(serviceDefOptions);
+
const conditionDefVal = useMemo(
- () =>
- Array.isArray(policyConditionDetails) ? policyConditionDetails : [],
+ () => (Array.isArray(policyConditionDetails) ? policyConditionDetails :
[]),
[policyConditionDetails]
);
@@ -231,6 +241,7 @@ export default function PolicyConditionsComp(props) {
const renderMultiValueField = (m, uiHintAttb) => {
const isActionMatches =
+ hasActionMatcherEnabled &&
isPolicyItemScope &&
isPerRowCondition(m.name) &&
Array.isArray(uiHintAttb?.options);
@@ -281,9 +292,7 @@ export default function PolicyConditionsComp(props) {
value={ipRangeVal(input.value)}
onChange={(e) => handleChange(e, input)}
styles={selectInputCustomStyles}
- formatCreateLabel={(inputValue) =>
- `Create "${inputValue.trim()}"`
- }
+ formatCreateLabel={(inputValue) => `Create "${inputValue.trim()}"`}
onCreateOption={(inputValue) => {
const trimmedValue = inputValue.trim();
if (trimmedValue) {
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 60a48dff1..351925128 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, { useMemo, useRef, useState } from "react";
+import React, { useRef, useState } from "react";
import { Table, Button, Form } from "react-bootstrap";
import { FieldArray } from "react-final-form-arrays";
import { Col } from "react-bootstrap";
@@ -25,7 +25,6 @@ import { Field, useForm } from "react-final-form";
import AsyncSelect from "react-select/async";
import {
find,
- groupBy,
isEmpty,
isArray,
map,
@@ -43,19 +42,19 @@ import {
dragEnter,
drop,
dragOver,
- policyConditionUpdatedJSON,
getPolicyConditionDisplayLbl,
safeJsonParse
} from "Utils/XAUtils";
-import { selectInputCustomStyles, ConfirmationClearIndicator } from
"Components/CommonComponents";
+import {
+ selectInputCustomStyles,
+ ConfirmationClearIndicator
+} from "Components/CommonComponents";
import PolicyConditionsComp from "./PolicyConditionsComp";
import {
- getSelectedLeafResourceTypes,
getSelectedAccessTypesForRow,
- buildActionReqsMapFromConditionDef,
getCleanConditions
} from "Utils/policyConditionUtils";
-import { usePruneStaleConditions } from "../../hooks/usePruneStaleConditions";
+import { usePolicyPermissionConditionContext } from
"Hooks/usePolicyPermissionConditionContext";
const noneOptions = {
label: "None",
@@ -87,6 +86,8 @@ export default function PolicyPermissionItem(props) {
const permList = ["Select Roles", "Select Groups", "Select Users"];
+ const form = useForm();
+
if (serviceCompDetails?.policyConditions?.length > 0) {
permList.push("Rule Conditions");
}
@@ -123,66 +124,17 @@ export default function PolicyPermissionItem(props) {
});
};
- const grpResourcesKeys = useMemo(() => {
- const { resources = [] } = serviceCompDetails || {};
- const grpResources = groupBy(resources, "level");
- let grpResourcesKeys = [];
- for (const resourceKey in grpResources) {
- grpResourcesKeys.push(+resourceKey);
- }
- grpResourcesKeys = grpResourcesKeys.sort((a, b) => a - b);
- return grpResourcesKeys;
- }, [serviceCompDetails?.resources]);
-
- // Narrow dependency: only recompute leafResourceTypes when resource
selection
- // fields change, not on every keystroke in user/group/permission fields.
- const resourceSelectionPart = (sel) => {
- if (!sel) {
- return "";
- }
- return `${sel.name ?? ""}:${sel.value ?? ""}`;
- };
-
- const resourceSelectionSignature = Array.isArray(
- formValues?.additionalResources
- )
- ? formValues.additionalResources
- .map((b) =>
- grpResourcesKeys
- .map((level) =>
resourceSelectionPart(b?.[`resourceName-${level}`]))
- .join("|")
- )
- .join(";")
- : grpResourcesKeys
- .map((level) =>
- resourceSelectionPart(formValues?.[`resourceName-${level}`])
- )
- .join("|");
-
- const leafResourceTypes = useMemo(() => {
- return getSelectedLeafResourceTypes(serviceCompDetails, formValues);
- }, [serviceCompDetails, resourceSelectionSignature]);
-
- const conditionDefVal = useMemo(
- () => policyConditionUpdatedJSON(serviceCompDetails.policyConditions),
- [serviceCompDetails.policyConditions]
- );
-
- const form = useForm();
-
- const actionReqsMap = useMemo(
- () => buildActionReqsMapFromConditionDef(conditionDefVal),
- [conditionDefVal]
- );
-
- usePruneStaleConditions({
+ const {
+ conditionDefVal,
+ actionReqsMap,
+ leafResourceTypes,
+ grpResourcesKeys
+ } = usePolicyPermissionConditionContext({
+ serviceCompDetails,
formValues,
attrName,
form,
- leafResourceTypes,
- serviceCompDetails,
- conditionDefVal,
- actionReqsMap
+ enableResourcePruneDefer: true
});
const getAccessTypeOptions = () => {
@@ -414,8 +366,8 @@ export default function PolicyPermissionItem(props) {
>
<span className="line-clamp line-clamp-5 text-start">
{`${getPolicyConditionDisplayLbl(conditionObj.label)}: ${
- isArray(selectVal[property]) ? ipRangVal : selectVal[property]
- }`}
+ isArray(selectVal[property]) ? ipRangVal :
selectVal[property]
+ }`}
</span>
</span>
</div>
@@ -592,33 +544,40 @@ export default function PolicyPermissionItem(props) {
<>
{showConditionsModal && (
<PolicyConditionsComp
- policyConditionDetails={
- conditionDefVal
- }
- inputVal={input}
+ // Visibility & Control
showModal={true}
+ modalHeader="Rule Conditions"
handleCloseModal={() =>
setActiveConditionRow(null)
}
- modalHeader="Rule Conditions"
+ // Data & Value Binding
scope="policyItem"
+ inputVal={input}
+ policyConditionDetails={
+ conditionDefVal
+ }
+ // Service & Actions Metadata
servicedefName={
serviceCompDetails?.name
}
+ serviceDefOptions={
+ serviceCompDetails?.options
+ }
actionReqsMap={actionReqsMap}
+ // Filter Context Evaluation
actionFilterContext={{
+ leafResourceTypes,
+ accessTypeDefs:
+
serviceCompDetails?.accessTypes,
selectedAccessTypes:
getSelectedAccessTypesForRow(
formValues,
attrName,
index
- ),
- leafResourceTypes,
- accessTypeDefs:
- serviceCompDetails?.accessTypes
+ )
}}
/>
- )}
+ )}
<div className="table-editable">
{hasValue ? (
<h6>
@@ -633,6 +592,8 @@ export default function PolicyPermissionItem(props) {
onClick={() =>
setActiveConditionRow(name)
}
+
data-js="customPolicyConditions"
+
data-cy="customPolicyConditions"
>
<i className="fa-fw fa
fa-pencil"></i>
</Button>
@@ -650,6 +611,8 @@ export default function PolicyPermissionItem(props) {
onClick={() =>
setActiveConditionRow(name)
}
+
data-js="customPolicyConditions"
+
data-cy="customPolicyConditions"
>
<i className="fa-fw fa
fa-plus"></i>
</Button>