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

pradeepagrawal8184 pushed a commit to branch ranger-2.9
in repository https://gitbox.apache.org/repos/asf/ranger.git


The following commit(s) were added to refs/heads/ranger-2.9 by this push:
     new 8603cbb36 RANGER-5628/RANGER-5667: Backport Ozone action policy 
support and follow-on fixes to ranger-2.9 (#1054)
8603cbb36 is described below

commit 8603cbb362344085db57f771d83bd3b630020291
Author: Ramachandran Krishnan <[email protected]>
AuthorDate: Fri Jul 10 10:26:30 2026 +0530

    RANGER-5628/RANGER-5667: Backport Ozone action policy support and follow-on 
fixes to ranger-2.9 (#1054)
    
    * RANGER-5628: Backport support for actions in Ozone Service Definition to 
ranger-2.9 (#995)
    
    Backport of https://github.com/apache/ranger/pull/995 for the ranger-2.9 
release branch.
    
    * RANGER-5667: fixes in support for actions in Ozone policies (#1053)
    
    ---------
    
    Co-authored-by: fmorg-git <[email protected]>
---
 .../conditionevaluator/RangerActionMatcher.java    |  54 ++
 .../ranger/plugin/model/RangerInlinePolicy.java    |  20 +-
 .../RangerInlinePolicyEvaluator.java               |  17 +-
 .../plugin/util/RangerActionListMatcher.java       | 125 ++++
 .../service-defs/ranger-servicedef-ozone.json      |   8 +
 .../RangerActionMatcherTest.java                   | 111 ++++
 .../plugin/util/RangerActionListMatcherTest.java   | 265 ++++++++
 .../test_inline_policies_ozone.json                | 111 ++++
 dev-support/checkstyle-suppressions.xml            |   1 +
 .../ozone/authorizer/RangerOzoneAuthorizer.java    |   8 +-
 .../authorizer/TestRangerOzoneAuthorizer.java      |  72 ++
 plugin-ozone/src/test/resources/om_dev_ozone.json  |   6 +
 pom.xml                                            |   2 +-
 ...zoneServiceDefPolicyConditionUpdate_J10065.java | 167 +++++
 .../src/components/CommonComponents.jsx            |  26 +
 .../react-webapp/src/components/Editable.jsx       | 486 +++++++++-----
 .../src/hooks/usePruneStaleConditions.js           | 153 +++++
 .../main/webapp/react-webapp/src/styles/style.css  |  12 +
 .../src/utils/actionRequirements/ozone.json        |  47 ++
 .../src/utils/actionRequirements/registry.js       |  30 +
 .../react-webapp/src/utils/policyConditionUtils.js | 722 +++++++++++++++++++++
 .../views/PolicyListing/AddUpdatePolicyForm.jsx    |   4 +
 .../views/PolicyListing/PolicyConditionsComp.jsx   |  42 +-
 .../views/PolicyListing/PolicyPermissionItem.jsx   |  89 ++-
 24 files changed, 2375 insertions(+), 203 deletions(-)

diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/conditionevaluator/RangerActionMatcher.java
 
b/agents-common/src/main/java/org/apache/ranger/plugin/conditionevaluator/RangerActionMatcher.java
new file mode 100644
index 000000000..9342ea68b
--- /dev/null
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/conditionevaluator/RangerActionMatcher.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+package org.apache.ranger.plugin.conditionevaluator;
+
+import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
+import org.apache.ranger.plugin.util.RangerActionListMatcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class RangerActionMatcher extends RangerAbstractConditionEvaluator {
+    private static final Logger LOG = 
LoggerFactory.getLogger(RangerActionMatcher.class);
+
+    private RangerActionListMatcher actionMatcher;
+
+    @Override
+    public void init() {
+        LOG.debug("==> RangerActionMatcher.init({})", condition);
+
+        super.init();
+
+        this.actionMatcher = new RangerActionListMatcher(condition == null ? 
null : condition.getValues());
+
+        LOG.debug("<== RangerActionMatcher.init({})", condition);
+    }
+
+    @Override
+    public boolean isMatched(final RangerAccessRequest request) {
+        LOG.debug("==> RangerActionMatcher.isMatched({})", request);
+
+        final RangerActionListMatcher matcher = actionMatcher;
+        final String                  action  = request != null ? 
request.getAction() : null;
+        final boolean                 ret     = matcher == null || 
matcher.isMatch(action);
+
+        LOG.debug("<== RangerActionMatcher.isMatched({}): {}", request, ret);
+
+        return ret;
+    }
+}
diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/model/RangerInlinePolicy.java
 
b/agents-common/src/main/java/org/apache/ranger/plugin/model/RangerInlinePolicy.java
index f9144d265..cd894119c 100644
--- 
a/agents-common/src/main/java/org/apache/ranger/plugin/model/RangerInlinePolicy.java
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/model/RangerInlinePolicy.java
@@ -112,14 +112,20 @@ public static class Grant {
         private Set<String> principals;  // example: [ "u:user1, "g:group1", 
"r:role1" ]; if empty, means public grant
         private Set<String> resources;   // example: [ 
"key:vol1/bucket1/db1/tbl1/*", "key:vol1/bucket1/db1/tbl2/*" ]; if empty, means 
all resources
         private Set<String> permissions; // example: [ "read", "write" ]; if 
empty, means no permission
+        private Set<String> actions;     // optional: [ "GetObject", "Put*", 
"*" ]; if empty or null, means all actions
 
         public Grant() {
         }
 
         public Grant(Set<String> principals, Set<String> resources, 
Set<String> permissions) {
+            this(principals, resources, permissions, null);
+        }
+
+        public Grant(Set<String> principals, Set<String> resources, 
Set<String> permissions, Set<String> actions) {
             this.principals  = principals;
             this.resources   = resources;
             this.permissions = permissions;
+            this.actions     = actions;
         }
 
         public Set<String> getPrincipals() {
@@ -146,6 +152,14 @@ public void setPermissions(Set<String> permissions) {
             this.permissions = permissions;
         }
 
+        public Set<String> getActions() {
+            return actions;
+        }
+
+        public void setActions(Set<String> actions) {
+            this.actions = actions;
+        }
+
         @Override
         public boolean equals(Object o) {
             if (this == o) {
@@ -158,12 +172,13 @@ public boolean equals(Object o) {
 
             return Objects.equals(principals, that.principals) &&
                     Objects.equals(resources, that.resources) &&
-                    Objects.equals(permissions, that.permissions);
+                    Objects.equals(permissions, that.permissions) &&
+                    Objects.equals(actions, that.actions);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(principals, resources, permissions);
+            return Objects.hash(principals, resources, permissions, actions);
         }
 
         @Override
@@ -172,6 +187,7 @@ public String toString() {
                     "principals=" + principals +
                     ", resources=" + resources +
                     ", permissions=" + permissions +
+                    ", actions=" + actions +
                     '}';
         }
     }
diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerInlinePolicyEvaluator.java
 
b/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerInlinePolicyEvaluator.java
index 05e372c48..d230bc17a 100644
--- 
a/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerInlinePolicyEvaluator.java
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerInlinePolicyEvaluator.java
@@ -33,6 +33,7 @@
 import 
org.apache.ranger.plugin.policyresourcematcher.RangerDefaultPolicyResourceMatcher;
 import 
org.apache.ranger.plugin.policyresourcematcher.RangerPolicyResourceMatcher;
 import org.apache.ranger.plugin.util.RangerAccessRequestUtil;
+import org.apache.ranger.plugin.util.RangerActionListMatcher;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -156,11 +157,13 @@ private List<GrantEvaluator> 
toGrantEvaluators(RangerInlinePolicy policy) {
     private class GrantEvaluator {
         private final RangerInlinePolicy.Grant         grant;
         private final Set<String>                      permissions;
+        private final RangerActionListMatcher          actionMatcher;
         private final Set<RangerPolicyResourceMatcher> resourceMatchers = new 
HashSet<>();
 
         public GrantEvaluator(RangerInlinePolicy.Grant grant) {
-            this.grant       = grant;
-            this.permissions = 
policyEngine.getServiceDefHelper().expandImpliedAccessGrants(grant.getPermissions());
+            this.grant         = grant;
+            this.permissions   = 
policyEngine.getServiceDefHelper().expandImpliedAccessGrants(grant.getPermissions());
+            this.actionMatcher = new 
RangerActionListMatcher(grant.getActions());
 
             if (grant.getResources() != null) {
                 for (String resource : grant.getResources()) {
@@ -187,7 +190,7 @@ public GrantEvaluator(RangerInlinePolicy.Grant grant) {
         }
 
         public boolean isAllowed(RangerAccessRequest request) {
-            boolean ret = isPrincipalMatch(request) && 
isPermissionMatch(request) && isResourceMatch(request);
+            boolean ret = isPrincipalMatch(request) && 
isPermissionMatch(request) && isActionMatch(request) && 
isResourceMatch(request);
 
             LOG.debug("isAllowed(grant={}, request={}): ret={}", grant, 
request, ret);
 
@@ -238,6 +241,14 @@ private boolean isPermissionMatch(RangerAccessRequest 
request) {
             return ret;
         }
 
+        private boolean isActionMatch(RangerAccessRequest request) {
+            boolean ret = actionMatcher.isMatch(request != null ? 
request.getAction() : null);
+
+            LOG.debug("isActionMatch(grant={}, request={}): ret={}", grant, 
request, ret);
+
+            return ret;
+        }
+
         private boolean isResourceMatch(RangerAccessRequest request) {
             boolean ret = CollectionUtils.isEmpty(grant.getResources()); // 
match all resources
 
diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerActionListMatcher.java
 
b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerActionListMatcher.java
new file mode 100644
index 000000000..db7807b70
--- /dev/null
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerActionListMatcher.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+
+package org.apache.ranger.plugin.util;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+public class RangerActionListMatcher {
+    private final boolean     allowAnyAction;
+    private final Set<String> exactActions = new HashSet<>();
+    private final String[]    prefixActions;
+
+    public RangerActionListMatcher(Collection<String> actions) {
+        boolean            allowAny       = CollectionUtils.isEmpty(actions);
+        final List<String> tempPrefixList = new ArrayList<>();
+
+        if (!allowAny) {
+            for (String a : actions) {
+                final String action = StringUtils.trimToEmpty(a);
+
+                if (action.isEmpty()) {
+                    continue;
+                }
+
+                if (action.equals("*")) {
+                    allowAny = true;
+                    exactActions.clear();
+                    tempPrefixList.clear();
+                    break;
+                }
+
+                if (action.endsWith("*")) {
+                    final String prefix = 
StringUtils.trimToEmpty(action.substring(0, action.length() - 
1)).toLowerCase(Locale.ROOT);
+
+                    if (prefix.isEmpty()) {
+                        allowAny = true;
+                        exactActions.clear();
+                        tempPrefixList.clear();
+                        break;
+                    }
+
+                    tempPrefixList.add(prefix);
+                } else {
+                    exactActions.add(action.toLowerCase(Locale.ROOT));
+                }
+            }
+
+            if (!allowAny && exactActions.isEmpty() && 
tempPrefixList.isEmpty()) {
+                allowAny = true;
+            }
+        }
+
+        this.allowAnyAction = allowAny;
+
+        if (!tempPrefixList.isEmpty()) {
+            tempPrefixList.sort((s1, s2) -> Integer.compare(s1.length(), 
s2.length()));
+
+            List<String> optimizedPrefixes = new ArrayList<>();
+            for (String p : tempPrefixList) {
+                boolean isCovered = false;
+                for (String optPrefix : optimizedPrefixes) {
+                    if (p.startsWith(optPrefix)) {
+                        isCovered = true;
+                        break;
+                    }
+                }
+                if (!isCovered) {
+                    optimizedPrefixes.add(p);
+                }
+            }
+            this.prefixActions = optimizedPrefixes.toArray(new String[0]);
+        } else {
+            this.prefixActions = new String[0];
+        }
+    }
+
+    public boolean isMatch(String requestAction) {
+        if (allowAnyAction) {
+            return true;
+        }
+
+        // Action restrictions exist; missing request action is not a match.
+        if (StringUtils.isBlank(requestAction)) {
+            return false;
+        }
+
+        final String actionLower = requestAction.toLowerCase(Locale.ROOT);
+
+        if (exactActions.contains(actionLower)) {
+            return true;
+        }
+
+        for (String prefix : prefixActions) {
+            if (actionLower.startsWith(prefix)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git 
a/agents-common/src/main/resources/service-defs/ranger-servicedef-ozone.json 
b/agents-common/src/main/resources/service-defs/ranger-servicedef-ozone.json
index 9f67ae4c6..bef99877c 100755
--- a/agents-common/src/main/resources/service-defs/ranger-servicedef-ozone.json
+++ b/agents-common/src/main/resources/service-defs/ranger-servicedef-ozone.json
@@ -117,6 +117,14 @@
       "label":       "IP Address Range",
       "description": "IP Address Range",
       "uiHint" :     "{ \"isMultiValue\":true }"
+    },
+    {
+      "itemId":      2,
+      "name":        "action-matches",
+      "evaluator":   
"org.apache.ranger.plugin.conditionevaluator.RangerActionMatcher",
+      "label":       "Action",
+      "description": "Action",
+      "uiHint":      "{ \"isMultiValue\":true, \"options\": [\"*\", \"Get*\", 
\"List*\", \"Put*\", \"Delete*\", \"Create*\", \"GetObject\", 
\"GetObjectTagging\", \"PutObject\", \"PutObjectTagging\", \"ListBucket\", 
\"CreateBucket\", \"DeleteBucket\", \"GetBucketAcl\", \"PutBucketAcl\", 
\"ListBucketMultipartUploads\", \"DeleteObject\", \"ListAllMyBuckets\", 
\"ListMultipartUploadParts\", \"AbortMultipartUpload\", 
\"DeleteObjectTagging\"], \"actionRequirementsFile\": \"ozone\" }"
     }
   ]
 }
diff --git 
a/agents-common/src/test/java/org/apache/ranger/plugin/conditionevaluator/RangerActionMatcherTest.java
 
b/agents-common/src/test/java/org/apache/ranger/plugin/conditionevaluator/RangerActionMatcherTest.java
new file mode 100644
index 000000000..5e23c0f55
--- /dev/null
+++ 
b/agents-common/src/test/java/org/apache/ranger/plugin/conditionevaluator/RangerActionMatcherTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+package org.apache.ranger.plugin.conditionevaluator;
+
+import org.apache.ranger.plugin.model.RangerPolicy.RangerPolicyItemCondition;
+import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class RangerActionMatcherTest {
+    @Test
+    void testNullOrEmptyConditionMatchesAll() {
+        final RangerActionMatcher matcherNull = createMatcher(null);
+
+        assertTrue(matcherNull.isMatched(createRequest("PutObject")));
+        assertTrue(matcherNull.isMatched(createRequest(null)));
+
+        final RangerActionMatcher matcherEmpty = createMatcher(new String[] 
{});
+
+        assertTrue(matcherEmpty.isMatched(createRequest("PutObject")));
+        assertTrue(matcherEmpty.isMatched(createRequest(null)));
+    }
+
+    @Test
+    void testWildcardMatchesAll() {
+        final RangerActionMatcher matcher = createMatcher(new String[] {"*"});
+
+        assertTrue(matcher.isMatched(createRequest("GetObject")));
+        assertTrue(matcher.isMatched(createRequest("PutObject")));
+        assertTrue(matcher.isMatched(createRequest(null)));
+    }
+
+    @Test
+    void testNoRequestActionDoesNotMatchWhenActionsSpecified() {
+        final RangerActionMatcher matcher = createMatcher(new String[] 
{"PutObject"});
+
+        assertFalse(matcher.isMatched(createRequest(null)));
+        assertFalse(matcher.isMatched(createRequest("")));
+        assertFalse(matcher.isMatched(createRequest("   ")));
+    }
+
+    @Test
+    void testExactMatchCaseInsensitive() {
+        final RangerActionMatcher matcher = createMatcher(new String[] 
{"GetObject"});
+
+        assertTrue(matcher.isMatched(createRequest("GetObject")));
+        assertTrue(matcher.isMatched(createRequest("getobject")));
+        assertFalse(matcher.isMatched(createRequest("PutObject")));
+    }
+
+    @Test
+    void testTrailingWildcardPrefixMatchCaseInsensitive() {
+        final RangerActionMatcher matcher = createMatcher(new String[] 
{"Put*"});
+
+        assertTrue(matcher.isMatched(createRequest("PutObject")));
+        assertTrue(matcher.isMatched(createRequest("putobjecttagging")));
+        assertFalse(matcher.isMatched(createRequest("GetObject")));
+    }
+
+    private RangerActionMatcher createMatcher(final String[] actionsArray) {
+        final RangerActionMatcher matcher = new RangerActionMatcher();
+
+        if (actionsArray == null) {
+            matcher.setConditionDef(null);
+            matcher.setPolicyItemCondition(null);
+        } else {
+            final RangerPolicyItemCondition condition = 
mock(RangerPolicyItemCondition.class);
+            final List<String>              actions   = 
Arrays.asList(actionsArray);
+
+            when(condition.getValues()).thenReturn(actions);
+            matcher.setConditionDef(null);
+            matcher.setPolicyItemCondition(condition);
+        }
+
+        matcher.init();
+
+        return matcher;
+    }
+
+    private RangerAccessRequest createRequest(final String action) {
+        final RangerAccessRequest request = mock(RangerAccessRequest.class);
+
+        when(request.getAction()).thenReturn(action);
+
+        return request;
+    }
+}
diff --git 
a/agents-common/src/test/java/org/apache/ranger/plugin/util/RangerActionListMatcherTest.java
 
b/agents-common/src/test/java/org/apache/ranger/plugin/util/RangerActionListMatcherTest.java
new file mode 100644
index 000000000..b6bdfebf9
--- /dev/null
+++ 
b/agents-common/src/test/java/org/apache/ranger/plugin/util/RangerActionListMatcherTest.java
@@ -0,0 +1,265 @@
+/*
+ * 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.
+ */
+
+package org.apache.ranger.plugin.util;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class RangerActionListMatcherTest {
+    @Test
+    void testNullActionsMatchAll() {
+        final RangerActionListMatcher matcher = new 
RangerActionListMatcher(null);
+
+        assertTrue(matcher.isMatch("PutObject"));
+        assertTrue(matcher.isMatch("GetObject"));
+        assertTrue(matcher.isMatch(null));
+        assertTrue(matcher.isMatch(""));
+    }
+
+    @Test
+    void testEmptyActionsMatchAll() {
+        final RangerActionListMatcher matcher = new 
RangerActionListMatcher(Collections.emptyList());
+
+        assertTrue(matcher.isMatch("PutObject"));
+        assertTrue(matcher.isMatch("GetObject"));
+        assertTrue(matcher.isMatch(null));
+    }
+
+    @Test
+    void testUniversalWildcard() {
+        final RangerActionListMatcher matcher = new 
RangerActionListMatcher(Arrays.asList("*"));
+
+        assertTrue(matcher.isMatch("PutObject"));
+        assertTrue(matcher.isMatch("GetObject"));
+        assertTrue(matcher.isMatch("DeleteBucket"));
+        assertTrue(matcher.isMatch(null));
+    }
+
+    @Test
+    void testBareWildcardWithSpaces() {
+        // "  *  " after trim becomes "*" which should be treated as allow-all
+        final RangerActionListMatcher matcher = new 
RangerActionListMatcher(Arrays.asList("  *  "));
+
+        assertTrue(matcher.isMatch("PutObject"));
+        assertTrue(matcher.isMatch("GetObject"));
+    }
+
+    @Test
+    void testExactMatchCaseInsensitive() {
+        final RangerActionListMatcher matcher = new 
RangerActionListMatcher(Arrays.asList("GetObject"));
+
+        assertTrue(matcher.isMatch("GetObject"));
+        assertTrue(matcher.isMatch("getobject"));
+        assertTrue(matcher.isMatch("GETOBJECT"));
+        assertFalse(matcher.isMatch("PutObject"));
+        assertFalse(matcher.isMatch("GetObjectTagging"));
+    }
+
+    @Test
+    void testMultipleExactActions() {
+        final RangerActionListMatcher matcher = new RangerActionListMatcher(
+                Arrays.asList("GetObject", "PutObject", "DeleteObject"));
+
+        assertTrue(matcher.isMatch("GetObject"));
+        assertTrue(matcher.isMatch("PutObject"));
+        assertTrue(matcher.isMatch("DeleteObject"));
+        assertFalse(matcher.isMatch("ListBucket"));
+        assertFalse(matcher.isMatch("CreateBucket"));
+    }
+
+    @Test
+    void testSinglePrefixWildcard() {
+        final RangerActionListMatcher matcher = new 
RangerActionListMatcher(Arrays.asList("Put*"));
+
+        assertTrue(matcher.isMatch("PutObject"));
+        assertTrue(matcher.isMatch("PutObjectTagging"));
+        assertTrue(matcher.isMatch("PutBucketAcl"));
+        assertFalse(matcher.isMatch("GetObject"));
+        assertFalse(matcher.isMatch("DeleteObject"));
+    }
+
+    @Test
+    void testMultiplePrefixWildcards() {
+        final RangerActionListMatcher matcher = new RangerActionListMatcher(
+                Arrays.asList("Put*", "Get*"));
+
+        assertTrue(matcher.isMatch("PutObject"));
+        assertTrue(matcher.isMatch("GetObject"));
+        assertTrue(matcher.isMatch("GetObjectTagging"));
+        assertTrue(matcher.isMatch("PutBucketAcl"));
+        assertFalse(matcher.isMatch("DeleteObject"));
+        assertFalse(matcher.isMatch("ListBucket"));
+    }
+
+    @Test
+    void testMixedExactAndPrefix() {
+        final RangerActionListMatcher matcher = new RangerActionListMatcher(
+                Arrays.asList("GetObject", "Put*", "ListBucket"));
+
+        assertTrue(matcher.isMatch("GetObject"));
+        assertTrue(matcher.isMatch("PutObject"));
+        assertTrue(matcher.isMatch("PutObjectTagging"));
+        assertTrue(matcher.isMatch("ListBucket"));
+        assertFalse(matcher.isMatch("GetObjectTagging"));
+        assertFalse(matcher.isMatch("DeleteObject"));
+        assertFalse(matcher.isMatch("ListAllMyBuckets"));
+    }
+
+    @Test
+    void testPrefixOptimizationRedundantPrefixes() {
+        // The constructor optimizes prefix patterns by removing any longer 
prefix that is
+        // already covered by a shorter one. Here "Put*" matches anything 
starting with "put"
+        // (lowercased), which is a superset of what "PutObject*" matches 
(only things starting
+        // with "putobject"). So "PutObject*" is redundant and gets removed 
from the prefix
+        // array to avoid unnecessary iteration at match time. This test 
proves the optimization
+        // doesn't break matching — "PutBucketAcl" still matches via the 
surviving "Put*" prefix.
+        final RangerActionListMatcher matcher = new RangerActionListMatcher(
+                Arrays.asList("PutObject*", "Put*"));
+
+        assertTrue(matcher.isMatch("PutObject"));
+        assertTrue(matcher.isMatch("PutObjectTagging"));
+        assertTrue(matcher.isMatch("PutBucketAcl"));
+    }
+
+    @Test
+    void testPrefixOptimizationNonRedundant() {
+        // "Put*" and "Get*" are independent — neither covers the other
+        final RangerActionListMatcher matcher = new RangerActionListMatcher(
+                Arrays.asList("Put*", "Get*"));
+
+        assertTrue(matcher.isMatch("PutObject"));
+        assertTrue(matcher.isMatch("GetObject"));
+        assertFalse(matcher.isMatch("DeleteObject"));
+    }
+
+    @Test
+    void testDuplicatePrefixes() {
+        // Duplicate "Put*" entries should be handled gracefully
+        final RangerActionListMatcher matcher = new RangerActionListMatcher(
+                Arrays.asList("Put*", "Put*", "Put*"));
+
+        assertTrue(matcher.isMatch("PutObject"));
+        assertFalse(matcher.isMatch("GetObject"));
+    }
+
+    @Test
+    void testBlankRequestActionDoesNotMatchWhenActionsSpecified() {
+        final RangerActionListMatcher matcher = new RangerActionListMatcher(
+                Arrays.asList("PutObject"));
+
+        // When action restrictions exist, missing action is not a match
+        assertFalse(matcher.isMatch(null));
+        assertFalse(matcher.isMatch(""));
+        assertFalse(matcher.isMatch("   "));
+    }
+
+    @Test
+    void testBlankRequestActionStillMatchesWhenNoActionsSpecified() {
+        final RangerActionListMatcher matcherNull = new 
RangerActionListMatcher(null);
+        assertTrue(matcherNull.isMatch(null));
+        assertTrue(matcherNull.isMatch(""));
+
+        final RangerActionListMatcher matcherEmpty = new 
RangerActionListMatcher(Collections.emptyList());
+        assertTrue(matcherEmpty.isMatch(null));
+        assertTrue(matcherEmpty.isMatch("   "));
+    }
+
+    @Test
+    void testBlankEntriesInActionsIgnored() {
+        final RangerActionListMatcher matcher = new RangerActionListMatcher(
+                Arrays.asList("", "  ", "PutObject"));
+
+        assertTrue(matcher.isMatch("PutObject"));
+        assertFalse(matcher.isMatch("GetObject"));
+    }
+
+    @Test
+    void testAllBlankActionsMatchAll() {
+        final RangerActionListMatcher matcher = new RangerActionListMatcher(
+                Arrays.asList("", "  ", "   "));
+
+        assertTrue(matcher.isMatch("PutObject"));
+        assertTrue(matcher.isMatch("GetObject"));
+        assertTrue(matcher.isMatch(null));
+        assertTrue(matcher.isMatch("   "));
+    }
+
+    @Test
+    void testPrefixWildcardCaseInsensitive() {
+        final RangerActionListMatcher matcher = new 
RangerActionListMatcher(Arrays.asList("Put*"));
+
+        assertTrue(matcher.isMatch("PUTOBJECT"));
+        assertTrue(matcher.isMatch("putobject"));
+        assertTrue(matcher.isMatch("PuToBjEcT"));
+    }
+
+    @Test
+    void testWildcardAmongOtherActions() {
+        // If "*" appears alongside other actions, it should still match all
+        final RangerActionListMatcher matcher = new RangerActionListMatcher(
+                Arrays.asList("PutObject", "*", "GetObject"));
+
+        assertTrue(matcher.isMatch("DeleteBucket"));
+        assertTrue(matcher.isMatch("ListAllMyBuckets"));
+        assertTrue(matcher.isMatch("PutObject"));
+    }
+
+    @Test
+    void testSingleCharacterPrefix() {
+        // "G*" should match anything starting with G
+        final RangerActionListMatcher matcher = new 
RangerActionListMatcher(Arrays.asList("G*"));
+
+        assertTrue(matcher.isMatch("GetObject"));
+        assertTrue(matcher.isMatch("GetBucketAcl"));
+        assertFalse(matcher.isMatch("PutObject"));
+    }
+
+    @Test
+    void testAllOzoneS3Actions() {
+        // Grant with both wildcards and exact actions matching Ozone S3 
patterns
+        final List<String> grantActions = Arrays.asList("Get*", "List*", 
"PutObject");
+        final RangerActionListMatcher matcher = new 
RangerActionListMatcher(grantActions);
+
+        // Should match
+        assertTrue(matcher.isMatch("GetObject"));
+        assertTrue(matcher.isMatch("GetObjectTagging"));
+        assertTrue(matcher.isMatch("GetBucketAcl"));
+        assertTrue(matcher.isMatch("ListBucket"));
+        assertTrue(matcher.isMatch("ListAllMyBuckets"));
+        assertTrue(matcher.isMatch("ListBucketMultipartUploads"));
+        assertTrue(matcher.isMatch("ListMultipartUploadParts"));
+        assertTrue(matcher.isMatch("PutObject"));
+
+        // Should not match
+        assertFalse(matcher.isMatch("PutObjectTagging"));
+        assertFalse(matcher.isMatch("PutBucketAcl"));
+        assertFalse(matcher.isMatch("DeleteObject"));
+        assertFalse(matcher.isMatch("DeleteObjectTagging"));
+        assertFalse(matcher.isMatch("DeleteBucket"));
+        assertFalse(matcher.isMatch("CreateBucket"));
+        assertFalse(matcher.isMatch("AbortMultipartUpload"));
+    }
+}
diff --git 
a/agents-common/src/test/resources/policyevaluator/test_inline_policies_ozone.json
 
b/agents-common/src/test/resources/policyevaluator/test_inline_policies_ozone.json
index 809c791a3..845ae8c36 100644
--- 
a/agents-common/src/test/resources/policyevaluator/test_inline_policies_ozone.json
+++ 
b/agents-common/src/test/resources/policyevaluator/test_inline_policies_ozone.json
@@ -227,6 +227,117 @@
         }
       },
       "result": { "isAudited": true, "isAllowed": true, "policyId": -1 }
+    },
+    { "name": "ALLOW 'write s3v/iceberg/key2;' for user=svc-etl; inline-policy 
restricts action=PutObject",
+      "request": {
+        "resource": { "elements": { "volume": "s3v", "bucket": "iceberg", 
"key": "key2" } }, "user": "svc-etl", "accessType": "write", "action": 
"PutObject",
+        "inlinePolicy": { "grantor":  "r:data-all-access", "mode": "INLINE",
+          "grants": [
+            { "principals": [ "u:svc-etl" ], "resources": [ 
"key:s3v/iceberg/key2" ], "permissions": [ "all" ], "actions": [ "PutObject" ] }
+          ]
+        }
+      },
+      "result": { "isAudited": true, "isAllowed": true, "policyId": -1 }
+    },
+    { "name": "ALLOW 'create s3v/iceberg/key2;' for user=svc-etl; 
inline-policy restricts action=PutObject",
+      "request": {
+        "resource": { "elements": { "volume": "s3v", "bucket": "iceberg", 
"key": "key2" } }, "user": "svc-etl", "accessType": "create", "action": 
"PutObject",
+        "inlinePolicy": { "grantor":  "r:data-all-access", "mode": "INLINE",
+          "grants": [
+            { "principals": [ "u:svc-etl" ], "resources": [ 
"key:s3v/iceberg/key2" ], "permissions": [ "all" ], "actions": [ "PutObject" ] }
+          ]
+        }
+      },
+      "result": { "isAudited": true, "isAllowed": true, "policyId": -1 }
+    },
+    { "name": "DENY 'read s3v/iceberg/key2;' for user=svc-etl; inline-policy 
restricts action=PutObject but request action=GetObject",
+      "request": {
+        "resource": { "elements": { "volume": "s3v", "bucket": "iceberg", 
"key": "key2" } }, "user": "svc-etl", "accessType": "read", "action": 
"GetObject",
+        "inlinePolicy": { "grantor":  "r:data-all-access", "mode": "INLINE",
+          "grants": [
+            { "principals": [ "u:svc-etl" ], "resources": [ 
"key:s3v/iceberg/key2" ], "permissions": [ "all" ], "actions": [ "PutObject" ] }
+          ]
+        }
+      },
+      "result": { "isAudited": true, "isAllowed": false, "policyId": -1 }
+    },
+
+    { "name": "ALLOW 'write s3v/iceberg/key2;' for user=svc-etl; 
action=PutObjectTagging matches wildcard 'Put*'",
+      "request": {
+        "resource": { "elements": { "volume": "s3v", "bucket": "iceberg", 
"key": "key2" } }, "user": "svc-etl", "accessType": "write", "action": 
"PutObjectTagging",
+        "inlinePolicy": { "grantor":  "r:data-all-access", "mode": "INLINE",
+          "grants": [
+            { "principals": [ "u:svc-etl" ], "resources": [ 
"key:s3v/iceberg/key2" ], "permissions": [ "all" ], "actions": [ "Put*" ] }
+          ]
+        }
+      },
+      "result": { "isAudited": true, "isAllowed": true, "policyId": -1 }
+    },
+    { "name": "DENY 'read s3v/iceberg/key2;' for user=svc-etl; 
action=GetObject does NOT match wildcard 'Put*'",
+      "request": {
+        "resource": { "elements": { "volume": "s3v", "bucket": "iceberg", 
"key": "key2" } }, "user": "svc-etl", "accessType": "read", "action": 
"GetObject",
+        "inlinePolicy": { "grantor":  "r:data-all-access", "mode": "INLINE",
+          "grants": [
+            { "principals": [ "u:svc-etl" ], "resources": [ 
"key:s3v/iceberg/key2" ], "permissions": [ "all" ], "actions": [ "Put*" ] }
+          ]
+        }
+      },
+      "result": { "isAudited": true, "isAllowed": false, "policyId": -1 }
+    },
+    { "name": "ALLOW 'delete s3v/iceberg/key2;' for user=svc-etl; 
action=DeleteObject matches universal wildcard '*'",
+      "request": {
+        "resource": { "elements": { "volume": "s3v", "bucket": "iceberg", 
"key": "key2" } }, "user": "svc-etl", "accessType": "delete", "action": 
"DeleteObject",
+        "inlinePolicy": { "grantor":  "r:data-all-access", "mode": "INLINE",
+          "grants": [
+            { "principals": [ "u:svc-etl" ], "resources": [ 
"key:s3v/iceberg/key2" ], "permissions": [ "all" ], "actions": [ "*" ] }
+          ]
+        }
+      },
+      "result": { "isAudited": true, "isAllowed": true, "policyId": -1 }
+    },
+    { "name": "DENY 'write s3v/iceberg/key2;' for user=svc-etl; no action in 
request does not satisfy action restriction",
+      "request": {
+        "resource": { "elements": { "volume": "s3v", "bucket": "iceberg", 
"key": "key2" } }, "user": "svc-etl", "accessType": "write",
+        "inlinePolicy": { "grantor":  "r:data-all-access", "mode": "INLINE",
+          "grants": [
+            { "principals": [ "u:svc-etl" ], "resources": [ 
"key:s3v/iceberg/key2" ], "permissions": [ "all" ], "actions": [ "PutObject" ] }
+          ]
+        }
+      },
+      "result": { "isAudited": true, "isAllowed": false, "policyId": -1 }
+    },
+    { "name": "ALLOW 'read s3v/iceberg/key2;' for user=svc-etl; 
action=GetObject matches second entry in multi-action grant",
+      "request": {
+        "resource": { "elements": { "volume": "s3v", "bucket": "iceberg", 
"key": "key2" } }, "user": "svc-etl", "accessType": "read", "action": 
"GetObject",
+        "inlinePolicy": { "grantor":  "r:data-all-access", "mode": "INLINE",
+          "grants": [
+            { "principals": [ "u:svc-etl" ], "resources": [ 
"key:s3v/iceberg/key2" ], "permissions": [ "all" ], "actions": [ "PutObject", 
"GetObject" ] }
+          ]
+        }
+      },
+      "result": { "isAudited": true, "isAllowed": true, "policyId": -1 }
+    },
+    { "name": "DENY 'read s3v/iceberg/key2;' for user=svc-etl; permission 
matches but action=ListBucket not in grant actions [Put*, Get*]",
+      "request": {
+        "resource": { "elements": { "volume": "s3v", "bucket": "iceberg", 
"key": "key2" } }, "user": "svc-etl", "accessType": "read", "action": 
"ListBucket",
+        "inlinePolicy": { "grantor":  "r:data-all-access", "mode": "INLINE",
+          "grants": [
+            { "principals": [ "u:svc-etl" ], "resources": [ 
"key:s3v/iceberg/key2" ], "permissions": [ "all" ], "actions": [ "Put*", "Get*" 
] }
+          ]
+        }
+      },
+      "result": { "isAudited": true, "isAllowed": false, "policyId": -1 }
+    },
+    { "name": "ALLOW 'write s3v/iceberg/key2;' for user=svc-etl; no actions 
field in grant means all actions allowed",
+      "request": {
+        "resource": { "elements": { "volume": "s3v", "bucket": "iceberg", 
"key": "key2" } }, "user": "svc-etl", "accessType": "write", "action": 
"PutObject",
+        "inlinePolicy": { "grantor":  "r:data-all-access", "mode": "INLINE",
+          "grants": [
+            { "principals": [ "u:svc-etl" ], "resources": [ 
"key:s3v/iceberg/key2" ], "permissions": [ "all" ] }
+          ]
+        }
+      },
+      "result": { "isAudited": true, "isAllowed": true, "policyId": -1 }
     }
   ]
 }
diff --git a/dev-support/checkstyle-suppressions.xml 
b/dev-support/checkstyle-suppressions.xml
index 8fa908165..59d1a77af 100644
--- a/dev-support/checkstyle-suppressions.xml
+++ b/dev-support/checkstyle-suppressions.xml
@@ -22,4 +22,5 @@
 <suppressions>
   <suppress files="[\\/]generated-sources[\\/]" checks="[a-zA-Z0-9]*"/>
   <suppress files="[\\/]surefire-reports[\\/]" checks="[a-zA-Z0-9]*"/>
+  <suppress files="PatchForOzoneServiceDefPolicyConditionUpdate_J10065" 
checks="TypeName"/>
 </suppressions>
diff --git 
a/plugin-ozone/src/main/java/org/apache/ranger/authorization/ozone/authorizer/RangerOzoneAuthorizer.java
 
b/plugin-ozone/src/main/java/org/apache/ranger/authorization/ozone/authorizer/RangerOzoneAuthorizer.java
index 03ac8d67a..44159ce92 100644
--- 
a/plugin-ozone/src/main/java/org/apache/ranger/authorization/ozone/authorizer/RangerOzoneAuthorizer.java
+++ 
b/plugin-ozone/src/main/java/org/apache/ranger/authorization/ozone/authorizer/RangerOzoneAuthorizer.java
@@ -150,7 +150,6 @@ public boolean checkAccess(IOzoneObj ozoneObject, 
RequestContext context) {
                        LOG.error("Unsupported access type. operation=" + 
operation + ", resource=" + resource);
                        return returnValue;
                }
-               String action = accessType;
                String clusterName = plugin.getClusterName();
 
                RangerAccessRequestImpl rangerRequest = new 
RangerAccessRequestImpl();
@@ -165,7 +164,7 @@ public boolean checkAccess(IOzoneObj ozoneObject, 
RequestContext context) {
 
                rangerRequest.setResource(rangerResource);
                rangerRequest.setAccessType(accessType);
-               rangerRequest.setAction(action);
+               rangerRequest.setAction(context.getS3Action());
                rangerRequest.setRequestData(resource);
                rangerRequest.setClusterName(clusterName);
 
@@ -303,6 +302,11 @@ private static RangerInlinePolicy.Grant 
toRangerGrant(OzoneGrant ozoneGrant, Ran
             
ret.setPermissions(ozoneGrant.getPermissions().stream().map(RangerOzoneAuthorizer::toRangerPermission).filter(Objects::nonNull).collect(Collectors.toSet()));
         }
 
+        final Set<String> s3Actions = ozoneGrant.getS3Actions();
+        if (s3Actions != null && !s3Actions.isEmpty()) {
+            ret.setActions(s3Actions);
+        }
+
         LOG.debug("toRangerGrant(ozoneGrant={}): ret={}", ozoneGrant, ret);
 
         return ret;
diff --git 
a/plugin-ozone/src/test/java/org/apache/ranger/authorization/ozone/authorizer/TestRangerOzoneAuthorizer.java
 
b/plugin-ozone/src/test/java/org/apache/ranger/authorization/ozone/authorizer/TestRangerOzoneAuthorizer.java
index 1f365dd61..a715e9c6f 100644
--- 
a/plugin-ozone/src/test/java/org/apache/ranger/authorization/ozone/authorizer/TestRangerOzoneAuthorizer.java
+++ 
b/plugin-ozone/src/test/java/org/apache/ranger/authorization/ozone/authorizer/TestRangerOzoneAuthorizer.java
@@ -68,6 +68,7 @@ public class TestRangerOzoneAuthorizer {
     private final OzoneObj   vol2  = new 
OzoneObjInfo.Builder().setResType(OzoneObj.ResourceType.VOLUME).setStoreType(OzoneObj.StoreType.OZONE).setVolumeName("vol2").build();
     private final OzoneGrant grantList = new OzoneGrant(new 
HashSet<>(Arrays.asList(vol1, buck1)), 
Collections.singleton(IAccessAuthorizer.ACLType.LIST));
     private final OzoneGrant grantRead = new 
OzoneGrant(Collections.singleton(key1), 
Collections.singleton(IAccessAuthorizer.ACLType.READ));
+    private final OzoneGrant grantWriteWithS3Actions = new 
OzoneGrant(Collections.singleton(key1), 
Collections.singleton(IAccessAuthorizer.ACLType.WRITE), new 
HashSet<>(Arrays.asList("AbortMultipartUpload", "PutObjectTagging")));
 
     private final RequestContext.Builder reqCtxBuilder = 
RequestContext.newBuilder()
             .setHost(hostname)
@@ -201,4 +202,75 @@ public void testAssumeRoleWithGrants() throws Exception {
         assertTrue(ozoneAuthorizer.checkAccess(buck1, 
ctxListWithSessionPolicy), "session-policy should allow list on bucket 
vol1/buck1");
         assertTrue(ozoneAuthorizer.checkAccess(key1, 
ctxReadWithSessionPolicy), "session-policy should allow read on key 
vol1/buck1/key1");
     }
+
+    @Test
+    public void testAssumeRoleWithS3ActionsInGrant() throws Exception {
+        // Verify that S3 actions from OzoneGrant are propagated to 
RangerInlinePolicy.Grant
+        // Note - these tests use policy 103 in om_dev_ozone.json
+        Set<OzoneGrant>   grants  = 
Collections.singleton(grantWriteWithS3Actions);
+        AssumeRoleRequest request = new AssumeRoleRequest(hostname, ipAddress, 
user1, role1, grants);
+
+        String sessionPolicy = 
ozoneAuthorizer.generateAssumeRoleSessionPolicy(request);
+
+        assertNotNull(sessionPolicy);
+
+        RangerInlinePolicy inlinePolicy = JsonUtilsV2.jsonToObj(sessionPolicy, 
RangerInlinePolicy.class);
+
+        assertNotNull(inlinePolicy.getGrants());
+        assertEquals(1, inlinePolicy.getGrants().size());
+
+        RangerInlinePolicy.Grant grant = 
inlinePolicy.getGrants().iterator().next();
+
+        // Verify S3 actions are set on the grant
+        assertNotNull(grant.getActions(), "S3 actions should be propagated to 
the inline policy grant");
+        assertEquals(new HashSet<>(Arrays.asList("AbortMultipartUpload", 
"PutObjectTagging")), grant.getActions());
+        assertEquals(Collections.singleton("write"), grant.getPermissions());
+        assertTrue(grant.getResources().contains("key:vol1/buck1/key1"));
+    }
+
+    @Test
+    public void testCheckAccessWithS3Action() throws Exception {
+        // Verify that checkAccess passes the S3 action from RequestContext to 
the Ranger request,
+        // allowing action-based inline policy evaluation
+        // Note - these tests use policy 103 in om_dev_ozone.json
+        Set<OzoneGrant>   grants  = 
Collections.singleton(grantWriteWithS3Actions);
+        AssumeRoleRequest request = new AssumeRoleRequest(hostname, ipAddress, 
user1, role1, grants);
+
+        String sessionPolicy = 
ozoneAuthorizer.generateAssumeRoleSessionPolicy(request);
+
+        assertNotNull(sessionPolicy);
+
+        // AbortMultipartUpload should be allowed - it matches the grant's S3 
actions
+        RequestContext ctxWriteAbortMultipartUpload = reqCtxBuilder
+                .setAclRights(IAccessAuthorizer.ACLType.WRITE)
+                .setRecursiveAccessCheck(false)
+                .setSessionPolicy(sessionPolicy)
+                .setS3Action("AbortMultipartUpload")
+                .build();
+
+        assertTrue(ozoneAuthorizer.checkAccess(key1, 
ctxWriteAbortMultipartUpload),
+                "session-policy should allow write on key1 when S3 action is 
AbortMultipartUpload");
+
+        // GetObject should be denied - it doesn't match the grant's S3 
actions [AbortMultipartUpload, PutObjectTagging]
+        RequestContext ctxWriteGetObject = reqCtxBuilder
+                .setAclRights(IAccessAuthorizer.ACLType.WRITE)
+                .setRecursiveAccessCheck(false)
+                .setSessionPolicy(sessionPolicy)
+                .setS3Action("GetObject")
+                .build();
+
+        assertFalse(ozoneAuthorizer.checkAccess(key1, ctxWriteGetObject),
+                "session-policy should deny write on key1 when S3 action is 
GetObject (not in grant)");
+
+        // No S3 action specified should be denied when the grant has 
S3-action restrictions
+        RequestContext ctxWriteNoAction = reqCtxBuilder
+                .setAclRights(IAccessAuthorizer.ACLType.WRITE)
+                .setRecursiveAccessCheck(false)
+                .setSessionPolicy(sessionPolicy)
+                .setS3Action(null)
+                .build();
+
+        assertFalse(ozoneAuthorizer.checkAccess(key1, ctxWriteNoAction),
+                "session-policy should deny write on key1 when no S3 action is 
specified and grant has S3 actions");
+    }
 }
diff --git a/plugin-ozone/src/test/resources/om_dev_ozone.json 
b/plugin-ozone/src/test/resources/om_dev_ozone.json
index e8d761801..d131df82c 100644
--- a/plugin-ozone/src/test/resources/om_dev_ozone.json
+++ b/plugin-ozone/src/test/resources/om_dev_ozone.json
@@ -41,6 +41,12 @@
       "policyItems":[
         { "accesses": [ { "type": "read" } ], "roles": [ "role1" ] }
       ]
+    },
+    { "id": 103, "name": "key: vol1/buck1/key1 - write", "isEnabled": true, 
"isAuditEnabled": true,
+      "resources": { "volume": { "values": [ "vol1" ] }, "bucket": { "values": 
[ "buck1" ] }, "key": { "values": [ "key1" ] } },
+      "policyItems":[
+        { "accesses": [ { "type": "write" } ], "roles": [ "role1" ] }
+      ]
     }
   ]
 }
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index d380e460a..bc91e77bb 100755
--- a/pom.xml
+++ b/pom.xml
@@ -175,7 +175,7 @@
         <org.bouncycastle.bcpkix-jdk18on>1.84</org.bouncycastle.bcpkix-jdk18on>
         <org.bouncycastle.bcprov-jdk18on>1.84</org.bouncycastle.bcprov-jdk18on>
         
<owasp-java-html-sanitizer.version>20211018.2</owasp-java-html-sanitizer.version>
-        <ozone.version>2.1.0</ozone.version>
+        <ozone.version>2.1.1</ozone.version>
         <paranamer.version>2.3</paranamer.version>
         <poi.version>5.2.2</poi.version>
         <!-- presto plugin deps -->
diff --git 
a/security-admin/src/main/java/org/apache/ranger/patch/PatchForOzoneServiceDefPolicyConditionUpdate_J10065.java
 
b/security-admin/src/main/java/org/apache/ranger/patch/PatchForOzoneServiceDefPolicyConditionUpdate_J10065.java
new file mode 100644
index 000000000..62e9846a0
--- /dev/null
+++ 
b/security-admin/src/main/java/org/apache/ranger/patch/PatchForOzoneServiceDefPolicyConditionUpdate_J10065.java
@@ -0,0 +1,167 @@
+/*
+ * 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.
+ */
+
+package org.apache.ranger.patch;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.biz.ServiceDBStore;
+import org.apache.ranger.common.JSONUtil;
+import org.apache.ranger.common.RangerValidatorFactory;
+import org.apache.ranger.db.RangerDaoManager;
+import org.apache.ranger.entity.XXServiceDef;
+import org.apache.ranger.plugin.model.RangerServiceDef;
+import org.apache.ranger.plugin.model.validation.RangerServiceDefValidator;
+import org.apache.ranger.plugin.model.validation.RangerValidator.Action;
+import org.apache.ranger.plugin.store.EmbeddedServiceDefsUtil;
+import org.apache.ranger.util.CLIUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class PatchForOzoneServiceDefPolicyConditionUpdate_J10065 extends 
BaseLoader {
+    private static final Logger logger = 
LoggerFactory.getLogger(PatchForOzoneServiceDefPolicyConditionUpdate_J10065.class);
+
+    @Autowired
+    RangerDaoManager daoMgr;
+
+    @Autowired
+    ServiceDBStore svcDBStore;
+
+    @Autowired
+    JSONUtil jsonUtil;
+
+    @Autowired
+    RangerValidatorFactory validatorFactory;
+
+    @Autowired
+    ServiceDBStore svcStore;
+
+    public static void main(String[] args) {
+        logger.info("main()");
+        try {
+            PatchForOzoneServiceDefPolicyConditionUpdate_J10065 loader = 
(PatchForOzoneServiceDefPolicyConditionUpdate_J10065) 
CLIUtil.getBean(PatchForOzoneServiceDefPolicyConditionUpdate_J10065.class);
+            loader.init();
+            while (loader.isMoreToProcess()) {
+                loader.load();
+            }
+            logger.info("Load complete. Exiting!!!");
+            System.exit(0);
+        } catch (Exception e) {
+            logger.error("Error loading", e);
+            System.exit(1);
+        }
+    }
+
+    @Override
+    public void init() throws Exception {
+        // Do Nothing
+    }
+
+    @Override
+    public void printStats() {
+        logger.info("PatchForOzoneServiceDefPolicyConditionUpdate_J10065");
+    }
+
+    @Override
+    public void execLoad() {
+        logger.info("==> 
PatchForOzoneServiceDefPolicyConditionUpdate_J10065.execLoad()");
+        try {
+            updateOzoneServiceDef();
+        } catch (Exception e) {
+            logger.error("Error while applying 
PatchForOzoneServiceDefPolicyConditionUpdate_J10065", e);
+            throw new 
RuntimeException("PatchForOzoneServiceDefPolicyConditionUpdate_J10065 failed", 
e);
+        }
+        logger.info("<== 
PatchForOzoneServiceDefPolicyConditionUpdate_J10065.execLoad()");
+    }
+
+    private void updateOzoneServiceDef() {
+        try {
+            final String ozoneServiceDefName = 
EmbeddedServiceDefsUtil.EMBEDDED_SERVICEDEF_OZONE_NAME;
+            final RangerServiceDef embeddedOzoneServiceDef = 
EmbeddedServiceDefsUtil.instance().getEmbeddedServiceDef(ozoneServiceDefName);
+
+            if (embeddedOzoneServiceDef == null) {
+                logger.error("Embedded service-def for {} not found", 
ozoneServiceDefName);
+                return;
+            }
+
+            final List<RangerServiceDef.RangerPolicyConditionDef> 
embeddedPolicyConditions = embeddedOzoneServiceDef.getPolicyConditions();
+
+            if (embeddedPolicyConditions == null) {
+                logger.error("Policy conditions are empty in embedded {} 
service-def", ozoneServiceDefName);
+                return;
+            }
+
+            XXServiceDef xXServiceDefObj = 
daoMgr.getXXServiceDef().findByName(ozoneServiceDefName);
+
+            if (xXServiceDefObj == null) {
+                logger.error("Service def for {} is not found in DB", 
ozoneServiceDefName);
+                return;
+            }
+
+            Map<String, String> serviceDefOptionsPreUpdate = null;
+            final String        jsonStrPreUpdate           = 
xXServiceDefObj.getDefOptions();
+
+            if (StringUtils.isNotEmpty(jsonStrPreUpdate)) {
+                serviceDefOptionsPreUpdate = 
jsonUtil.jsonToMap(jsonStrPreUpdate);
+            }
+
+            final RangerServiceDef dbOzoneServiceDef = 
svcDBStore.getServiceDefByName(ozoneServiceDefName);
+
+            if (dbOzoneServiceDef == null) {
+                logger.error("Service def for {} is not found in 
ServiceDBStore", ozoneServiceDefName);
+                return;
+            }
+
+            dbOzoneServiceDef.setPolicyConditions(embeddedPolicyConditions);
+
+            final RangerServiceDefValidator validator = 
validatorFactory.getServiceDefValidator(svcStore);
+
+            validator.validate(dbOzoneServiceDef, Action.UPDATE);
+
+            svcStore.updateServiceDef(dbOzoneServiceDef);
+
+            xXServiceDefObj = 
daoMgr.getXXServiceDef().findByName(ozoneServiceDefName);
+
+            if (xXServiceDefObj != null) {
+                final String              jsonStrPostUpdate           = 
xXServiceDefObj.getDefOptions();
+                Map<String, String>       serviceDefOptionsPostUpdate = null;
+
+                if (StringUtils.isNotEmpty(jsonStrPostUpdate)) {
+                    serviceDefOptionsPostUpdate = 
jsonUtil.jsonToMap(jsonStrPostUpdate);
+                }
+
+                if (serviceDefOptionsPostUpdate != null && 
serviceDefOptionsPostUpdate.containsKey(RangerServiceDef.OPTION_ENABLE_DENY_AND_EXCEPTIONS_IN_POLICIES))
 {
+                    if (serviceDefOptionsPreUpdate == null || 
!serviceDefOptionsPreUpdate.containsKey(RangerServiceDef.OPTION_ENABLE_DENY_AND_EXCEPTIONS_IN_POLICIES))
 {
+                        
serviceDefOptionsPostUpdate.remove(RangerServiceDef.OPTION_ENABLE_DENY_AND_EXCEPTIONS_IN_POLICIES);
+
+                        
xXServiceDefObj.setDefOptions(jsonUtil.readMapToString(serviceDefOptionsPostUpdate));
+
+                        daoMgr.getXXServiceDef().update(xXServiceDefObj);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            logger.error("Error while updating ozone service-def policy 
conditions", e);
+            throw new RuntimeException("Failed to update ozone service-def 
policy conditions", e);
+        }
+    }
+}
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 15b8569f8..7b7319838 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
@@ -492,6 +492,10 @@ export const scrollToError = (selector) => {
 };
 
 export const selectInputCustomStyles = {
+  multiValue: (base) => ({
+    ...base,
+    maxWidth: "100%"
+  }),
   option: (base) => ({
     ...base,
     textOverflow: "unset",
@@ -512,6 +516,28 @@ export const selectInputCustomStyles = {
   })
 };
 
+/** Multi-value select styles that wrap chips (e.g. action-matches in 
permission rows). */
+export const selectInputWrappingCustomStyles = {
+  ...selectInputCustomStyles,
+  control: (base) => ({
+    ...base,
+    flexWrap: "wrap"
+  }),
+  valueContainer: (base) => ({
+    ...base,
+    flexWrap: "wrap"
+  })
+};
+
+export const trimInputValue = (e, input) => {
+  const value = e.target.value;
+  const trimmed = value.trim();
+  if (value !== trimmed) {
+    input.onChange(trimmed);
+  }
+  input.onBlur(e);
+};
+
 export const selectInputCustomErrorStyles = {
   ...selectInputCustomStyles,
   control: () => {
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 34c8c3a87..8aabb9244 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
@@ -33,7 +33,15 @@ import CreatableSelect from "react-select/creatable";
 import Select from "react-select";
 import { InfoIcon } from "Utils/XAUtils";
 import { RegexMessage } from "Utils/XAMessages";
-import { selectInputCustomStyles } from "Components/CommonComponents";
+import { selectInputWrappingCustomStyles } from "Components/CommonComponents";
+import {
+  sortPolicyConditions,
+  buildActionReqsMapFromConditionDef,
+  isPerRowCondition,
+  getAllowedActionMatchesForCondition,
+  getCleanConditions,
+  parseConditionUiHint
+} from "Utils/policyConditionUtils";
 
 const esprima = require("esprima");
 const TYPE_SELECT = "select";
@@ -42,6 +50,28 @@ const TYPE_INPUT = "input";
 const TYPE_RADIO = "radio";
 const TYPE_CUSTOM = "custom";
 
+/**
+ * Default react-select props for condition fields rendered inside the 
Bootstrap
+ * OverlayTrigger popover (e.g. Action / action-matches on a permission row).
+ * Portaling the menu to document.body with fixed positioning prevents a long
+ * option list from resizing the popover or scrolling the policy form when the
+ * menu opens. menuShouldScrollIntoView is disabled for the same reason.
+ * Callers may pass selectProps to merge or override (PolicyPermissionItem 
does).
+ */
+const CONDITION_POPOVER_SELECT_PROPS = {
+  menuPortalTarget:
+    typeof document !== "undefined" ? document.body : undefined,
+  menuPosition: "fixed",
+  menuPlacement: "auto",
+  menuShouldScrollIntoView: false
+};
+
+/** Above .table-editable popover overlay (z-index 1060 in style.css). */
+const conditionPopoverSelectStyles = {
+  ...selectInputWrappingCustomStyles,
+  menuPortal: (base) => ({ ...base, zIndex: 1061 })
+};
+
 const CheckboxComp = (props) => {
   const { options, value = [], valRef, showSelectAll, selectAllLabel } = props;
   const [selectedVal, setVal] = useState(value);
@@ -148,9 +178,20 @@ const InputBoxComp = (props) => {
   );
 };
 
-const CustomCondition = (props) => {
-  const { value, valRef, conditionDefVal, selectProps, validExpression } =
-    props;
+/**
+ * One policy condition field inside the per-row popover (ip-range, 
_expression, or action-matches).
+ * uiHint shape selects control: singleValue | isMultiline | isMultiValue 
(fixed or creatable select).
+ */
+const ConditionRow = ({
+  m,
+  value,
+  valRef,
+  selectProps,
+  validExpression,
+  servicedefName,
+  actionFilterContext,
+  actionReqsMap
+}) => {
   const tagAccessData = (val, key) => {
     if (!isObject(valRef.current)) {
       valRef.current = {};
@@ -158,158 +199,239 @@ const CustomCondition = (props) => {
     valRef.current[key] = val;
   };
 
-  return (
-    <>
-      {conditionDefVal?.length > 0 &&
-        conditionDefVal.map((m) => {
-          let uiHintAttb =
-            m.uiHint != undefined && m.uiHint != "" ? JSON.parse(m.uiHint) : 
"";
-          if (uiHintAttb != "") {
-            if (uiHintAttb?.singleValue) {
-              const [selectedCondVal, setCondSelect] = useState(
-                value?.[m.name] || value
-              );
-              const accessedOpt = [
-                { value: "yes", label: "Yes" },
-                { value: "no", label: "No" }
-              ];
-              const accessedVal = (val) => {
-                let value = null;
-                if (val) {
-                  let opObj = accessedOpt?.filter((m) => {
-                    if (m.value == (val[0]?.value || val)) {
-                      return m;
-                    }
-                  });
-                  if (opObj) {
-                    value = opObj;
-                  }
-                }
-                return value;
-              };
-              const selectHandleChange = (e, name) => {
-                let filterVal = accessedOpt?.filter((m) => {
-                  if (m.value != e[0]?.value) {
-                    return m;
-                  }
-                });
-                setCondSelect(
-                  !isEmpty(e) ? (e?.length > 1 ? filterVal : e) : null
-                );
-                tagAccessData(
-                  !isEmpty(e)
-                    ? e?.length > 1
-                      ? filterVal[0].value
-                      : e[0].value
-                    : null,
-                  name
-                );
-              };
-              return (
-                <div key={m.name}>
-                  <Form.Group className="mb-3">
-                    <b>{m.label}:</b>
-                    <Select
-                      options={accessedOpt}
-                      onChange={(e) => selectHandleChange(e, m.name)}
-                      value={
-                        selectedCondVal?.value
-                          ? accessedVal(selectedCondVal.value)
-                          : accessedVal(selectedCondVal)
-                      }
-                      isMulti={true}
-                      isClearable={false}
-                    />
-                  </Form.Group>
-                </div>
-              );
+  const uiHintAttb = parseConditionUiHint(m.uiHint);
+  const conditionValue = value?.[m.name];
+
+  const [selectedCondVal, setCondSelect] = useState(conditionValue);
+  const [selectedJSCondVal, setJSCondVal] = useState(
+    typeof conditionValue === "string" ? conditionValue : ""
+  );
+  const [selectedInputVal, setSelectVal] = useState(
+    Array.isArray(conditionValue) ? conditionValue : []
+  );
+
+  useEffect(() => {
+    setCondSelect(conditionValue);
+    setJSCondVal(typeof conditionValue === "string" ? conditionValue : "");
+    setSelectVal(Array.isArray(conditionValue) ? conditionValue : []);
+  }, [conditionValue]);
+
+  if (!uiHintAttb) return null;
+
+  if (uiHintAttb?.singleValue) {
+    const accessedOpt = [
+      { value: "yes", label: "Yes" },
+      { value: "no", label: "No" }
+    ];
+    const accessedVal = (val) => {
+      let value = null;
+      if (val) {
+        let opObj = accessedOpt?.filter((mOp) => {
+          if (mOp.value == (val[0]?.value || val)) {
+            return mOp;
+          }
+        });
+        if (opObj) {
+          value = opObj;
+        }
+      }
+      return value;
+    };
+    const selectHandleChange = (e, name) => {
+      let filterVal = accessedOpt?.filter((mOp) => {
+        if (mOp.value != e[0]?.value) {
+          return mOp;
+        }
+      });
+      setCondSelect(
+        !isEmpty(e) ? (e?.length > 1 ? filterVal : e) : null
+      );
+      tagAccessData(
+        !isEmpty(e)
+          ? e?.length > 1
+            ? filterVal[0].value
+            : e[0].value
+          : null,
+        name
+      );
+    };
+    return (
+      <div key={m.name}>
+        <Form.Group className="mb-3">
+          <b>{m.label}:</b>
+          <Select
+            options={accessedOpt}
+            onChange={(e) => selectHandleChange(e, m.name)}
+            value={
+              selectedCondVal?.value
+                ? accessedVal(selectedCondVal.value)
+                : accessedVal(selectedCondVal)
             }
-            if (uiHintAttb?.isMultiline) {
-              const [selectedJSCondVal, setJSCondVal] = useState(
-                value?.[m.name] || value
-              );
-              const expressionVal = (val) => {
-                let value = null;
-                if (val != "" && typeof val != "object") {
-                  valRef.current[m.name] = val;
-                  return (value = val);
+            isMulti={true}
+            isClearable={false}
+          />
+        </Form.Group>
+      </div>
+    );
+  }
+
+  if (uiHintAttb?.isMultiline) {
+    const expressionVal = (val) => {
+      let value = null;
+      if (val != "" && typeof val != "object") {
+        valRef.current[m.name] = val.trim();
+        return (value = val);
+      }
+      return value !== null ? value : "";
+    };
+    const textAreaHandleChange = (e, name) => {
+      setJSCondVal(e.target.value);
+      tagAccessData(e.target.value, name);
+    };
+    return (
+      <div key={m.name}>
+        <Form.Group className="mb-3">
+          <Row>
+            <Col>
+              <b>{m.label}:</b>
+              <InfoIcon
+                position="right"
+                message={
+                  <p className="pd-10">
+                    {RegexMessage.MESSAGE.policyConditionInfoIcon}
+                  </p>
                 }
-                return value !== null ? value : "";
-              };
-              const textAreaHandleChange = (e, name) => {
-                setJSCondVal(e.target.value);
-                tagAccessData(e.target.value, name);
-              };
-              return (
-                <div key={m.name}>
-                  <Form.Group className="mb-3">
-                    <Row>
-                      <Col>
-                        <b>{m.label}:</b>
-                        <InfoIcon
-                          position="right"
-                          message={
-                            <p className="pd-10">
-                              {RegexMessage.MESSAGE.policyConditionInfoIcon}
-                            </p>
-                          }
-                        />
-                      </Col>
-                    </Row>
-                    <Row>
-                      <Col>
-                        <Form.Control
-                          as="textarea"
-                          rows={3}
-                          key={m.name}
-                          value={expressionVal(selectedJSCondVal)}
-                          onChange={(e) => textAreaHandleChange(e, m.name)}
-                          isInvalid={validExpression.state}
-                        />
-                        {validExpression.state && (
-                          <div className="text-danger">
-                            {validExpression.errorMSG}
-                          </div>
-                        )}
-                      </Col>
-                    </Row>
-                  </Form.Group>
-                </div>
-              );
-            }
-            if (uiHintAttb?.isMultiValue) {
-              const [selectedInputVal, setSelectVal] = useState(
-                value?.[m.name] || []
-              );
-              const handleChange = (e, name) => {
-                setSelectVal(e);
-                tagAccessData(e, name);
-              };
-              return (
-                <div key={m.name}>
-                  <Form.Group
-                    className="mb-3"
-                    controlId="Ip-range"
-                    key={m.name}
-                  >
-                    <b>{m.label}:</b>
-                    <CreatableSelect
-                      {...selectProps}
-                      defaultValue={
-                        selectedInputVal == "" ? null : selectedInputVal
-                      }
-                      onChange={(e) => handleChange(e, m.name)}
-                      placeholder=""
-                      width="500px"
-                      isClearable={false}
-                      styles={selectInputCustomStyles}
-                    />
-                  </Form.Group>
+              />
+            </Col>
+          </Row>
+          <Row>
+            <Col>
+              <Form.Control
+                as="textarea"
+                rows={3}
+                key={m.name}
+                value={expressionVal(selectedJSCondVal)}
+                onChange={(e) => textAreaHandleChange(e, m.name)}
+                onBlur={(e) => {
+                  textAreaHandleChange(
+                    { target: { value: e.target.value.trim() } },
+                    m.name
+                  );
+                }}
+                isInvalid={validExpression.state}
+              />
+              {validExpression.state && (
+                <div className="text-danger">
+                  {validExpression.errorMSG}
                 </div>
-              );
-            }
-          }
-        })}
+              )}
+            </Col>
+          </Row>
+        </Form.Group>
+      </div>
+    );
+  }
+
+  if (uiHintAttb?.isMultiValue) {
+    const { dropdownOptions, prunedSelection: displayedValue } =
+      getAllowedActionMatchesForCondition({
+        conditionName: m.name,
+        actionFilterContext,
+        actionReqsMap,
+        servicedefName,
+        uiHintAttb,
+        currentSelection: selectedInputVal
+      });
+
+    const handleChange = (e, name) => {
+      setSelectVal(e);
+      tagAccessData(e, name);
+    };
+    return (
+      <div key={m.name}>
+        <Form.Group className="mb-3" controlId={m.name} key={m.name}>
+          <b>{m.label}:</b>
+          {dropdownOptions ? (
+            <Select
+              {...CONDITION_POPOVER_SELECT_PROPS}
+              {...selectProps}
+              value={displayedValue || null}
+              onChange={(e) => handleChange(e, m.name)}
+              options={dropdownOptions}
+              placeholder=""
+              isClearable={false}
+              styles={conditionPopoverSelectStyles}
+            />
+          ) : (
+            <CreatableSelect
+              {...CONDITION_POPOVER_SELECT_PROPS}
+              {...selectProps}
+              value={displayedValue || null}
+              onChange={(e) => handleChange(e, m.name)}
+              placeholder=""
+              width="500px"
+              isClearable={false}
+              styles={conditionPopoverSelectStyles}
+              formatCreateLabel={(inputValue) =>
+                `Create "${inputValue.trim()}"`
+              }
+              onCreateOption={(inputValue) => {
+                const trimmedValue = inputValue.trim();
+                if (trimmedValue) {
+                  const newOption = {
+                    label: trimmedValue,
+                    value: trimmedValue
+                  };
+                  const currentValues = selectedInputVal || [];
+                  const newValues = Array.isArray(currentValues)
+                    ? [...currentValues, newOption]
+                    : [newOption];
+                  setSelectVal(newValues);
+                  tagAccessData(newValues, m.name);
+                }
+              }}
+            />
+          )}
+        </Form.Group>
+      </div>
+    );
+  }
+
+  return null;
+};
+
+/**
+ * Per permission-row conditions popover (TYPE_CUSTOM in PolicyPermissionItem).
+ * Renders all service-def conditions; action-matches is filtered on save at 
policy level only.
+ */
+const CustomCondition = (props) => {
+  const {
+    value,
+    valRef,
+    conditionDefVal,
+    selectProps,
+    validExpression,
+    servicedefName,
+    actionFilterContext,
+    actionReqsMap = {}
+  } = props;
+
+  return (
+    <>
+      {conditionDefVal?.length > 0 &&
+        sortPolicyConditions(conditionDefVal).map((m) => (
+          <ConditionRow
+            key={m.name}
+            m={m}
+            value={value}
+            valRef={valRef}
+            selectProps={selectProps}
+            validExpression={validExpression}
+            servicedefName={servicedefName}
+            actionFilterContext={actionFilterContext}
+            actionReqsMap={actionReqsMap}
+          />
+        ))}
     </>
   );
 };
@@ -361,7 +483,8 @@ const Editable = (props) => {
     onChange,
     options = [],
     conditionDefVal,
-    servicedefName
+    servicedefName,
+    actionFilterContext
   } = props;
 
   const initialLoad = useRef(true);
@@ -375,6 +498,11 @@ const Editable = (props) => {
   const { show, value, target } = state;
   let isListenerAttached = false;
 
+  const actionReqsMap = React.useMemo(
+    () => buildActionReqsMapFromConditionDef(conditionDefVal),
+    [conditionDefVal]
+  );
+
   const handleClickOutside = (e) => {
     if (
       document.getElementById("popover-basic")?.contains(e?.target) == false
@@ -401,7 +529,7 @@ const Editable = (props) => {
 
   const displayValue = () => {
     let val = "--";
-    const selectVal = value;
+    let selectVal = value;
     const policyConditionDisplayValue = () => {
       let ipRangVal, uiHintVal;
       if (selectVal) {
@@ -411,9 +539,10 @@ const Editable = (props) => {
               return m;
             }
           });
-          if (conditionObj?.uiHint && conditionObj?.uiHint != "") {
-            uiHintVal = JSON.parse(conditionObj.uiHint);
+          if (!conditionObj) {
+            return null;
           }
+          uiHintVal = parseConditionUiHint(conditionObj.uiHint);
           if (isArray(selectVal[property])) {
             ipRangVal = selectVal[property]
               ?.map(function (m) {
@@ -476,7 +605,6 @@ const Editable = (props) => {
                 variant="outline-dark"
                 size="sm"
                 type="button"
-                s
               >
                 <i className="fa-fw fa fa-plus"></i>
               </Button>
@@ -586,11 +714,7 @@ const Editable = (props) => {
             </div>
           );
       } else if (type === TYPE_CUSTOM) {
-        for (const key in selectVal) {
-          if (selectVal[key] == null || selectVal[key] == "") {
-            delete selectVal[key];
-          }
-        }
+        selectVal = getCleanConditions(selectVal);
         if (Object.keys(selectVal).length != 0) {
           val = (
             <h6>
@@ -647,14 +771,41 @@ const Editable = (props) => {
   const handleApply = () => {
     let errors, uiHintVal;
     if (selectValRef?.current) {
-      sortBy(Object.keys(selectValRef.current)).map((property) => {
+      // Re-validate action-matches against current row permissions before 
persisting.
+      sortBy(Object.keys(selectValRef.current)).forEach((property) => {
         let conditionObj = find(conditionDefVal, function (m) {
           if (m.name == property) {
             return m;
           }
         });
-        if (conditionObj != undefined && conditionObj?.uiHint != "") {
-          uiHintVal = JSON.parse(conditionObj.uiHint);
+        uiHintVal = parseConditionUiHint(conditionObj?.uiHint);
+        if (conditionObj != undefined && uiHintVal) {
+          if (
+            isPerRowCondition(conditionObj.name) &&
+            uiHintVal?.isMultiValue &&
+            Array.isArray(uiHintVal?.options) &&
+            actionFilterContext?.selectedAccessTypes?.length > 0
+          ) {
+            const current = selectValRef.current[conditionObj.name];
+            
+            if (Array.isArray(current)) {
+              const { prunedSelection } = getAllowedActionMatchesForCondition({
+                conditionName: conditionObj.name,
+                actionFilterContext,
+                actionReqsMap,
+                servicedefName,
+                uiHintAttb: uiHintVal,
+                currentSelection: current
+              });
+              
+              if (prunedSelection && prunedSelection.length > 0) {
+                selectValRef.current[conditionObj.name] = prunedSelection;
+              } else {
+                delete selectValRef.current[conditionObj.name];
+              }
+            }
+          }
+
           if (
             uiHintVal?.isMultiline &&
             selectValRef.current[conditionObj.name] != "" &&
@@ -722,6 +873,9 @@ const Editable = (props) => {
             conditionDefVal={props.conditionDefVal}
             selectProps={props.selectProps}
             validExpression={validExpression}
+            servicedefName={servicedefName}
+            actionFilterContext={actionFilterContext}
+            actionReqsMap={actionReqsMap}
           />
         ) : null}
       </Popover.Body>
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
new file mode 100644
index 000000000..6cb1cb0ed
--- /dev/null
+++ 
b/security-admin/src/main/webapp/react-webapp/src/hooks/usePruneStaleConditions.js
@@ -0,0 +1,153 @@
+/*
+ * 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.
+ */
+
+/**
+ * Keeps policyItems[].conditions["action-matches"] in sync when permissions or
+ * resources change. Mounted from PolicyPermissionItem; does not touch ip-range
+ * or other per-row conditions.
+ */
+
+import { useEffect } from "react";
+import { isArray, find, isEqual } from "lodash";
+import {
+  getSelectedAccessTypesForRow,
+  getAllowedActionMatchesForCondition,
+  isPerRowCondition,
+  parseConditionUiHint
+} from "Utils/policyConditionUtils";
+
+/**
+ * Stable dependency string: only permission accesses and per-row condition 
values.
+ * Avoids re-running when unrelated form fields (user/group names, etc.) 
change.
+ */
+const serializePruneDeps = (formValues, attrName) => {
+  const items = formValues?.[attrName];
+  if (!Array.isArray(items)) {
+    return "[]";
+  }
+  return JSON.stringify(
+    items.map((item, index) => ({
+      accesses: getSelectedAccessTypesForRow(formValues, attrName, index),
+      actionMatches: item?.conditions?.["action-matches"] ?? null
+    }))
+  );
+};
+
+export const usePruneStaleConditions = ({
+  formValues,
+  attrName,
+  form,
+  leafResourceTypes,
+  serviceCompDetails,
+  conditionDefVal,
+  actionReqsMap
+}) => {
+  const serializedPruneDeps = serializePruneDeps(formValues, attrName);
+
+  useEffect(() => {
+    const items = formValues?.[attrName];
+    if (!items || !isArray(items)) {
+      return;
+    }
+
+    let hasChanges = false;
+    const newItems = [...items];
+
+    newItems.forEach((item, index) => {
+      if (!item?.conditions) {
+        return;
+      }
+
+      const accesses = getSelectedAccessTypesForRow(formValues, attrName, 
index);
+      if (accesses.length === 0) {
+        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;
+        }
+        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;
+        }
+
+        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;
+          }
+        }
+      }
+
+      if (itemChanged) {
+        newItems[index] = { ...item, conditions: newConditions };
+      }
+    });
+
+    if (hasChanges) {
+      form.change(attrName, newItems);
+    }
+  }, [
+    serializedPruneDeps,
+    leafResourceTypes,
+    actionReqsMap,
+    attrName,
+    form,
+    conditionDefVal,
+    serviceCompDetails
+  ]);
+};
diff --git a/security-admin/src/main/webapp/react-webapp/src/styles/style.css 
b/security-admin/src/main/webapp/react-webapp/src/styles/style.css
index 57adcf354..0ec43d82c 100644
--- a/security-admin/src/main/webapp/react-webapp/src/styles/style.css
+++ b/security-admin/src/main/webapp/react-webapp/src/styles/style.css
@@ -1603,6 +1603,18 @@ header {
   text-align: center;
 }
 
+.table-responsive > .table.policy-permission-table > tbody > tr > td {
+  white-space: normal;
+}
+
+.policy-permission-table .badge,
+.policy-permission-table .editable-label {
+  white-space: normal;
+  overflow-wrap: anywhere;
+  word-break: break-word;
+  max-width: 100%;
+}
+
 /* Button toggle tag*/
 .switch.btn-xs {
   border-radius: 15px;
diff --git 
a/security-admin/src/main/webapp/react-webapp/src/utils/actionRequirements/ozone.json
 
b/security-admin/src/main/webapp/react-webapp/src/utils/actionRequirements/ozone.json
new file mode 100644
index 000000000..edc14a5d5
--- /dev/null
+++ 
b/security-admin/src/main/webapp/react-webapp/src/utils/actionRequirements/ozone.json
@@ -0,0 +1,47 @@
+{
+  "volume": {
+    "ListAllMyBuckets": ["read", "list"],
+    "CreateBucket": ["read"],
+    "DeleteBucket": ["read"],
+    "GetBucketAcl": ["read"],
+    "ListBucket": ["read"],
+    "ListBucketMultipartUploads": ["read"],
+    "PutBucketAcl": ["read"],
+    "AbortMultipartUpload": ["read"],
+    "DeleteObject": ["read"],
+    "DeleteObjectTagging": ["read"],
+    "GetObject": ["read"],
+    "GetObjectTagging": ["read"],
+    "ListMultipartUploadParts": ["read"],
+    "PutObject": ["read"],
+    "PutObjectTagging": ["read"]
+  },
+  "bucket": {
+    "ListBucket": ["read", "list"],
+    "ListBucketMultipartUploads": ["read", "list"],
+    "CreateBucket": ["create"],
+    "DeleteBucket": ["delete"],
+    "GetBucketAcl": ["read", "read_acl"],
+    "PutBucketAcl": ["read", "read_acl", "write_acl"],
+    "AbortMultipartUpload": ["read"],
+    "DeleteObject": ["read"],
+    "DeleteObjectTagging": ["read"],
+    "GetObject": ["read"],
+    "GetObjectTagging": ["read"],
+    "ListMultipartUploadParts": ["read"],
+    "PutObject": ["read"],
+    "PutObjectTagging": ["read"]
+  },
+  "key": {
+    "GetObject": ["read"],
+    "GetObjectTagging": ["read"],
+    "ListBucket": ["read"],
+    "PutObject": ["create", "write"],
+    "PutObjectTagging": ["write"],
+    "DeleteObjectTagging": ["write"],
+    "AbortMultipartUpload": ["write"],
+    "DeleteObject": ["delete"],
+    "ListMultipartUploadParts": ["read"]
+  },
+  "role": {}
+}
diff --git 
a/security-admin/src/main/webapp/react-webapp/src/utils/actionRequirements/registry.js
 
b/security-admin/src/main/webapp/react-webapp/src/utils/actionRequirements/registry.js
new file mode 100644
index 000000000..b0462fc4f
--- /dev/null
+++ 
b/security-admin/src/main/webapp/react-webapp/src/utils/actionRequirements/registry.js
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+/**
+ * Bundled action-to-permission maps referenced by service-def uiHint
+ * (actionRequirementsFile). Loaded once at build time; add new services here
+ * and a matching JSON file alongside ozone.json.
+ */
+
+import ozoneReqs from "./ozone.json";
+
+export const actionRequirementsRegistry = {
+  ozone: ozoneReqs
+};
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
new file mode 100644
index 000000000..e8c1c3c00
--- /dev/null
+++ 
b/security-admin/src/main/webapp/react-webapp/src/utils/policyConditionUtils.js
@@ -0,0 +1,722 @@
+/*
+ * 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.
+ */
+
+/**
+ * Policy condition helpers for the React policy form.
+ *
+ * UI data flow (Ozone action-matches example):
+ *
+ *   PolicyPermissionItem
+ *     ├─ getSelectedLeafResourceTypes()  → deepest resource type(s) in the 
form
+ *     ├─ buildActionReqsMapFromConditionDef() → loads ozone.json via registry
+ *     ├─ usePruneStaleConditions()       → drops invalid action-matches on 
change
+ *     └─ Editable (type=custom) per row
+ *           ├─ CustomCondition popover   → ip-range, _expression, 
action-matches
+ *           └─ getAllowedActionMatchesForCondition()
+ *                 └─ getActionMatchesOptions() → filter + sort dropdown
+ *
+ * Storage split:
+ *   - policy.conditions (header modal via PolicyConditionsComp): all defs 
EXCEPT action-matches
+ *   - policyItems[i].conditions (permission row popover): all defs including 
action-matches
+ *
+ * action-matches filtering uses uiHint.options (service def) plus 
actionRequirements
+ * (ozone.json keyed by volume | bucket | key) and the row's selected access 
types.
+ */
+
+import { actionRequirementsRegistry } from "./actionRequirements/registry";
+
+/**
+ * Sorts action options for the dropdown, placing wildcard patterns first
+ * for Ozone (e.g. *, Create*, Get*, etc.) followed by concrete actions
+ * alphabetically. For other services, sorts all options alphabetically.
+ *
+ * @param {string[]} options - Raw action option strings from the service def 
uiHint.
+ * @param {string} servicedefName - The service definition name (e.g. "ozone").
+ * @returns {string[]} Sorted and trimmed action options.
+ */
+const sortActionOptions = (options, servicedefName) => {
+  if (!Array.isArray(options)) {
+    return options;
+  }
+
+  const normalized = options
+    .map((v) => (typeof v === "string" ? v.trim() : ""))
+    .filter((v) => v.length > 0);
+
+  if (servicedefName === "ozone") {
+    // Ensure wildcard options are shown first for usability
+    const pinnedOrder = ["*", "Create*", "Delete*", "Get*", "List*", "Put*"];
+    const pinned = pinnedOrder.filter((v) => normalized.includes(v));
+    const pinnedSet = new Set(pinned);
+    const rest = normalized
+      .filter((v) => !pinnedSet.has(v))
+      .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
+
+    return [...pinned, ...rest];
+  }
+
+  return normalized.sort((a, b) =>
+    a.localeCompare(b, undefined, { sensitivity: "base" })
+  );
+};
+
+const NONE_RESOURCE_VALUE = "none";
+
+/** Trims and filters an array of strings, removing blanks. */
+const normalizeStringArray = (values) => {
+  if (!Array.isArray(values)) {
+    return [];
+  }
+
+  return values
+    .map((v) => (typeof v === "string" ? v.trim() : ""))
+    .filter((v) => v.length > 0);
+};
+
+/** Converts a string array to a Set of lowercased, trimmed values. */
+const toLowerSet = (values) => {
+  const set = new Set();
+  for (const v of normalizeStringArray(values)) {
+    set.add(v.toLowerCase());
+  }
+  return set;
+};
+
+/**
+ * Returns true if every entry in `required` is present in `grantedSet`.
+ * An empty or null `required` array is considered satisfied.
+ */
+const hasAll = (required, grantedSet) => {
+  if (!Array.isArray(required) || required.length === 0) {
+    return true;
+  }
+  for (const r of required) {
+    if (!grantedSet.has(r)) {
+      return false;
+    }
+  }
+  return true;
+};
+
+/**
+ * Expands a set of selected access types by following impliedGrants chains.
+ * For example, selecting "all" expands to include "read", "write", "create", 
etc.
+ * Uses a fixpoint loop to transitively resolve implied grants.
+ *
+ * @param {string[]} selectedAccessTypes - Currently selected access type 
names.
+ * @param {Array<{name: string, impliedGrants?: string[]}>} accessTypeDefs - 
Service def access types.
+ * @returns {string[]} Expanded set of access type names (lowercased).
+ */
+const expandImpliedAccessTypes = (selectedAccessTypes, accessTypeDefs) => {
+  const selected = toLowerSet(selectedAccessTypes);
+
+  if (!Array.isArray(accessTypeDefs) || accessTypeDefs.length === 0) {
+    return [...selected];
+  }
+
+  let changed = true;
+  while (changed) {
+    changed = false;
+    for (const def of accessTypeDefs) {
+      const name = def?.name;
+      const implied = def?.impliedGrants;
+      if (!name || !selected.has(String(name).toLowerCase())) {
+        continue;
+      }
+      if (!Array.isArray(implied) || implied.length === 0) {
+        continue;
+      }
+      for (const g of implied) {
+        const grant = typeof g === "string" ? g.trim().toLowerCase() : "";
+        if (grant && !selected.has(grant)) {
+          selected.add(grant);
+          changed = true;
+        }
+      }
+    }
+  }
+
+  return [...selected];
+};
+
+/**
+ * Extracts the currently selected access type names for a specific permission 
row
+ * in the policy form. Used to determine which action options should be 
available.
+ *
+ * @param {object} formValues - The form state object.
+ * @param {string} attrName - The field array attribute name (e.g. 
"policyItems").
+ * @param {number} index - The row index within the field array.
+ * @returns {string[]} Selected access type values for that row.
+ */
+export const getSelectedAccessTypesForRow = (formValues, attrName, index) => {
+  const accesses = formValues?.[attrName]?.[index]?.accesses;
+  if (!Array.isArray(accesses)) {
+    return [];
+  }
+  return accesses
+    .map((a) => (typeof a?.value === "string" ? a.value.trim() : ""))
+    .filter((v) => v.length > 0);
+};
+
+/**
+ * True when the condition is stored on policyItems[].conditions rather than
+ * policy.conditions. Today only "action-matches" uses that storage path.
+ *
+ * Used to exclude action-matches from the policy-level PolicyConditionsComp 
modal
+ * and to scope prune/apply logic. ip-range and _expression still render in the
+ * per-row Editable popover (CustomCondition) and are evaluated on the policy 
item.
+ */
+export const isPerRowCondition = (name) => name === "action-matches";
+
+/**
+ * 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 === "") {
+    return null;
+  }
+  try {
+    const parsed = JSON.parse(uiHint);
+    return parsed && typeof parsed === "object" ? parsed : null;
+  } catch (e) {
+    return null;
+  }
+};
+
+/** Desired render order for policy conditions in the UI. */
+const CONDITION_ORDER = ["ip-range", "_expression", "action-matches"];
+
+/**
+ * Sorts policy condition definitions into a consistent render order.
+ * Known conditions (ip-range, _expression, action-matches) are placed first
+ * in the order defined by CONDITION_ORDER; unknown conditions follow.
+ *
+ * @param {Array<{name: string}>} conditions - Condition definitions from the 
service def.
+ * @returns {Array} Sorted copy of the conditions array.
+ */
+export const sortPolicyConditions = (conditions) => {
+  if (!Array.isArray(conditions)) {
+    return conditions;
+  }
+
+  return [...conditions].sort((a, b) => {
+    const indexA = CONDITION_ORDER.indexOf(a?.name);
+    const indexB = CONDITION_ORDER.indexOf(b?.name);
+
+    const rankA = indexA === -1 ? 999 : indexA;
+    const rankB = indexB === -1 ? 999 : indexB;
+
+    return rankA - rankB;
+  });
+};
+
+/**
+ * Determines the deepest (leaf) resource types currently selected in the 
policy form.
+ * For Ozone, this might return {"key"} or {"bucket"} depending on which 
resource
+ * levels the user has filled in. Used to scope which action requirements 
apply —
+ * e.g. key-level actions differ from bucket-level actions.
+ *
+ * Walks the resource hierarchy (volume → bucket → key) and finds the deepest
+ * non-"none" resource the user has selected in each resource block.
+ *
+ * @param {object} serviceCompDetails - Service definition details with 
resources array.
+ * @param {object} formValues - Form state (may contain additionalResources 
for multi-resource policies).
+ * @returns {Set<string>} Set of leaf resource type names currently in use.
+ */
+export const getSelectedLeafResourceTypes = (serviceCompDetails, formValues) 
=> {
+  const result = new Set();
+  const resources = serviceCompDetails?.resources;
+  if (!Array.isArray(resources) || resources.length === 0) {
+    return result;
+  }
+
+  const levels = [...new Set(resources.map((r) => 
r?.level).filter(Number.isFinite))]
+    .sort((a, b) => a - b);
+
+  const blocks = Array.isArray(formValues?.additionalResources)
+    ? formValues.additionalResources
+    : [formValues];
+
+  for (const block of blocks) {
+    if (!block) {
+      continue;
+    }
+
+    let leaf = null;
+    for (const level of levels) {
+      const sel = block[`resourceName-${level}`];
+      if (!sel) {
+        continue;
+      }
+      if (sel?.value === NONE_RESOURCE_VALUE) {
+        break;
+      }
+      if (typeof sel?.name === "string" && sel.name.trim().length > 0) {
+        leaf = sel.name.trim();
+      }
+    }
+
+    if (leaf) {
+      result.add(leaf);
+    }
+  }
+
+  return result;
+};
+
+/**
+ * Determines if actionRequirements is nested (keyed by resource type like 
Ozone)
+ * or flat (single-level mapping).
+ *
+ * Nested example (Ozone): { volume: {ListAllMyBuckets: ["read","list"], ...}, 
bucket: {...}, key: {...} }
+ * Flat example: { open: ["read"], create: ["write"], ... }
+ */
+const isNestedActionRequirements = (actionRequirements) =>
+  Object.values(actionRequirements).some(
+    (v) => v && typeof v === "object" && !Array.isArray(v)
+  );
+
+/**
+ * Builds a case-insensitive lookup map from the original keys of an object.
+ * Used to match resource type names regardless of casing.
+ * @returns {Map<string, string>} Map from lowercased key to original key.
+ */
+const buildKeyLookup = (obj) => {
+  const map = new Map();
+  for (const key of Object.keys(obj)) {
+    map.set(key.toLowerCase(), key);
+  }
+  return map;
+};
+
+/**
+ * Resolves which action requirements apply given the currently selected leaf 
resource types.
+ *
+ * For nested requirements (like Ozone's ozone.json which is keyed by resource 
type):
+ *   - If no leaf types selected: returns all non-role sections (so all 
actions are visible).
+ *   - If only "role" is selected: returns only the role section (which is 
typically empty).
+ *   - Otherwise: returns only the sections matching the selected leaf types.
+ *   - Fallback: if no match found, returns all non-role sections.
+ *
+ * For flat requirements: wraps them under a "*" key (applies regardless of 
resource type).
+ *
+ * @param {object} actionRequirements - The action-to-permission mapping from 
ozone.json.
+ * @param {Set<string>} leafResourceTypes - Currently selected leaf resource 
types.
+ * @returns {object} Filtered requirements keyed by resource type (or "*" for 
flat).
+ */
+const getActionRequirements = (actionRequirements, leafResourceTypes) => {
+  if (!actionRequirements || typeof actionRequirements !== "object") {
+    return {};
+  }
+
+  if (!isNestedActionRequirements(actionRequirements)) {
+    return { "*": actionRequirements };
+  }
+
+  const keyLookup = buildKeyLookup(actionRequirements);
+  const result = {};
+
+  if (!leafResourceTypes || leafResourceTypes.size === 0) {
+    for (const [lowerKey, originalKey] of keyLookup) {
+      if (lowerKey !== "role") {
+        result[originalKey] = actionRequirements[originalKey];
+      }
+    }
+    return Object.keys(result).length > 0 ? result : actionRequirements;
+  }
+
+  const loweredLeaves = new Set(
+    [...leafResourceTypes].map((l) => l.trim().toLowerCase())
+  );
+  const nonRoleKeys = [...keyLookup].filter(([lk]) => lk !== "role");
+
+  const hasNonRole = nonRoleKeys.some(([lk]) => loweredLeaves.has(lk));
+  if (!hasNonRole && loweredLeaves.has("role")) {
+    const roleOriginal = keyLookup.get("role");
+    result.role = roleOriginal ? actionRequirements[roleOriginal] : {};
+    return result;
+  }
+
+  for (const [lowerKey, originalKey] of nonRoleKeys) {
+    if (loweredLeaves.has(lowerKey)) {
+      result[originalKey] = actionRequirements[originalKey];
+    }
+  }
+
+  if (Object.keys(result).length === 0) {
+    for (const [, originalKey] of nonRoleKeys) {
+      result[originalKey] = actionRequirements[originalKey];
+    }
+  }
+
+  return result;
+};
+
+/**
+ * Checks whether a concrete (non-wildcard) action is allowed given the granted
+ * access types and the permission requirements from ozone.json.
+ *
+ * An action is allowed if ANY leaf resource type's requirements for that 
action
+ * are fully satisfied by the granted permission set. This means if PutObject
+ * requires ["create", "write"] at the key level, and the user has selected 
both
+ * "create" and "write" permissions, this returns true.
+ *
+ * @param {string} actionLower - Lowercased concrete action name (e.g. 
"putobject").
+ * @param {Set<string>} grantedAccessTypes - Lowercased set of granted 
permissions (expanded).
+ * @param {Map<string, string[]>[]} normalizedReqsByLeaf - Output of 
buildNormalizedReqs().
+ * @returns {boolean} True if the action's requirements are met.
+ */
+const isConcreteActionAllowed = (
+  actionLower,
+  grantedAccessTypes,
+  normalizedReqsByLeaf
+) => {
+  if (!actionLower) {
+    return false;
+  }
+
+  for (const leafReqs of normalizedReqsByLeaf) {
+    const required = leafReqs.get(actionLower);
+
+    if (required && hasAll(required, grantedAccessTypes)) {
+      return true;
+    }
+  }
+
+  return false;
+};
+
+/**
+ * Pre-builds a normalized (lowercased keys) lookup structure from 
requirementsByLeaf
+ * so that isConcreteActionAllowed can do O(1) lookups instead of linear scans.
+ */
+const buildNormalizedReqs = (requirementsByLeaf) => {
+  const result = [];
+  for (const leaf of Object.keys(requirementsByLeaf)) {
+    const reqsByAction = requirementsByLeaf[leaf];
+    if (!reqsByAction) {
+      continue;
+    }
+    const map = new Map();
+    for (const key of Object.keys(reqsByAction)) {
+      map.set(key.toLowerCase(), reqsByAction[key]);
+    }
+    result.push(map);
+  }
+  return result;
+};
+
+/**
+ * Filters the full list of action options down to only those that are 
achievable
+ * given the currently selected permissions. This is the core filtering logic 
that
+ * drives the action-matches dropdown.
+ *
+ * Logic by option type:
+ *   - "*" (universal wildcard): shown only if "all" is granted, or if every 
concrete
+ *     action in the requirements is satisfied by the selected permissions.
+ *   - "Put*" (prefix wildcard): shown only if ALL concrete actions starting 
with
+ *     that prefix are satisfied (e.g. Put* requires PutObject, 
PutObjectTagging,
+ *     PutBucketAcl all to be achievable).
+ *   - "PutObject" (concrete action): shown only if its permission requirements
+ *     are met by the selected access types.
+ *
+ * @param {object} params
+ * @param {string[]} params.baseOptions - All possible action options from 
uiHint.
+ * @param {string[]} params.selectedAccessTypes - Currently selected access 
types for this row.
+ * @param {Set<string>} params.leafResourceTypes - Currently selected leaf 
resource types.
+ * @param {Array} params.accessTypeDefs - Service def access type definitions 
(for implied grants).
+ * @param {object} params.actionRequirements - The action-to-permission map 
(from ozone.json).
+ * @returns {string[]} Filtered action options that are achievable with 
selected permissions.
+ *   Returns an empty array when no access types are selected for the row.
+ */
+const filterActionOptions = ({
+  baseOptions,
+  selectedAccessTypes,
+  leafResourceTypes,
+  accessTypeDefs,
+  actionRequirements
+}) => {
+  if (!Array.isArray(baseOptions)) {
+    return baseOptions;
+  }
+
+  const normalizedBase = normalizeStringArray(baseOptions);
+  const selectedExpanded = expandImpliedAccessTypes(
+    selectedAccessTypes,
+    accessTypeDefs
+  );
+  const granted = toLowerSet(selectedExpanded);
+
+  if (granted.size === 0) {
+    return [];
+  }
+
+  if (!actionRequirements || Object.keys(actionRequirements).length === 0) {
+    return normalizedBase;
+  }
+
+  const requirementsByLeaf = getActionRequirements(
+    actionRequirements,
+    leafResourceTypes
+  );
+  const normalizedReqs = buildNormalizedReqs(requirementsByLeaf);
+  const scopedToLeaves =
+    leafResourceTypes &&
+    leafResourceTypes.size > 0 &&
+    isNestedActionRequirements(actionRequirements);
+
+  // Collect all concrete action names (lowercased) from the requirements data
+  const concreteActionsLower = new Set();
+  for (const leafMap of normalizedReqs) {
+    for (const key of leafMap.keys()) {
+      concreteActionsLower.add(key);
+    }
+  }
+
+  return normalizedBase.filter((opt) => {
+    // Universal wildcard: only show if all concrete actions are achievable
+    if (opt === "*") {
+      if (granted.has("all")) {
+        return true;
+      }
+
+      if (concreteActionsLower.size === 0) {
+        return false;
+      }
+
+      for (const a of concreteActionsLower) {
+        if (!isConcreteActionAllowed(a, granted, normalizedReqs)) {
+          return false;
+        }
+      }
+
+      return true;
+    }
+
+    // Prefix wildcard (e.g. "Put*"): show only if ALL matching concrete 
actions are achievable
+    if (opt.endsWith("*")) {
+      const prefixLower = opt.slice(0, -1).toLowerCase();
+      if (!prefixLower) {
+        return false;
+      }
+
+      const candidates = [...concreteActionsLower].filter((a) =>
+        a.startsWith(prefixLower)
+      );
+      if (candidates.length === 0) {
+        return false;
+      }
+
+      return candidates.every((a) =>
+        isConcreteActionAllowed(a, granted, normalizedReqs)
+      );
+    }
+
+    // Mid-string wildcards are not supported
+    if (opt.includes("*")) {
+      return false;
+    }
+
+    // Concrete action: check if its requirements are met
+    const optLower = opt.toLowerCase();
+
+    if (!concreteActionsLower.has(optLower)) {
+      // When scoped to a leaf resource (e.g. key), hide actions that do not 
apply
+      // at that depth. When unscoped, keep uiHint-only options for 
forward-compat.
+      return !scopedToLeaves;
+    }
+
+    return isConcreteActionAllowed(optLower, granted, normalizedReqs);
+  });
+};
+
+/**
+ * Entry point for computing the action-matches dropdown options for a 
permission row.
+ * Filters the base options using the current row's access types and resource 
context,
+ * then sorts the result.
+ *
+ * @param {object} params
+ * @param {string} params.servicedefName - Service definition name.
+ * @param {string[]} params.baseOptions - All possible action options from 
uiHint.
+ * @param {object} params.actionFilterContext - Contains selectedAccessTypes, 
leafResourceTypes, accessTypeDefs.
+ * @param {object} params.actionRequirements - Action-to-permission map from 
ozone.json.
+ * @returns {string[]} Filtered and sorted action options, or [] when the row 
has no permissions.
+ */
+const getActionMatchesOptions = ({
+  servicedefName,
+  baseOptions,
+  actionFilterContext,
+  actionRequirements
+}) => {
+  const selectedAccessTypes = actionFilterContext?.selectedAccessTypes;
+  if (!Array.isArray(selectedAccessTypes) || selectedAccessTypes.length === 0) 
{
+    return [];
+  }
+
+  const filtered = filterActionOptions({
+    baseOptions,
+    selectedAccessTypes,
+    leafResourceTypes: actionFilterContext.leafResourceTypes,
+    accessTypeDefs: actionFilterContext.accessTypeDefs,
+    actionRequirements
+  });
+  return sortActionOptions(filtered, servicedefName);
+};
+
+/**
+ * Returns a new object with any null or empty string values removed.
+ * Useful to prevent React state mutation and to get accurate keys count.
+ */
+export const getCleanConditions = (conditions) => {
+  if (!conditions) {
+    return {};
+  }
+  const cleaned = { ...conditions };
+  for (const key in cleaned) {
+    if (cleaned[key] == null || cleaned[key] === "") {
+      delete cleaned[key];
+    }
+  }
+  return cleaned;
+};
+
+/**
+ * Removes any previously selected action-matches values that are no longer
+ * in the allowed options set. Called when the user changes permissions or
+ * resources, potentially invalidating prior action selections.
+ *
+ * Handles both string arrays and react-select option objects ({label, value}).
+ *
+ * @param {object} params
+ * @param {Array} params.selected - Currently selected values (strings or 
{label, value} objects).
+ * @param {string[]|Set} params.allowedOptions - The currently valid action 
options.
+ * @returns {Array} Filtered selection with only still-valid entries.
+ */
+export const pruneSelectedActionMatches = ({ selected, allowedOptions }) => {
+  if (!Array.isArray(selected)) {
+    return selected;
+  }
+  const allowedSet =
+    allowedOptions instanceof Set
+      ? allowedOptions
+      : new Set(
+          (allowedOptions || []).map((a) =>
+            typeof a === "string" ? a.trim() : ""
+          )
+        );
+
+  return selected.filter((o) => {
+    // selected might be an array of strings or objects {label, value}
+    const v = typeof o === "object" ? o?.value : o;
+    const normalized = typeof v === "string" ? v.trim() : "";
+    return normalized && allowedSet.has(normalized);
+  });
+};
+
+/**
+ * Shared helper to compute allowed action matches and prune the selection.
+ * Used by ConditionRow (popover) and usePruneStaleConditions (background 
sync).
+ *
+ * Only action-matches runs getActionMatchesOptions; ip-range uses 
CreatableSelect
+ * when uiHint has no fixed options array.
+ *
+ * @param {object} params
+ * @param {string} params.conditionName - Condition def name (e.g. 
"action-matches").
+ * @param {object} params.actionFilterContext - Row permissions + leaf 
resources + accessTypeDefs.
+ * @param {object} params.actionReqsMap - From 
buildActionReqsMapFromConditionDef().
+ * @param {string} params.servicedefName - Service definition name.
+ * @param {object} params.uiHintAttb - Parsed uiHint for this condition.
+ * @param {Array} params.currentSelection - Current react-select value for the 
field.
+ * @returns {{ dropdownOptions: Array|null, prunedSelection: Array }}
+ */
+export const getAllowedActionMatchesForCondition = ({
+  conditionName,
+  actionFilterContext,
+  actionReqsMap,
+  servicedefName,
+  uiHintAttb,
+  currentSelection
+}) => {
+  const fixedOptions = Array.isArray(uiHintAttb?.options)
+    ? uiHintAttb.options
+    : null;
+  const isActionMatches = isPerRowCondition(conditionName);
+
+  const dropdownOptions = fixedOptions
+    ? (isActionMatches
+        ? getActionMatchesOptions({
+            servicedefName,
+            baseOptions: fixedOptions,
+            actionFilterContext,
+            actionRequirements:
+              actionReqsMap[conditionName] || uiHintAttb?.actionRequirements
+          })
+        : fixedOptions
+      ).map((v) => ({ label: v, value: v }))
+    : null;
+
+  const allowedValueSet = dropdownOptions
+    ? new Set(dropdownOptions.map((o) => o.value))
+    : null;
+
+  const prunedSelection =
+    allowedValueSet && Array.isArray(currentSelection)
+      ? pruneSelectedActionMatches({
+          selected: currentSelection,
+          allowedOptions: allowedValueSet
+        })
+      : currentSelection;
+
+  return { dropdownOptions, prunedSelection };
+};
+
+/**
+ * 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.
+ *
+ * This is memoized in PolicyPermissionItem and Editable so we don't re-parse
+ * uiHint JSON on every render.
+ *
+ * @param {Array<{name: string, uiHint: string}>} conditionDefVal - Condition 
definitions from service def.
+ * @returns {object} Map of condition name → action requirements object.
+ */
+export const buildActionReqsMapFromConditionDef = (conditionDefVal) => {
+  const map = {};
+  if (!Array.isArray(conditionDefVal) || conditionDefVal.length === 0) {
+    return map;
+  }
+
+  conditionDefVal.forEach((m) => {
+    const uiHintAttb = parseConditionUiHint(m.uiHint);
+
+    if (uiHintAttb) {
+      const fileToLoad = uiHintAttb?.actionRequirementsFile;
+      if (fileToLoad && actionRequirementsRegistry[fileToLoad]) {
+        map[m.name] = actionRequirementsRegistry[fileToLoad];
+      } else if (uiHintAttb?.actionRequirements) {
+        map[m.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 515c2b0d0..139592264 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
@@ -62,6 +62,7 @@ import PolicyPermissionItem from 
"../PolicyListing/PolicyPermissionItem";
 import { useParams, useNavigate, useLocation } from "react-router-dom";
 import PolicyValidityPeriodComp from "./PolicyValidityPeriodComp";
 import PolicyConditionsComp from "./PolicyConditionsComp";
+import { isPerRowCondition } from "Utils/policyConditionUtils";
 import moment from "moment";
 import {
   InfoIcon,
@@ -1510,6 +1511,9 @@ 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/PolicyConditionsComp.jsx
 
b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyConditionsComp.jsx
index 7c8aba953..678b43840 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
@@ -25,25 +25,43 @@ import CreatableSelect from "react-select/creatable";
 import { find, isEmpty } from "lodash";
 import { InfoIcon } from "Utils/XAUtils";
 import { RegexMessage } from "Utils/XAMessages";
-import { selectInputCustomStyles } from "Components/CommonComponents";
+import {
+  selectInputCustomStyles,
+  trimInputValue
+} from "Components/CommonComponents";
+import {
+  isPerRowCondition,
+  getCleanConditions,
+  parseConditionUiHint
+} from "Utils/policyConditionUtils";
 const esprima = require("esprima");
 
 export default function PolicyConditionsComp(props) {
   const { policyConditionDetails, inputVal, showModal, handleCloseModal } =
     props;
 
+  // Policy-level modal only; action-matches is edited on each permission row 
(Editable).
+  const filteredPolicyConditionDetails = Array.isArray(policyConditionDetails)
+    ? policyConditionDetails.filter((c) => !isPerRowCondition(c?.name))
+    : policyConditionDetails;
+
   const accessedOpt = [
     { value: "yes", label: "Yes" },
     { value: "no", label: "No" }
   ];
 
   const handleSubmit = (values) => {
-    for (let val in values.conditions) {
-      if (values.conditions[val] == null || values.conditions[val] == "") {
-        delete values.conditions[val];
+    if (values?.conditions) {
+      const newConditions = getCleanConditions(values.conditions);
+      for (let val in newConditions) {
+        if (isPerRowCondition(val)) {
+          delete newConditions[val];
+        }
       }
+      inputVal.onChange(newConditions);
+    } else {
+      inputVal.onChange(values?.conditions);
     }
-    inputVal.onChange(values.conditions);
     handleClose();
   };
 
@@ -125,13 +143,10 @@ export default function PolicyConditionsComp(props) {
                 <Modal.Title>Policy Condition</Modal.Title>
               </Modal.Header>
               <Modal.Body>
-                {policyConditionDetails?.length > 0 &&
-                  policyConditionDetails.map((m) => {
-                    let uiHintAttb =
-                      m.uiHint != undefined && m.uiHint != ""
-                        ? JSON.parse(m.uiHint)
-                        : "";
-                    if (uiHintAttb != "") {
+                {filteredPolicyConditionDetails?.length > 0 &&
+                  filteredPolicyConditionDetails.map((m) => {
+                    const uiHintAttb = parseConditionUiHint(m.uiHint);
+                    if (uiHintAttb) {
                       if (uiHintAttb?.singleValue) {
                         return (
                           <div key={m.name}>
@@ -193,6 +208,9 @@ export default function PolicyConditionsComp(props) {
                                           }
                                           as="textarea"
                                           rows={3}
+                                          onBlur={(e) =>
+                                            trimInputValue(e, input)
+                                          }
                                         />
                                         {meta.error && (
                                           <span className="invalid-field">
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 4496f7541..342034f6f 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
@@ -21,7 +21,7 @@ import React, { useMemo, useRef, useState } from "react";
 import { Table, Button, Form } from "react-bootstrap";
 import { FieldArray } from "react-final-form-arrays";
 import { Col } from "react-bootstrap";
-import { Field } from "react-final-form";
+import { Field, useForm } from "react-final-form";
 import AsyncSelect from "react-select/async";
 import {
   find,
@@ -45,6 +45,8 @@ import {
   policyConditionUpdatedJSON
 } from "Utils/XAUtils";
 import { selectInputCustomStyles } from "Components/CommonComponents";
+import { getSelectedLeafResourceTypes, getSelectedAccessTypesForRow, 
buildActionReqsMapFromConditionDef, getCleanConditions } from 
"Utils/policyConditionUtils";
+import { usePruneStaleConditions } from "../../hooks/usePruneStaleConditions";
 
 const noneOptions = {
   label: "None",
@@ -105,15 +107,64 @@ export default function PolicyPermissionItem(props) {
   };
 
   const grpResourcesKeys = useMemo(() => {
-    const { resources = [] } = serviceCompDetails;
+    const { resources = [] } = serviceCompDetails || {};
     const grpResources = groupBy(resources, "level");
     let grpResourcesKeys = [];
     for (const resourceKey in grpResources) {
       grpResourcesKeys.push(+resourceKey);
     }
-    grpResourcesKeys = grpResourcesKeys.sort();
+    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({
+    formValues,
+    attrName,
+    form,
+    leafResourceTypes,
+    serviceCompDetails,
+    conditionDefVal,
+    actionReqsMap
+  });
 
   const getAccessTypeOptions = () => {
     let srcOp = [],
@@ -263,16 +314,9 @@ export default function PolicyPermissionItem(props) {
         error = "Please select user/group/role for the selected delegate 
Admin";
       }
       if (policyConditionVal) {
-        for (const key in policyConditionVal) {
-          if (
-            policyConditionVal[key] == null ||
-            policyConditionVal[key] == ""
-          ) {
-            delete policyConditionVal[key];
-          }
-        }
+        const newConditions = getCleanConditions(policyConditionVal);
         if (
-          Object.keys(policyConditionVal).length != 0 &&
+          Object.keys(newConditions).length != 0 &&
           !users &&
           !grps &&
           !roles
@@ -445,6 +489,7 @@ export default function PolicyPermissionItem(props) {
                             serviceCompDetails?.policyConditions?.length >
                               0 && (
                               <td key={colName} className="align-middle">
+                                {/* Per-row conditions: Editable + 
actionFilterContext for action-matches */}
                                 <Field
                                   className="form-control"
                                   name={`${name}.conditions`}
@@ -460,10 +505,20 @@ export default function PolicyPermissionItem(props) {
                                         {...input}
                                         placement="auto"
                                         type="custom"
-                                        
conditionDefVal={policyConditionUpdatedJSON(
-                                          serviceCompDetails.policyConditions
-                                        )}
-                                        selectProps={{ isMulti: true }}
+                                        conditionDefVal={conditionDefVal}
+                                        
servicedefName={serviceCompDetails?.name}
+                                        actionFilterContext={{
+                                          selectedAccessTypes:
+                                            
getSelectedAccessTypesForRow(formValues, attrName, index),
+                                          leafResourceTypes,
+                                          accessTypeDefs:
+                                            serviceCompDetails?.accessTypes
+                                        }}
+                                        selectProps={{
+                                          isMulti: true,
+                                          // Portal menus to body (see 
Editable CONDITION_POPOVER_SELECT_PROPS)
+                                          // so a long Action list with ALL 
permission does not jump the page.
+                                        }}
                                       />
                                     </div>
                                   )}

Reply via email to