From 549de95eabb51163891f43ca80c6ddcea77ba46e Mon Sep 17 00:00:00 2001 From: Fabian Morgan Date: Wed, 3 Jun 2026 14:54:32 -0700 Subject: [PATCH 1/5] update Ranger policy logic and Ozone authorizer Generated-By: Cursor, Claude Code and then with additional manual updates. --- .../plugin/model/RangerInlinePolicy.java | 20 +++- .../RangerInlinePolicyEvaluator.java | 17 ++- .../test_inline_policies_ozone.json | 111 ++++++++++++++++++ .../authorizer/RangerOzoneAuthorizer.java | 7 +- .../authorizer/TestRangerOzoneAuthorizer.java | 72 ++++++++++++ .../src/test/resources/om_dev_ozone.json | 6 + 6 files changed, 227 insertions(+), 6 deletions(-) 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 d33eddbd3f..0032a674bb 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 principals; // example: [ "u:user1, "g:group1", "r:role1" ]; if empty, means public grant private Set resources; // example: [ "key:vol1/bucket1/db1/tbl1/*", "key:vol1/bucket1/db1/tbl2/*" ]; if empty, means all resources private Set permissions; // example: [ "read", "write" ]; if empty, means no permission + private Set actions; // optional: [ "GetObject", "Put*", "*" ]; if empty or null, means all actions public Grant() { } public Grant(Set principals, Set resources, Set permissions) { + this(principals, resources, permissions, null); + } + + public Grant(Set principals, Set resources, Set permissions, Set actions) { this.principals = principals; this.resources = resources; this.permissions = permissions; + this.actions = actions; } public Set getPrincipals() { @@ -146,6 +152,14 @@ public void setPermissions(Set permissions) { this.permissions = permissions; } + public Set getActions() { + return actions; + } + + public void setActions(Set 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 8f2b319d66..6702166daf 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 toGrantEvaluators(RangerInlinePolicy policy) { private class GrantEvaluator { private final RangerInlinePolicy.Grant grant; private final Set permissions; + private final RangerActionListMatcher actionMatcher; private final Set 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/test/resources/policyevaluator/test_inline_policies_ozone.json b/agents-common/src/test/resources/policyevaluator/test_inline_policies_ozone.json index bb41f4bbdd..4c185453db 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": "ALLOW 'write s3v/iceberg/key2;' for user=svc-etl; no action in request bypasses 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": true, "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/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 4414f22ada..f7d61fec29 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 @@ -168,7 +168,7 @@ public boolean checkAccess(IOzoneObj ozoneObject, RequestContext context) { rangerResource.setOwnerUser(context.getOwnerName()); rangerRequest.setResource(rangerResource); rangerRequest.setAccessType(accessType); - rangerRequest.setAction(accessType); + rangerRequest.setAction(context.getS3Action()); rangerRequest.setRequestData(resource); rangerRequest.setClusterName(clusterName); @@ -313,6 +313,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 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 1f365dd61b..3cfc282a49 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 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 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 bypass action restriction (allow through) + RequestContext ctxWriteNoAction = reqCtxBuilder + .setAclRights(IAccessAuthorizer.ACLType.WRITE) + .setRecursiveAccessCheck(false) + .setSessionPolicy(sessionPolicy) + .setS3Action(null) + .build(); + + assertTrue(ozoneAuthorizer.checkAccess(key1, ctxWriteNoAction), + "session-policy should allow write on key1 when no S3 action is specified (bypass)"); + } } diff --git a/plugin-ozone/src/test/resources/om_dev_ozone.json b/plugin-ozone/src/test/resources/om_dev_ozone.json index e8d761801a..d131df82c2 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 From 4d4ee8344dcceda8d77ee9d61b611e42d25b2467 Mon Sep 17 00:00:00 2001 From: Fabian Morgan Date: Wed, 3 Jun 2026 15:01:33 -0700 Subject: [PATCH 2/5] add a new matcher for actions Generated-By: Cursor, Claude Code and then with additional manual updates. --- .../RangerActionMatcher.java | 52 ++++ .../plugin/util/RangerActionListMatcher.java | 122 +++++++++ .../RangerActionMatcherTest.java | 111 ++++++++ .../util/RangerActionListMatcherTest.java | 243 ++++++++++++++++++ 4 files changed, 528 insertions(+) create mode 100644 agents-common/src/main/java/org/apache/ranger/plugin/conditionevaluator/RangerActionMatcher.java create mode 100644 agents-common/src/main/java/org/apache/ranger/plugin/util/RangerActionListMatcher.java create mode 100644 agents-common/src/test/java/org/apache/ranger/plugin/conditionevaluator/RangerActionMatcherTest.java create mode 100644 agents-common/src/test/java/org/apache/ranger/plugin/util/RangerActionListMatcherTest.java 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 0000000000..31d5228998 --- /dev/null +++ b/agents-common/src/main/java/org/apache/ranger/plugin/conditionevaluator/RangerActionMatcher.java @@ -0,0 +1,52 @@ +/* + * 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 = new RangerActionListMatcher(null); + + @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 boolean ret = actionMatcher.isMatch(request != null ? request.getAction() : null); + + LOG.debug("<== RangerActionMatcher.isMatched({}): {}", request, ret); + + return ret; + } +} 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 0000000000..507de29cf1 --- /dev/null +++ b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerActionListMatcher.java @@ -0,0 +1,122 @@ +/* + * 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 exactActions = new HashSet<>(); + private final String[] prefixActions; + + public RangerActionListMatcher(Collection actions) { + boolean allowAny = CollectionUtils.isEmpty(actions); + final List 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)); + } + } + } + + this.allowAnyAction = allowAny; + + if (!tempPrefixList.isEmpty()) { + tempPrefixList.sort((s1, s2) -> Integer.compare(s1.length(), s2.length())); + + List 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) { + boolean ret = allowAnyAction; + + if (!ret) { + // if action is not available, don't enforce action restrictions + if (StringUtils.isBlank(requestAction)) { + ret = true; + } else { + final String actionLower = requestAction.toLowerCase(Locale.ROOT); + + if (exactActions.contains(actionLower)) { + ret = true; + } else { + for (String prefix : prefixActions) { + if (actionLower.startsWith(prefix)) { + ret = true; + break; + } + } + } + } + } + + return ret; + } +} 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 0000000000..79b86a0603 --- /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; + +public class RangerActionMatcherTest { + @Test + public 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 + public void testWildcardMatchesAll() { + final RangerActionMatcher matcher = createMatcher(new String[] {"*"}); + + assertTrue(matcher.isMatched(createRequest("GetObject"))); + assertTrue(matcher.isMatched(createRequest("PutObject"))); + assertTrue(matcher.isMatched(createRequest(null))); + } + + @Test + public void testNoRequestActionDoesNotEnforce() { + final RangerActionMatcher matcher = createMatcher(new String[] {"PutObject"}); + + assertTrue(matcher.isMatched(createRequest(null))); + assertTrue(matcher.isMatched(createRequest(""))); + assertTrue(matcher.isMatched(createRequest(" "))); + } + + @Test + public 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 + public 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 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 0000000000..a32a1b1cd7 --- /dev/null +++ b/agents-common/src/test/java/org/apache/ranger/plugin/util/RangerActionListMatcherTest.java @@ -0,0 +1,243 @@ +/* + * 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; + +public class RangerActionListMatcherTest { + @Test + public void testNullActionsMatchAll() { + final RangerActionListMatcher matcher = new RangerActionListMatcher(null); + + assertTrue(matcher.isMatch("PutObject")); + assertTrue(matcher.isMatch("GetObject")); + assertTrue(matcher.isMatch(null)); + assertTrue(matcher.isMatch("")); + } + + @Test + public void testEmptyActionsMatchAll() { + final RangerActionListMatcher matcher = new RangerActionListMatcher(Collections.emptyList()); + + assertTrue(matcher.isMatch("PutObject")); + assertTrue(matcher.isMatch("GetObject")); + assertTrue(matcher.isMatch(null)); + } + + @Test + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public 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 + public void testBlankRequestActionBypassesRestriction() { + final RangerActionListMatcher matcher = new RangerActionListMatcher( + Arrays.asList("PutObject")); + + // When no action is provided, action restrictions are not enforced + assertTrue(matcher.isMatch(null)); + assertTrue(matcher.isMatch("")); + assertTrue(matcher.isMatch(" ")); + } + + @Test + public void testBlankEntriesInActionsIgnored() { + final RangerActionListMatcher matcher = new RangerActionListMatcher( + Arrays.asList("", " ", "PutObject")); + + assertTrue(matcher.isMatch("PutObject")); + assertFalse(matcher.isMatch("GetObject")); + } + + @Test + public void testPrefixWildcardCaseInsensitive() { + final RangerActionListMatcher matcher = new RangerActionListMatcher(Arrays.asList("Put*")); + + assertTrue(matcher.isMatch("PUTOBJECT")); + assertTrue(matcher.isMatch("putobject")); + assertTrue(matcher.isMatch("PuToBjEcT")); + } + + @Test + public 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 + public 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 + public void testAllOzoneS3Actions() { + // Grant with both wildcards and exact actions matching Ozone S3 patterns + final List 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")); + } +} From 047e378db86448b1823bbfc5f1767923adace540 Mon Sep 17 00:00:00 2001 From: Fabian Morgan Date: Wed, 3 Jun 2026 15:03:12 -0700 Subject: [PATCH 3/5] temporarily use Ozone 2.1.1 release candidate for compilation purposes Generated-By: Cursor --- pom.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f296447346..db16fa5a44 100755 --- a/pom.xml +++ b/pom.xml @@ -190,7 +190,7 @@ 1.79 1.79 20211018.2 - 2.1.0 + 2.1.1 2.3 5.2.2 0.192 @@ -892,6 +892,11 @@ jetbrains-intellij-dependencies https://packages.jetbrains.team/maven/p/ij/intellij-dependencies + + apache-ozone-staging + Apache Ozone 2.1.1 RC0 Staging + https://repository.apache.org/content/repositories/orgapacheozone-1062/ + From f963573ec64ba45c8bd2263bf04ac49de8996fc2 Mon Sep 17 00:00:00 2001 From: Fabian Morgan Date: Wed, 3 Jun 2026 15:14:33 -0700 Subject: [PATCH 4/5] Support actions in UI, populate Ozone action choices based on permissions, and ensure actions wrap around instead of scrolling horizontally indefinitely. It also fixes latent React hook violations of having useState within a conditional (CommonComponents.jsx) and a loop (Editable.jsx). It also ensures that the action field only shows up in the Policy Conditions section, not on the overall Resource Policy. Generated-By: Cursor, Claude Code --- .../service-defs/ranger-servicedef-ozone.json | 8 + .../src/components/CommonComponents.jsx | 17 + .../react-webapp/src/components/Editable.jsx | 509 +++++++----- .../src/hooks/usePruneStaleConditions.js | 145 ++++ .../webapp/react-webapp/src/styles/style.css | 12 + .../src/utils/actionRequirements/ozone.json | 47 ++ .../src/utils/actionRequirements/registry.js | 30 + .../src/utils/policyConditionUtils.js | 722 ++++++++++++++++++ .../GovernedData/Dataset/AccessGrantForm.jsx | 4 + .../Datashare/DatashareDetailLayout.jsx | 4 + .../PolicyListing/AddUpdatePolicyForm.jsx | 4 + .../PolicyListing/PolicyConditionsComp.jsx | 34 +- .../PolicyListing/PolicyPermissionItem.jsx | 80 +- 13 files changed, 1399 insertions(+), 217 deletions(-) create mode 100644 security-admin/src/main/webapp/react-webapp/src/hooks/usePruneStaleConditions.js create mode 100644 security-admin/src/main/webapp/react-webapp/src/utils/actionRequirements/ozone.json create mode 100644 security-admin/src/main/webapp/react-webapp/src/utils/actionRequirements/registry.js create mode 100644 security-admin/src/main/webapp/react-webapp/src/utils/policyConditionUtils.js 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 9f67ae4c61..bef99877ce 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/security-admin/src/main/webapp/react-webapp/src/components/CommonComponents.jsx b/security-admin/src/main/webapp/react-webapp/src/components/CommonComponents.jsx index 053ced0e21..0830f5e1d5 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,19 @@ 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 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 c5754ac958..cfd59ec60d 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,183 +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 ( -
- - {m.label}: - 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.trim(); - return (value = val); + isMulti={true} + isClearable={false} + /> + +
+ ); + } + + 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 ( +
+ + + + {m.label}: + + {RegexMessage.MESSAGE.policyConditionInfoIcon} +

} - return value !== null ? value : ""; - }; - const textAreaHandleChange = (e, name) => { - setJSCondVal(e.target.value); - tagAccessData(e.target.value, name); - }; - return ( -
- - - - {m.label}: - - {RegexMessage.MESSAGE.policyConditionInfoIcon} -

- } - /> - -
- - - textAreaHandleChange(e, m.name)} - onBlur={(e) => { - textAreaHandleChange( - { target: { value: e.target.value.trim() } }, - m.name - ); - }} - isInvalid={validExpression.state} - /> - {validExpression.state && ( -
- {validExpression.errorMSG} -
- )} - -
-
-
- ); - } - if (uiHintAttb?.isMultiValue) { - const [selectedInputVal, setSelectVal] = useState( - value?.[m.name] || [] - ); - const handleChange = (e, name) => { - setSelectVal(e); - tagAccessData(e, name); - }; - return ( -
- - {m.label}: - { - setSelectVal(e); - handleChange(e, m.name); - }} - placeholder="" - width="500px" - isClearable={false} - styles={selectInputCustomStyles} - 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); - } - }} - /> - + /> + + + + + textAreaHandleChange(e, m.name)} + onBlur={(e) => { + textAreaHandleChange( + { target: { value: e.target.value.trim() } }, + m.name + ); + }} + isInvalid={validExpression.state} + /> + {validExpression.state && ( +
+ {validExpression.errorMSG}
- ); - } - } - })} + )} + +
+ +
+ ); + } + + 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 ( +
+ + {m.label}: + {dropdownOptions ? ( +