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/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/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/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/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")); + } +} 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 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/ + 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 0000000000..62e9846a0f --- /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 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 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 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 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 ? ( +