This is an automated email from the ASF dual-hosted git repository.

dhavaljrajpara pushed a commit to branch RANGER-5860
in repository https://gitbox.apache.org/repos/asf/ranger.git

commit 313f0d40b87ed6ce74037255c57bbb6b9d996d1f
Author: Dhaval Rajpara <[email protected]>
AuthorDate: Wed Oct 28 16:42:31 2026 +0530

    RANGER-5852 : Added access review and submit form logic to contraint policy 
form
---
 .../main/webapp/react-webapp/src/utils/XAUtils.js  |  76 +++++
 .../NewPolicyForm/CreateNewPolicyForm.jsx          | 331 +++++++++++++--------
 .../ReviewConfirmStep.jsx                          | 118 +++++++-
 .../NewPolicyForm/policyFormValidation.js          | 106 ++++++-
 4 files changed, 490 insertions(+), 141 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 89b3916a3..9d4c2e5bf 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
@@ -1993,6 +1993,82 @@ export const prunePolicyItemAccessesToAllowedTypes = (
   }
 };
 
+/**
+ * When resources change, drop constraint-specific fields on rules whose
+ * constraint is no longer matched; prune accesses on rules that remain valid.
+ */
+export const pruneConstraintPolicyItemsToMatchedResources = (
+  form,
+  selectedServiceComponentDef
+) => {
+  const formValues = form.getState().values;
+  if (
+    formValues?.policyType !==
+    RangerPolicyType.RANGER_CONSTRAINT_POLICY_TYPE.value
+  ) {
+    return;
+  }
+
+  const attrName = "constraintPolicyItems";
+  const items = formValues?.[attrName];
+  if (!Array.isArray(items)) {
+    return;
+  }
+
+  const matchedDefs = getMatchedConstraintDefs(
+    selectedServiceComponentDef,
+    formValues
+  );
+  const allowedConstraintNames = new Set(
+    (matchedDefs || []).map((def) => def.name).filter(Boolean)
+  );
+
+  for (let i = 0; i < items.length; i++) {
+    const item = items[i];
+    if (!item || typeof item !== "object") {
+      continue;
+    }
+
+    const constraintName = getConstraintNameFromField(
+      item.constraintInfo?.constraintName
+    );
+    if (!constraintName) {
+      continue;
+    }
+
+    if (!allowedConstraintNames.has(constraintName)) {
+      form.change(`${attrName}[${i}].constraintInfo.constraintName`, 
undefined);
+      form.change(`${attrName}[${i}].constraintInfo.parameters`, undefined);
+      form.change(`${attrName}[${i}].accesses`, undefined);
+      continue;
+    }
+
+    const constraintDef = getConstraintDefByName(
+      selectedServiceComponentDef,
+      constraintName
+    );
+    const allowedAccessTypes = new Set(
+      (
+        getAccessTypesForConstraintDef(
+          selectedServiceComponentDef,
+          constraintDef
+        ) || []
+      ).map((at) => at.name)
+    );
+
+    if (!Array.isArray(item.accesses) || allowedAccessTypes.size === 0) {
+      continue;
+    }
+
+    const prunedAccesses = item.accesses.filter(
+      (a) => a?.type && allowedAccessTypes.has(a.type)
+    );
+    if (prunedAccesses.length !== item.accesses.length) {
+      form.change(`${attrName}[${i}].accesses`, prunedAccesses);
+    }
+  }
+};
+
 //CommonFunction to get display label for policy permission item
 export const getPolicyPermissionItemDisplayLbl = (
   serviceDef,
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 97ff12b19..e41c48cac 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
@@ -25,7 +25,9 @@ import { fetchApi } from "Utils/fetchAPI";
 import {
   getResourcesDefVal,
   prunePolicyItemAccessesToAllowedTypes,
-  policyConditionUpdatedJSON
+  pruneConstraintPolicyItemsToMatchedResources,
+  policyConditionUpdatedJSON,
+  getConstraintNameFromField
 } from "Utils/XAUtils";
 import {
   buildActionReqsMapFromConditionDef,
@@ -40,7 +42,8 @@ import { RangerPolicyType } from "Utils/XAEnums";
 import {
   validatePolicyItems,
   validatePolicyDetails,
-  hasMissingAccessPermissionOnPolicyItems
+  hasMissingAccessPermissionOnPolicyItems,
+  hasMissingConstraintFieldsOnPolicyItems
 } from "./policyFormValidation";
 import ServiceStep from "./PolicyCreateStepperStepComp/ServiceStep";
 import ResourcesStep from "./PolicyCreateStepperStepComp/ResourcesStep";
@@ -68,6 +71,154 @@ const CreateNewPolicyForm = () => {
     }
   };
 
+  const principalName = (p) => {
+    if (typeof p !== "object") return p;
+    if (p.name) return p.name;
+    if (p.value) {
+      const colonIdx = p.value.indexOf(":");
+      return colonIdx !== -1 ? p.value.slice(colonIdx + 1) : p.value;
+    }
+    return p;
+  };
+
+  const appendAccessesPrincipalsAndConditions = (obj, key) => {
+    if (key?.accesses) {
+      if (key.accesses.tableList && isArray(key.accesses.tableList)) {
+        obj.accesses = key.accesses.tableList.flatMap((tableItem) => {
+          if (tableItem.permission && isArray(tableItem.permission)) {
+            return tableItem.permission.map((perm) => ({
+              type: perm,
+              isAllowed: true
+            }));
+          }
+          return [];
+        });
+      } else if (isArray(key.accesses) && key.accesses.length > 0) {
+        obj.accesses = key.accesses.map((access) => {
+          if (access.type && access.isAllowed !== undefined) {
+            return {
+              type: access.type,
+              isAllowed: access.isAllowed
+            };
+          }
+          return {
+            type:
+              typeof access === "object" && access.value
+                ? access.value
+                : access.type || access,
+            isAllowed: true
+          };
+        });
+      }
+    }
+
+    if (key.users) {
+      if (isArray(key.users)) {
+        obj.users = key.users.map(principalName);
+      } else if (key.users.users && isArray(key.users.users)) {
+        obj.users = key.users.users.map(principalName);
+      }
+    }
+
+    if (
+      key.users?.groups &&
+      isArray(key.users.groups) &&
+      key.users.groups.length > 0
+    ) {
+      obj.groups = key.users.groups.map(principalName);
+    } else if (key.groups && isArray(key.groups) && key.groups.length > 0) {
+      obj.groups = key.groups.map(principalName);
+    }
+
+    if (
+      key.users?.roles &&
+      isArray(key.users.roles) &&
+      key.users.roles.length > 0
+    ) {
+      obj.roles = key.users.roles.map(principalName);
+    } else if (key.roles && isArray(key.roles) && key.roles.length > 0) {
+      obj.roles = key.roles.map(principalName);
+    }
+
+    if (key?.conditions && !isEmpty(key.conditions)) {
+      obj.conditions = [];
+      Object.entries(key.conditions).forEach(
+        ([conditionKey, conditionValue]) => {
+          if (conditionValue != "" && conditionValue != null) {
+            obj.conditions.push({
+              type: conditionKey,
+              values: isArray(conditionValue)
+                ? conditionValue.map((m) =>
+                    typeof m === "object" && m.value ? m.value : m
+                  )
+                : [conditionValue]
+            });
+          }
+        }
+      );
+    }
+  };
+
+  const buildConstraintInfoForApi = (constraintInfoForm) => {
+    if (!constraintInfoForm) {
+      return null;
+    }
+    const constraintName = getConstraintNameFromField(
+      constraintInfoForm.constraintName
+    );
+    if (!constraintName) {
+      return null;
+    }
+
+    const info = { constraintName };
+    const rawParams = constraintInfoForm.parameters;
+    if (rawParams && typeof rawParams === "object") {
+      const parameters = {};
+      Object.entries(rawParams).forEach(([paramKey, paramVal]) => {
+        if (paramVal === "" || paramVal == null) {
+          return;
+        }
+        parameters[paramKey] =
+          typeof paramVal === "object" && paramVal.value != null
+            ? paramVal.value
+            : String(paramVal);
+      });
+      if (!isEmpty(parameters)) {
+        info.parameters = parameters;
+      }
+    }
+    return info;
+  };
+
+  /** Constraint policy (type 4) — API shape for constraintPolicyItems */
+  const getConstraintPolicyItemsVal = (formData) => {
+    const items = formData?.constraintPolicyItems;
+    if (!items || items.length === 0) {
+      return [];
+    }
+
+    const policyResourceItem = [];
+    for (const key of items) {
+      if (isEmpty(key) || Object.entries(key).length === 0) {
+        continue;
+      }
+
+      const obj = {};
+      appendAccessesPrincipalsAndConditions(obj, key);
+
+      const constraintInfo = buildConstraintInfoForApi(key.constraintInfo);
+      if (constraintInfo) {
+        obj.constraintInfo = constraintInfo;
+      }
+
+      if (!isEmpty(obj)) {
+        policyResourceItem.push(obj);
+      }
+    }
+
+    return policyResourceItem;
+  };
+
   // Transform policy items to API format
   const getPolicyItemsVal = (formData, name) => {
     if (!formData[name] || formData[name].length === 0) {
@@ -92,85 +243,7 @@ const CreateNewPolicyForm = () => {
           }
         }
 
-        // Accesses (permissions) - handle both regular and tag-based
-        if (key?.accesses) {
-          // For tag-based policies with tableList structure
-          if (key.accesses.tableList && isArray(key.accesses.tableList)) {
-            obj.accesses = key.accesses.tableList.flatMap((tableItem) => {
-              if (tableItem.permission && isArray(tableItem.permission)) {
-                return tableItem.permission.map((perm) => ({
-                  type: perm,
-                  isAllowed: true
-                }));
-              }
-              return [];
-            });
-          }
-          // For regular policies with array of accesses
-          else if (isArray(key.accesses) && key.accesses.length > 0) {
-            obj.accesses = key.accesses.map((access) => {
-              // Handle if access is already in correct format {type: "read", 
isAllowed: true}
-              if (access.type && access.isAllowed !== undefined) {
-                return {
-                  type: access.type,
-                  isAllowed: access.isAllowed
-                };
-              }
-              // Handle if access is object with value property
-              return {
-                type:
-                  typeof access === "object" && access.value
-                    ? access.value
-                    : access.type || access,
-                isAllowed: true
-              };
-            });
-          }
-        }
-
-        // Resolve a stored principal object to its plain name string.
-        // Items are stored as { value: "USER:admin", name: "admin", type: 
"USER" }.
-        // We always prefer .name; fall back to stripping the "TYPE:" prefix 
from .value.
-        const principalName = (p) => {
-          if (typeof p !== "object") return p;
-          if (p.name) return p.name;
-          if (p.value) {
-            const colonIdx = p.value.indexOf(":");
-            return colonIdx !== -1 ? p.value.slice(colonIdx + 1) : p.value;
-          }
-          return p;
-        };
-
-        // Users
-        if (key.users) {
-          if (isArray(key.users)) {
-            obj.users = key.users.map(principalName);
-          } else if (key.users.users && isArray(key.users.users)) {
-            obj.users = key.users.users.map(principalName);
-          }
-        }
-
-        // Groups
-        if (
-          key.users?.groups &&
-          isArray(key.users.groups) &&
-          key.users.groups.length > 0
-        ) {
-          obj.groups = key.users.groups.map(principalName);
-        } else if (key.groups && isArray(key.groups) && key.groups.length > 0) 
{
-          obj.groups = key.groups.map(principalName);
-        }
-
-        // Roles
-        if (
-          key.users?.roles &&
-          isArray(key.users.roles) &&
-          key.users.roles.length > 0
-        ) {
-          obj.roles = key.users.roles.map(principalName);
-        } else if (key.roles && isArray(key.roles) && key.roles.length > 0) {
-          obj.roles = key.roles.map(principalName);
-        }
+        appendAccessesPrincipalsAndConditions(obj, key);
 
         // Row Filter Info (for row filter policies)
         if (
@@ -179,14 +252,12 @@ const CreateNewPolicyForm = () => {
           key.rowFilterInfo !== ""
         ) {
           obj.rowFilterInfo = {};
-          // If it's already an object with filterExpr
           if (
             typeof key.rowFilterInfo === "object" &&
             key.rowFilterInfo.filterExpr
           ) {
             obj.rowFilterInfo.filterExpr = key.rowFilterInfo.filterExpr;
           } else {
-            // If it's a string, use it directly as filterExpr
             obj.rowFilterInfo.filterExpr = key.rowFilterInfo;
           }
         }
@@ -198,42 +269,17 @@ const CreateNewPolicyForm = () => {
           !isEmpty(key.dataMaskInfo)
         ) {
           obj.dataMaskInfo = {};
-
-          // Handle if dataMaskInfo is an object with value property
           if (typeof key.dataMaskInfo === "object") {
             obj.dataMaskInfo.dataMaskType =
               key.dataMaskInfo.value || key.dataMaskInfo.dataMaskType;
-
-            // Include valueExpr for custom masking
             if (key.dataMaskInfo.valueExpr) {
               obj.dataMaskInfo.valueExpr = key.dataMaskInfo.valueExpr;
             }
           } else {
-            // Handle if dataMaskInfo is a string
             obj.dataMaskInfo.dataMaskType = key.dataMaskInfo;
           }
         }
 
-        // Conditions (rule-level conditions)
-        if (key?.conditions && !isEmpty(key.conditions)) {
-          obj.conditions = [];
-          Object.entries(key.conditions).map(
-            ([conditionKey, conditionValue]) => {
-              if (conditionValue != "" && conditionValue != null) {
-                return obj.conditions.push({
-                  type: conditionKey,
-                  values: isArray(conditionValue)
-                    ? conditionValue.map((m) =>
-                        typeof m === "object" && m.value ? m.value : m
-                      )
-                    : [conditionValue]
-                });
-              }
-              return null;
-            }
-          );
-        }
-
         policyResourceItem.push(obj);
       }
     }
