From d2528b484a4d5db3b14380b6e735e445b97a8102 Mon Sep 17 00:00:00 2001 From: Vyom Mani Tiwari Date: Fri, 23 Oct 2026 18:40:39 +0530 Subject: [PATCH] RANGER-5858: Authz API: return constraints in the authorization result --- .../ranger/authz/model/RangerAuthzResult.java | 119 ++++++++++++++- .../authz/model/TestRangerAuthzResult.java | 91 ++++++++++++ .../authz/embedded/RangerAuthzPlugin.java | 30 ++++ .../TestRangerAuthzPluginConstraints.java | 139 ++++++++++++++++++ 4 files changed, 373 insertions(+), 6 deletions(-) create mode 100644 authz-api/src/test/java/org/apache/ranger/authz/model/TestRangerAuthzResult.java create mode 100644 authz-embedded/src/test/java/org/apache/ranger/authz/embedded/TestRangerAuthzPluginConstraints.java diff --git a/authz-api/src/main/java/org/apache/ranger/authz/model/RangerAuthzResult.java b/authz-api/src/main/java/org/apache/ranger/authz/model/RangerAuthzResult.java index 0dbf6b4cb1a..394b2c1bfdb 100644 --- a/authz-api/src/main/java/org/apache/ranger/authz/model/RangerAuthzResult.java +++ b/authz-api/src/main/java/org/apache/ranger/authz/model/RangerAuthzResult.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -121,6 +122,7 @@ public static class PermissionResult { private AccessResult access; private DataMaskResult dataMask; private RowFilterResult rowFilter; + private List constraints; private Map additionalInfo; private Map subResources; @@ -143,6 +145,7 @@ public PermissionResult(String permission, ResultInfo result, Map getConstraints() { + return constraints; + } + + public void setConstraints(List constraints) { + this.constraints = constraints; + } + public Map getAdditionalInfo() { return additionalInfo; } @@ -212,7 +223,7 @@ public void addSubResourceResult(String resourceName, ResultInfo result) { @Override public int hashCode() { - return Objects.hash(permission, access, dataMask, rowFilter, additionalInfo, subResources); + return Objects.hash(permission, access, dataMask, rowFilter, constraints, additionalInfo, subResources); } @Override @@ -229,6 +240,7 @@ public boolean equals(Object o) { Objects.equals(access, that.access) && Objects.equals(dataMask, that.dataMask) && Objects.equals(rowFilter, that.rowFilter) && + Objects.equals(constraints, that.constraints) && Objects.equals(additionalInfo, that.additionalInfo) && Objects.equals(subResources, that.subResources); } @@ -240,6 +252,7 @@ public String toString() { ", access=" + access + ", dataMask=" + dataMask + ", rowFilter=" + rowFilter + + ", constraints=" + constraints + ", additionalInfo=" + additionalInfo + ", subResources=" + subResources + '}'; @@ -250,10 +263,11 @@ public String toString() { @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonIgnoreProperties(ignoreUnknown = true) public static class ResultInfo { - private AccessResult access; - private DataMaskResult dataMask; - private RowFilterResult rowFilter; - private Map additionalInfo; + private AccessResult access; + private DataMaskResult dataMask; + private RowFilterResult rowFilter; + private List constraints; + private Map additionalInfo; public ResultInfo() { } @@ -289,6 +303,14 @@ public void setRowFilter(RowFilterResult rowFilter) { this.rowFilter = rowFilter; } + public List getConstraints() { + return constraints; + } + + public void setConstraints(List constraints) { + this.constraints = constraints; + } + public Map getAdditionalInfo() { return additionalInfo; } @@ -299,7 +321,7 @@ public void setAdditionalInfo(Map additionalInfo) { @Override public int hashCode() { - return Objects.hash(access, dataMask, rowFilter, additionalInfo); + return Objects.hash(access, dataMask, rowFilter, constraints, additionalInfo); } @Override @@ -315,6 +337,7 @@ public boolean equals(Object o) { return Objects.equals(access, that.access) && Objects.equals(dataMask, that.dataMask) && Objects.equals(rowFilter, that.rowFilter) && + Objects.equals(constraints, that.constraints) && Objects.equals(additionalInfo, that.additionalInfo); } @@ -324,6 +347,7 @@ public String toString() { "access=" + access + ", dataMask=" + dataMask + ", rowFilter=" + rowFilter + + ", constraints=" + constraints + ", additionalInfo=" + additionalInfo + '}'; } @@ -518,6 +542,89 @@ public String toString() { } } + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ConstraintResult { + private String name; + private Map parameters; + private PolicyInfo policy; + private Integer policyPriority; + + public ConstraintResult() { + } + + public ConstraintResult(String name, Map parameters, PolicyInfo policy, Integer policyPriority) { + this.name = name; + this.parameters = parameters; + this.policy = policy; + this.policyPriority = policyPriority; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + public PolicyInfo getPolicy() { + return policy; + } + + public void setPolicy(PolicyInfo policy) { + this.policy = policy; + } + + public Integer getPolicyPriority() { + return policyPriority; + } + + public void setPolicyPriority(Integer policyPriority) { + this.policyPriority = policyPriority; + } + + @Override + public int hashCode() { + return Objects.hash(name, parameters, policy, policyPriority); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (o == null || getClass() != o.getClass()) { + return false; + } + + ConstraintResult that = (ConstraintResult) o; + + return Objects.equals(name, that.name) && + Objects.equals(parameters, that.parameters) && + Objects.equals(policy, that.policy) && + Objects.equals(policyPriority, that.policyPriority); + } + + @Override + public String toString() { + return "ConstraintResult{" + + "name='" + name + '\'' + + ", parameters=" + parameters + + ", policy=" + policy + + ", policyPriority=" + policyPriority + + '}'; + } + } + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/authz-api/src/test/java/org/apache/ranger/authz/model/TestRangerAuthzResult.java b/authz-api/src/test/java/org/apache/ranger/authz/model/TestRangerAuthzResult.java new file mode 100644 index 00000000000..1b712645e15 --- /dev/null +++ b/authz-api/src/test/java/org/apache/ranger/authz/model/TestRangerAuthzResult.java @@ -0,0 +1,91 @@ +/* + * 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.authz.model; + +import org.apache.ranger.authz.model.RangerAuthzResult.AccessDecision; +import org.apache.ranger.authz.model.RangerAuthzResult.AccessResult; +import org.apache.ranger.authz.model.RangerAuthzResult.ConstraintResult; +import org.apache.ranger.authz.model.RangerAuthzResult.PermissionResult; +import org.apache.ranger.authz.model.RangerAuthzResult.PolicyInfo; +import org.apache.ranger.authz.model.RangerAuthzResult.ResultInfo; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestRangerAuthzResult { + @Test + public void testPermissionResultCopiesConstraintsFromResultInfo() { + List constraints = Collections.singletonList(requireApproval(21L)); + ResultInfo resultInfo = new ResultInfo(new AccessResult(AccessDecision.ALLOW, new PolicyInfo(12L, 3L)), null, null, null); + + resultInfo.setConstraints(constraints); + + PermissionResult permResult = new PermissionResult("execute", resultInfo); + + assertEquals(constraints, permResult.getConstraints()); + assertEquals(AccessDecision.ALLOW, permResult.getAccess().getDecision()); + } + + @Test + public void testConstraintsInEqualsAndHashCode() { + ResultInfo withConstraint = new ResultInfo(new AccessResult(AccessDecision.ALLOW, null), null, null, null); + ResultInfo withoutConstraint = new ResultInfo(new AccessResult(AccessDecision.ALLOW, null), null, null, null); + + withConstraint.setConstraints(Collections.singletonList(requireApproval(21L))); + + assertNotEquals(withConstraint, withoutConstraint); + + withoutConstraint.setConstraints(Collections.singletonList(requireApproval(21L))); + + assertEquals(withConstraint, withoutConstraint); + assertEquals(withConstraint.hashCode(), withoutConstraint.hashCode()); + + PermissionResult perm1 = new PermissionResult("execute", withConstraint); + PermissionResult perm2 = new PermissionResult("execute", withoutConstraint); + + assertEquals(perm1, perm2); + + perm2.setConstraints(Collections.singletonList(requireApproval(22L))); + + assertNotEquals(perm1, perm2); + } + + @Test + public void testConstraintResult() { + ConstraintResult constraint = requireApproval(21L); + + assertEquals("requireApproval", constraint.getName()); + assertEquals("finance-approvers", constraint.getParameters().get("approver")); + assertEquals(new PolicyInfo(21L, 1L), constraint.getPolicy()); + assertEquals(Integer.valueOf(0), constraint.getPolicyPriority()); + assertEquals(requireApproval(21L), constraint); + assertNotEquals(requireApproval(22L), constraint); + assertTrue(constraint.toString().contains("requireApproval")); + } + + private static ConstraintResult requireApproval(Long policyId) { + return new ConstraintResult("requireApproval", Collections.singletonMap("approver", "finance-approvers"), new PolicyInfo(policyId, 1L), 0); + } +} diff --git a/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerAuthzPlugin.java b/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerAuthzPlugin.java index 28ed1b9d5ae..3c6d6c2f4dc 100644 --- a/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerAuthzPlugin.java +++ b/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerAuthzPlugin.java @@ -20,6 +20,7 @@ package org.apache.ranger.authz.embedded; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig; import org.apache.ranger.authz.api.RangerAuthzApiErrorCode; @@ -30,6 +31,7 @@ import org.apache.ranger.authz.model.RangerAuthzResult; import org.apache.ranger.authz.model.RangerAuthzResult.AccessDecision; import org.apache.ranger.authz.model.RangerAuthzResult.AccessResult; +import org.apache.ranger.authz.model.RangerAuthzResult.ConstraintResult; import org.apache.ranger.authz.model.RangerAuthzResult.DataMaskResult; import org.apache.ranger.authz.model.RangerAuthzResult.PermissionResult; import org.apache.ranger.authz.model.RangerAuthzResult.PolicyInfo; @@ -47,6 +49,7 @@ import org.apache.ranger.plugin.policyengine.RangerAccessResource; import org.apache.ranger.plugin.policyengine.RangerAccessResourceImpl; import org.apache.ranger.plugin.policyengine.RangerAccessResult; +import org.apache.ranger.plugin.policyengine.RangerConstraintResult; import org.apache.ranger.plugin.policyengine.RangerResourceACLs; import org.apache.ranger.plugin.service.RangerBasePlugin; import org.apache.ranger.plugin.util.RangerAccessRequestUtil; @@ -54,8 +57,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Properties; @@ -133,6 +138,7 @@ public RangerAuthzResult authorize(RangerAuthzRequest request, RangerAuthzAuditH if (permResult.getAccess().getDecision() == AccessDecision.DENY) { permResult.setDataMask(null); permResult.setRowFilter(null); + permResult.setConstraints(null); } ret.getPermissions().put(permission, permResult); @@ -308,6 +314,30 @@ private ResultInfo toPermissionResult(RangerAccessResult result) { ret.setRowFilter(new RowFilterResult(result.getFilterExpr(), ret.getAccess().getPolicy())); } + ret.setConstraints(toConstraintResults(result)); + + return ret; + } + + // Constraints are returned only for an allowed access, ordered by RangerConstraintResult.CONSTRAINT_RESULT_COMPARATOR + static List toConstraintResults(RangerAccessResult result) { + List ret = null; + + if (result != null && result.getIsAccessDetermined() && result.getIsAllowed() && CollectionUtils.isNotEmpty(result.getConstraints())) { + List constraints = new ArrayList<>(result.getConstraints()); + + constraints.sort(RangerConstraintResult.CONSTRAINT_RESULT_COMPARATOR); + + ret = new ArrayList<>(constraints.size()); + + for (RangerConstraintResult constraint : constraints) { + Map parameters = MapUtils.isEmpty(constraint.getParameters()) ? null : new HashMap<>(constraint.getParameters()); + PolicyInfo policy = new PolicyInfo(constraint.getPolicyId(), constraint.getPolicyVersion()); + + ret.add(new ConstraintResult(constraint.getConstraintName(), parameters, policy, constraint.getPolicyPriority())); + } + } + return ret; } diff --git a/authz-embedded/src/test/java/org/apache/ranger/authz/embedded/TestRangerAuthzPluginConstraints.java b/authz-embedded/src/test/java/org/apache/ranger/authz/embedded/TestRangerAuthzPluginConstraints.java new file mode 100644 index 00000000000..b4a391347b3 --- /dev/null +++ b/authz-embedded/src/test/java/org/apache/ranger/authz/embedded/TestRangerAuthzPluginConstraints.java @@ -0,0 +1,139 @@ +/* + * 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.authz.embedded; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.ranger.authz.model.RangerAuthzResult; +import org.apache.ranger.authz.model.RangerAuthzResult.AccessDecision; +import org.apache.ranger.authz.model.RangerAuthzResult.AccessResult; +import org.apache.ranger.authz.model.RangerAuthzResult.ConstraintResult; +import org.apache.ranger.authz.model.RangerAuthzResult.PermissionResult; +import org.apache.ranger.authz.model.RangerAuthzResult.PolicyInfo; +import org.apache.ranger.authz.model.RangerAuthzResult.ResultInfo; +import org.apache.ranger.plugin.model.RangerPolicy; +import org.apache.ranger.plugin.policyengine.RangerAccessResult; +import org.apache.ranger.plugin.policyengine.RangerConstraintResult; +import org.apache.ranger.plugin.policyresourcematcher.RangerPolicyResourceMatcher.MatchType; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class TestRangerAuthzPluginConstraints { + @Test + public void testAllowedResultReturnsSortedConstraints() { + RangerAccessResult result = accessResult(true); + + result.addConstraint(constraint("requireApproval", 30L, RangerPolicy.POLICY_PRIORITY_NORMAL)); + result.addConstraint(constraint("maxRows", 20L, RangerPolicy.POLICY_PRIORITY_NORMAL)); + result.addConstraint(constraint("requireApproval", 40L, RangerPolicy.POLICY_PRIORITY_OVERRIDE)); + + List constraints = RangerAuthzPlugin.toConstraintResults(result); + + assertEquals(3, constraints.size()); + + // name, then policy priority (OVERRIDE first), then policy id + assertEquals("maxRows", constraints.get(0).getName()); + assertEquals(new PolicyInfo(40L, 2L), constraints.get(1).getPolicy()); + assertEquals(Integer.valueOf(RangerPolicy.POLICY_PRIORITY_OVERRIDE), constraints.get(1).getPolicyPriority()); + assertEquals(new PolicyInfo(30L, 2L), constraints.get(2).getPolicy()); + assertEquals("finance-approvers", constraints.get(2).getParameters().get("approver")); + } + + @Test + public void testNoConstraintsWhenNoneMatched() { + assertNull(RangerAuthzPlugin.toConstraintResults(accessResult(true))); + } + + @Test + public void testNoConstraintsWhenDeniedOrNotDetermined() { + RangerAccessResult denied = accessResult(true); + + denied.addConstraint(constraint("requireApproval", 30L, RangerPolicy.POLICY_PRIORITY_NORMAL)); + denied.setIsAllowed(false); + + assertNull(RangerAuthzPlugin.toConstraintResults(denied)); + + RangerAccessResult notDetermined = new RangerAccessResult(RangerPolicy.POLICY_TYPE_ACCESS, "agent", null, null); + + notDetermined.addConstraint(constraint("requireApproval", 30L, RangerPolicy.POLICY_PRIORITY_NORMAL)); + + assertNull(RangerAuthzPlugin.toConstraintResults(notDetermined)); + assertNull(RangerAuthzPlugin.toConstraintResults(null)); + } + + @Test + public void testParametersAreCopied() { + RangerAccessResult result = accessResult(true); + + result.addConstraint(constraint("requireApproval", 30L, RangerPolicy.POLICY_PRIORITY_NORMAL)); + + ConstraintResult constraint = RangerAuthzPlugin.toConstraintResults(result).get(0); + + constraint.getParameters().put("approver", "changed"); + + assertEquals("finance-approvers", result.getConstraints().get(0).getParameters().get("approver")); + } + + @Test + public void testJsonRoundTrip() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + ResultInfo resultInfo = new ResultInfo(new AccessResult(AccessDecision.ALLOW, new PolicyInfo(12L, 3L)), null, null, null); + + resultInfo.setConstraints(Collections.singletonList(new ConstraintResult("requireApproval", Collections.singletonMap("approver", "finance-approvers"), new PolicyInfo(21L, 1L), 0))); + + Map permissions = new HashMap<>(); + + permissions.put("execute", new PermissionResult("execute", resultInfo)); + + RangerAuthzResult authzResult = new RangerAuthzResult("req-1", AccessDecision.ALLOW, permissions); + String json = mapper.writeValueAsString(authzResult); + + assertEquals(authzResult, mapper.readValue(json, RangerAuthzResult.class)); + + RangerAuthzResult noConstraints = new RangerAuthzResult("req-2", AccessDecision.ALLOW, Collections.singletonMap("execute", + new PermissionResult("execute", new ResultInfo(new AccessResult(AccessDecision.ALLOW, null), null, null, null)))); + + assertFalse(mapper.writeValueAsString(noConstraints).contains("constraints")); + } + + private static RangerAccessResult accessResult(boolean isAllowed) { + RangerAccessResult ret = new RangerAccessResult(RangerPolicy.POLICY_TYPE_ACCESS, "agent", null, null); + + ret.setIsAccessDetermined(true); + ret.setIsAllowed(isAllowed); + + return ret; + } + + private static RangerConstraintResult constraint(String name, long policyId, int policyPriority) { + Map parameters = new HashMap<>(); + + parameters.put("approver", "finance-approvers"); + + return new RangerConstraintResult(name, parameters, policyId, 2L, policyPriority, MatchType.SELF); + } +}