@@ -248,32 +294,53 @@ const CreateNewPolicyForm = () => {
       );
       return;
     }
+    if (
+      hasMissingConstraintFieldsOnPolicyItems(
+        values,
+        selectedServiceComponentDef
+      )
+    ) {
+      toast.error(
+        "Please complete each constraint rule in the Access Rules step: select 
a constraint name, choose permissions, and fill all required parameters."
+      );
+      return;
+    }
     try {
       setBlockUI(true);
       let data = {};
-
-      // Policy Items (Allow Conditions)
-      data.policyItems = getPolicyItemsVal(values, "policyItems");
-      data.allowExceptions = getPolicyItemsVal(values, "allowExceptions");
-
-      // Deny Policy Items
-      if (values?.isDenyAllElse) {
+      const isConstraintPolicy =
+        values.policyType ===
+        RangerPolicyType.RANGER_CONSTRAINT_POLICY_TYPE.value;
+
+      if (isConstraintPolicy) {
+        data.constraintPolicyItems = getConstraintPolicyItemsVal(values);
+        data.policyItems = [];
+        data.allowExceptions = [];
         data.denyPolicyItems = [];
         data.denyExceptions = [];
+        data.dataMaskPolicyItems = [];
+        data.rowFilterPolicyItems = [];
       } else {
-        data.denyPolicyItems = getPolicyItemsVal(values, "denyPolicyItems");
-        data.denyExceptions = getPolicyItemsVal(values, "denyExceptions");
-      }
+        data.policyItems = getPolicyItemsVal(values, "policyItems");
+        data.allowExceptions = getPolicyItemsVal(values, "allowExceptions");
 
-      // Masking and Row Filter Policy Items
-      data.dataMaskPolicyItems = getPolicyItemsVal(
-        values,
-        "dataMaskPolicyItems"
-      );
-      data.rowFilterPolicyItems = getPolicyItemsVal(
-        values,
-        "rowFilterPolicyItems"
-      );
+        if (values?.isDenyAllElse) {
+          data.denyPolicyItems = [];
+          data.denyExceptions = [];
+        } else {
+          data.denyPolicyItems = getPolicyItemsVal(values, "denyPolicyItems");
+          data.denyExceptions = getPolicyItemsVal(values, "denyExceptions");
+        }
+
+        data.dataMaskPolicyItems = getPolicyItemsVal(
+          values,
+          "dataMaskPolicyItems"
+        );
+        data.rowFilterPolicyItems = getPolicyItemsVal(
+          values,
+          "rowFilterPolicyItems"
+        );
+      }
 
       // Basic policy fields
       data.name = values.policyName;
@@ -429,6 +496,9 @@ 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",
@@ -591,7 +661,8 @@ const CreateNewPolicyForm = () => {
           selectedServiceComponentDef={selectedServiceComponentDef}
         />
       ),
-      validate: validatePolicyItems
+      validate: (values) =>
+        validatePolicyItems(values, selectedServiceComponentDef)
     },
     {
       title: "Policy Details",
@@ -634,6 +705,10 @@ const CreateNewPolicyForm = () => {
                         form,
                         selectedServiceComponentDef
                       );
+                      pruneConstraintPolicyItemsToMatchedResources(
+                        form,
+                        selectedServiceComponentDef
+                      );
 
                       const conditionDefVal = policyConditionUpdatedJSON(
                         selectedServiceComponentDef?.policyConditions
diff --git 
a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/ReviewConfirmStep.jsx
 
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/ReviewConfirmStep.jsx
index 8f555c5dc..55728e4a8 100644
--- 
a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/ReviewConfirmStep.jsx
+++ 
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/PolicyCreateStepperStepComp/ReviewConfirmStep.jsx
@@ -23,7 +23,10 @@ import { isEmpty, isArray, find, map } from "lodash";
 import moment from "moment";
 import {
   getResourcesDefVal,
-  getPolicyConditionDisplayLbl
+  getPolicyConditionDisplayLbl,
+  getConstraintNameFromField,
+  getConstraintDefByName,
+  getAccessTypesForConstraintDef
 } from "Utils/XAUtils";
 import { DefStatus, RangerPolicyType } from "Utils/XAEnums";
 import userColourIcon from "Images/user-colour.svg";
@@ -91,6 +94,8 @@ const ReviewConfirmStep = ({ values, 
selectedServiceComponentDef }) => {
     policyType == RangerPolicyType.RANGER_MASKING_POLICY_TYPE.value;
   const isRowFilter =
     policyType == RangerPolicyType.RANGER_ROW_FILTER_POLICY_TYPE.value;
+  const isConstraint =
+    policyType == RangerPolicyType.RANGER_CONSTRAINT_POLICY_TYPE.value;
   const enableDenyAndExceptions =
     selectedServiceComponentDef?.options?.enableDenyAndExceptionsInPolicies ===
     "true";
@@ -99,14 +104,25 @@ const ReviewConfirmStep = ({ values, 
selectedServiceComponentDef }) => {
     ? "Masking"
     : isRowFilter
       ? "Row Filter"
-      : "Access";
+      : isConstraint
+        ? RangerPolicyType.RANGER_CONSTRAINT_POLICY_TYPE.label
+        : "Access";
 
-  const displayTagPermissionItem = (permission) => {
+  const displayTagPermissionItem = (permission, constraintName) => {
     let srcOp = [];
     if (isMasking) {
       srcOp = selectedServiceComponentDef.dataMaskDef?.accessTypes || [];
     } else if (isRowFilter) {
       srcOp = selectedServiceComponentDef.rowFilterDef?.accessTypes || [];
+    } else if (isConstraint && constraintName) {
+      const constraintDef = getConstraintDefByName(
+        selectedServiceComponentDef,
+        constraintName
+      );
+      srcOp = getAccessTypesForConstraintDef(
+        selectedServiceComponentDef,
+        constraintDef
+      );
     } else {
       srcOp = selectedServiceComponentDef.accessTypes;
     }
@@ -245,7 +261,25 @@ const ReviewConfirmStep = ({ values, 
selectedServiceComponentDef }) => {
     const hasRowFilter = !!(
       item.rowFilterInfo && String(item.rowFilterInfo).trim()
     );
-    const hasDelegateAdmin = !isMasking && !isRowFilter && !isTagAccesses;
+    const constraintName = getConstraintNameFromField(
+      item.constraintInfo?.constraintName
+    );
+    const constraintDef = constraintName
+      ? getConstraintDefByName(selectedServiceComponentDef, constraintName)
+      : null;
+    const constraintParams = item.constraintInfo?.parameters;
+    const hasConstraintParams =
+      constraintParams &&
+      typeof constraintParams === "object" &&
+      Object.keys(constraintParams).some(
+        (k) =>
+          constraintParams[k] !== "" &&
+          constraintParams[k] != null &&
+          constraintParams[k] !== undefined
+      );
+    const hasConstraintName = Boolean(constraintName);
+    const hasDelegateAdmin =
+      !isMasking && !isRowFilter && !isConstraint && !isTagAccesses;
 
     if (
       !hasPrincipal &&
@@ -253,6 +287,8 @@ const ReviewConfirmStep = ({ values, 
selectedServiceComponentDef }) => {
       !hasConditions &&
       !hasDataMask &&
       !hasRowFilter &&
+      !hasConstraintName &&
+      !hasConstraintParams &&
       !hasDelegateAdmin
     ) {
       return null;
@@ -269,6 +305,14 @@ const ReviewConfirmStep = ({ values, 
selectedServiceComponentDef }) => {
 
         {/* rule body */}
         <div className="px-3 py-2">
+          {isConstraint && hasConstraintName && (
+            <InfoRow label="Constraint Name">
+              <Badge bg="info">
+                {constraintDef?.label || constraintName}
+              </Badge>
+            </InfoRow>
+          )}
+
           {hasAccesses && (
             <InfoRow label="Permissions">
               {isTagAccesses ? (
@@ -288,7 +332,10 @@ const ReviewConfirmStep = ({ values, 
selectedServiceComponentDef }) => {
                         <td>
                           <div className="d-flex flex-wrap gap-1">
                             {(
-                              displayTagPermissionItem(t.permission || []) || 
[]
+                              displayTagPermissionItem(
+                                t.permission || [],
+                                constraintName
+                              ) || []
                             ).map((p, pi) => (
                               <Badge key={pi} bg="primary">
                                 {p}
@@ -381,13 +428,43 @@ const ReviewConfirmStep = ({ values, 
selectedServiceComponentDef }) => {
 
           {/* Row filter - ONLY SHOW IF HAS ROW FILTER */}
           {hasRowFilter && (
-            <InfoRow label="Row Level Filter" isLast={true}>
+            <InfoRow label="Row Level Filter" isLast={!hasConstraintParams}>
               <code className="bg-light border rounded px-2 py-1 small 
d-inline-block">
                 {String(item.rowFilterInfo)}
               </code>
             </InfoRow>
           )}
 
+          {isConstraint && hasConstraintParams && (
+            <InfoRow label="Parameters" isLast={true}>
+              <Table bordered size="sm" className="mb-0 align-middle">
+                <thead className="table-light">
+                  <tr>
+                    <th className="small p-2">Parameter</th>
+                    <th className="small p-2">Value</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {Object.keys(constraintParams).map((paramKey) => {
+                    const paramVal = constraintParams[paramKey];
+                    if (paramVal === "" || paramVal == null) return null;
+                    const paramDef = find(constraintDef?.parameters || [], {
+                      name: paramKey
+                    });
+                    return (
+                      <tr key={paramKey}>
+                        <td style={{ minWidth: "210px" }}>
+                          {paramDef?.label || paramKey}
+                        </td>
+                        <td className="text-word-break">{String(paramVal)}</td>
+                      </tr>
+                    );
+                  })}
+                </tbody>
+              </Table>
+            </InfoRow>
+          )}
+
           {/* Delegate Admin - ONLY SHOW IF HAS DELEGATE ADMIN */}
           {hasDelegateAdmin && (
             <InfoRow label="Delegate Admin" isLast={true}>
@@ -446,6 +523,13 @@ const ReviewConfirmStep = ({ values, 
selectedServiceComponentDef }) => {
       return (
         <RuleGroup attrName="rowFilterPolicyItems" title="Row Filter Rules" />
       );
+    if (isConstraint)
+      return (
+        <RuleGroup
+          attrName="constraintPolicyItems"
+          title="Constraint Rules"
+        />
+      );
 
     return (
       <>
@@ -584,7 +668,15 @@ const ReviewConfirmStep = ({ values, 
selectedServiceComponentDef }) => {
             </InfoRow>
             <InfoRow label="Policy Type" isLast={true}>
               <Badge
-                bg={isMasking ? "warning" : isRowFilter ? "info" : "primary"}
+                bg={
+                  isMasking
+                    ? "warning"
+                    : isRowFilter
+                      ? "info"
+                      : isConstraint
+                        ? "secondary"
+                        : "primary"
+                }
                 text={isMasking ? "dark" : undefined}
               >
                 {policyTypeLabel}
@@ -604,7 +696,17 @@ const ReviewConfirmStep = ({ values, 
selectedServiceComponentDef }) => {
         {/* Step 3 — Access Rules */}
         <Accordion.Item eventKey="3">
           <Accordion.Header>
-            <SectionHeader title="Access Rules" />
+            <SectionHeader
+              title={
+                isMasking
+                  ? "Mask Rules"
+                  : isRowFilter
+                    ? "Row Filter Rules"
+                    : isConstraint
+                      ? "Constraint Rules"
+                      : "Access Rules"
+              }
+            />
           </Accordion.Header>
           <Accordion.Body>{renderAccessRules()}</Accordion.Body>
         </Accordion.Item>
diff --git 
a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/policyFormValidation.js
 
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/policyFormValidation.js
index 91bfa331b..fd0cf1c72 100644
--- 
a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/policyFormValidation.js
+++ 
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/NewPolicyForm/policyFormValidation.js
@@ -18,6 +18,10 @@
  */
 
 import { isEmpty, isArray } from "lodash";
+import {
+  getConstraintNameFromField,
+  getConstraintDefByName
+} from "Utils/XAUtils";
 
 export const validatePolicyName = (policyName) => {
   if (!policyName || policyName.trim() === "") {
@@ -117,7 +121,61 @@ const validateRowFilterPolicyItem = (item) => {
   return ruleErrors;
 };
 
-export const validateSinglePolicyItem = (item, policyType) => {
+const hasConstraintNameInItem = (item) =>
+  Boolean(getConstraintNameFromField(item.constraintInfo?.constraintName));
+
+const hasMissingMandatoryConstraintParameters = (item, serviceDef) => {
+  if (!serviceDef) {
+    return false;
+  }
+  const constraintName = getConstraintNameFromField(
+    item.constraintInfo?.constraintName
+  );
+  if (!constraintName) {
+    return false;
+  }
+  const def = getConstraintDefByName(serviceDef, constraintName);
+  const parameters = item.constraintInfo?.parameters || {};
+  for (const param of def?.parameters || []) {
+    if (param.mandatory) {
+      const val = parameters[param.name];
+      if (val === "" || val == null) {
+        return true;
+      }
+    }
+  }
+  return false;
+};
+
+const validateConstraintPolicyItem = (item, serviceDef) => {
+  const ruleErrors = {};
+  const principals = hasPrincipals(item);
+  const accesses = hasAccessesInItem(item);
+  const constraintName = hasConstraintNameInItem(item);
+  const missingMandatoryParams = hasMissingMandatoryConstraintParameters(
+    item,
+    serviceDef
+  );
+
+  if (principals && !constraintName) {
+    ruleErrors.constraintName =
+      "Please select a constraint name for this policy";
+  }
+  if (principals && constraintName && !accesses) {
+    ruleErrors.accesses = "Please select permission for this policy";
+  }
+  if (principals && constraintName && missingMandatoryParams) {
+    ruleErrors.parameters =
+      "Please provide all required parameters for this constraint";
+  }
+  if ((accesses || constraintName) && !principals) {
+    ruleErrors.users = "Please select users/groups/roles for this policy";
+  }
+
+  return ruleErrors;
+};
+
+export const validateSinglePolicyItem = (item, policyType, serviceDef) => {
   if (!item || isEmpty(item)) {
     return {};
   }
@@ -127,6 +185,8 @@ export const validateSinglePolicyItem = (item, policyType) 
=> {
       return validateDataMaskPolicyItem(item);
     case 2:
       return validateRowFilterPolicyItem(item);
+    case 4:
+      return validateConstraintPolicyItem(item, serviceDef);
     case 0:
     default:
       return validateAccessPolicyItem(item);
@@ -138,6 +198,8 @@ const getArraysToValidate = (policyType) => {
     return ["dataMaskPolicyItems"];
   } else if (policyType == 2) {
     return ["rowFilterPolicyItems"];
+  } else if (policyType == 4) {
+    return ["constraintPolicyItems"];
   }
   return [
     "policyItems",
@@ -147,13 +209,17 @@ const getArraysToValidate = (policyType) => {
   ];
 };
 
-export const validatePolicyItemsArray = (values, policyType) => {
+export const validatePolicyItemsArray = (values, policyType, serviceDef) => {
   const errors = {};
   const arraysToValidate = getArraysToValidate(policyType);
 
   arraysToValidate.forEach((key) => {
     (values[key] || []).forEach((item, i) => {
-      const ruleErrors = validateSinglePolicyItem(item, policyType);
+      const ruleErrors = validateSinglePolicyItem(
+        item,
+        policyType,
+        serviceDef
+      );
 
       if (!isEmpty(ruleErrors)) {
         if (!errors[key]) errors[key] = [];
@@ -179,9 +245,9 @@ export const validatePolicyDetails = (values) => {
   return errors;
 };
 
-export const validatePolicyItems = (values) => {
+export const validatePolicyItems = (values, serviceDef) => {
   const policyType = values.policyType ?? 0;
-  return validatePolicyItemsArray(values, policyType);
+  return validatePolicyItemsArray(values, policyType, serviceDef);
 };
 
 /**
@@ -209,6 +275,36 @@ export const hasMissingAccessPermissionOnPolicyItems = 
(values) => {
   return false;
 };
 
+/**
+ * True when a constraint rule is missing constraint name, permissions, or
+ * mandatory parameters (e.g. after resources changed). Use on final submit.
+ */
+export const hasMissingConstraintFieldsOnPolicyItems = (values, serviceDef) => 
{
+  if ((values.policyType ?? 0) !== 4) {
+    return false;
+  }
+  const errors = validatePolicyItemsArray(values, 4, serviceDef);
+  if (isEmpty(errors)) {
+    return false;
+  }
+  const arr = errors.constraintPolicyItems;
+  if (!isArray(arr)) {
+    return false;
+  }
+  for (let i = 0; i < arr.length; i++) {
+    const itemError = arr[i];
+    if (
+      itemError &&
+      (itemError.accesses ||
+        itemError.constraintName ||
+        itemError.parameters)
+    ) {
+      return true;
+    }
+  }
+  return false;
+};
+
 export const validatePolicyForm = (values) => ({
   ...validatePolicyDetails(values),
   ...validatePolicyItems(values)

Reply via email to