xds: Added a CEL-based Authorization Engine (#7191)

* xds: add a CEL-based authorization engine that uses the mock CEL library
This commit is contained in:
cindyxue 2020-08-13 16:08:35 -07:00 committed by GitHub
parent 6593fc8d35
commit cd0cc95553
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1177 additions and 0 deletions

View File

@ -0,0 +1,91 @@
/*
* Copyright 2020 The gRPC Authors
*
* Licensed 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 io.grpc.xds.internal.rbac.engine;
import com.google.common.collect.ImmutableList;
import java.lang.StringBuilder;
import java.util.List;
/**
* The AuthorizationDecision class holds authorization decision
* returned by CEL-based Authorization Engine.
*/
public class AuthorizationDecision {
/** Output represents the possible decisions generated by CEL-based Authorization Engine.*/
public enum Output {
/**
* ALLOW indicates that CEL Evaluate Engine
* had authorized the gRPC call and allowed the gRPC call to go through.
*/
ALLOW,
/**
* DENY indicates that CEL Evaluate Engine
* had authorized the gRPC call and denied the gRPC call from going through.
*/
DENY,
/**
* UNKNOWN indicates that CEL Evaluate Engine
* did not have enough information to authorize the gRPC call.
* */
UNKNOWN,
}
private final Output decision;
private final ImmutableList<String> policyNames;
/**
* Creates a new authorization decision using the input {@code decision}
* for resolving authorization decision
* and {@code policyNames} for resolving authorization context.
*/
public AuthorizationDecision(Output decision, List<String> policyNames) {
this.decision = decision;
this.policyNames = ImmutableList.copyOf(policyNames);
}
/** Returns the authorization decision. */
public Output getDecision() {
return this.decision;
}
/** Returns the policy list. */
public ImmutableList<String> getPolicyNames() {
return this.policyNames;
}
@Override
public String toString() {
StringBuilder authzStr = new StringBuilder();
switch (this.decision) {
case ALLOW:
authzStr.append("Authorization Decision: ALLOW. \n");
break;
case DENY:
authzStr.append("Authorization Decision: DENY. \n");
break;
case UNKNOWN:
authzStr.append("Authorization Decision: UNKNOWN. \n");
break;
default:
break;
}
for (String policyName : this.policyNames) {
authzStr.append(policyName + "; \n");
}
return authzStr.toString();
}
}

View File

@ -0,0 +1,214 @@
/*
* Copyright 2020 The gRPC Authors
*
* Licensed 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 io.grpc.xds.internal.rbac.engine;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.api.expr.v1alpha1.Expr;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.Descriptors.Descriptor;
import io.envoyproxy.envoy.config.rbac.v2.Policy;
import io.envoyproxy.envoy.config.rbac.v2.RBAC;
import io.envoyproxy.envoy.config.rbac.v2.RBAC.Action;
import io.grpc.xds.internal.rbac.engine.cel.Activation;
import io.grpc.xds.internal.rbac.engine.cel.DefaultDispatcher;
import io.grpc.xds.internal.rbac.engine.cel.DefaultInterpreter;
import io.grpc.xds.internal.rbac.engine.cel.DescriptorMessageProvider;
import io.grpc.xds.internal.rbac.engine.cel.Dispatcher;
import io.grpc.xds.internal.rbac.engine.cel.IncompleteData;
import io.grpc.xds.internal.rbac.engine.cel.Interpretable;
import io.grpc.xds.internal.rbac.engine.cel.Interpreter;
import io.grpc.xds.internal.rbac.engine.cel.InterpreterException;
import io.grpc.xds.internal.rbac.engine.cel.RuntimeTypeProvider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* CEL-based Authorization Engine is part of the authorization framework in gRPC.
* CEL-based Authorization Engine takes one or two Envoy RBAC policies as input
* and uses CEL library to evaluate the condition field
* inside each RBAC policy based on the provided Envoy Attributes.
* CEL-based Authorization Engine will generate an authorization decision which
* could be ALLOW, DENY or UNKNOWN.
*
* <p>Use as in:
*
* <pre>
* AuthorizationEngine engine = new AuthorizationEngine(rbacPolicy);
* AuthorizationDecision result = engine.evaluate(new EvaluateArgs(call, headers));
* </pre>
*/
public class AuthorizationEngine {
/**
* RbacEngine is an inner class that holds RBAC action
* and a list of conditions in RBAC policy.
*/
private static class RbacEngine {
@SuppressWarnings("UnusedVariable")
private final Action action;
private final ImmutableMap<String, Expr> conditions;
public RbacEngine(Action action, ImmutableMap<String, Expr> conditions) {
this.action = checkNotNull(action);
this.conditions = checkNotNull(conditions);
}
}
private static final Logger log = Logger.getLogger(AuthorizationEngine.class.getName());
private final RbacEngine allowEngine;
private final RbacEngine denyEngine;
/**
* Creates a CEL-based Authorization Engine from one Envoy RBAC policy.
* @param rbacPolicy input Envoy RBAC policy.
*/
public AuthorizationEngine(RBAC rbacPolicy) {
Map<String, Expr> conditions = new LinkedHashMap<>();
for (Map.Entry<String, Policy> policy: rbacPolicy.getPolicies().entrySet()) {
conditions.put(policy.getKey(), policy.getValue().getCondition());
}
allowEngine = (rbacPolicy.getAction() == Action.ALLOW)
? new RbacEngine(Action.ALLOW, ImmutableMap.copyOf(conditions)) : null;
denyEngine = (rbacPolicy.getAction() == Action.DENY)
? new RbacEngine(Action.DENY, ImmutableMap.copyOf(conditions)) : null;
}
/**
* Creates a CEL-based Authorization Engine from two Envoy RBAC policies.
* When it takes two RBAC policies,
* the order has to be a DENY policy followed by an ALLOW policy.
* @param denyPolicy input Envoy RBAC policy with DENY action.
* @param allowPolicy input Envoy RBAC policy with ALLOW action.
* @throws IllegalArgumentException if the user inputs an invalid RBAC list.
*/
public AuthorizationEngine(RBAC denyPolicy, RBAC allowPolicy) throws IllegalArgumentException {
checkArgument(
denyPolicy.getAction() == Action.DENY && allowPolicy.getAction() == Action.ALLOW,
"Invalid RBAC list, "
+ "must provide a RBAC with DENY action followed by a RBAC with ALLOW action. ");
Map<String, Expr> denyConditions = new LinkedHashMap<>();
for (Map.Entry<String, Policy> policy: denyPolicy.getPolicies().entrySet()) {
denyConditions.put(policy.getKey(), policy.getValue().getCondition());
}
denyEngine = new RbacEngine(Action.DENY, ImmutableMap.copyOf(denyConditions));
Map<String, Expr> allowConditions = new LinkedHashMap<>();
for (Map.Entry<String, Policy> policy: allowPolicy.getPolicies().entrySet()) {
allowConditions.put(policy.getKey(), policy.getValue().getCondition());
}
allowEngine = new RbacEngine(Action.ALLOW, ImmutableMap.copyOf(allowConditions));
}
/**
* The evaluate function performs the core authorization mechanism
* of CEL-based Authorization Engine.
* It determines whether a gRPC call is allowed, denied, or unable to be decided.
* @param args evaluate argument that is used to evaluate the RBAC conditions.
* @return an AuthorizationDecision generated by CEL-based Authorization Engine.
*/
public AuthorizationDecision evaluate(EvaluateArgs args) {
List<String> unknownPolicyNames = new ArrayList<>();
// Set up activation used in CEL library's eval function.
Activation activation = Activation.copyOf(args.generateEnvoyAttributes());
// Iterate through denyEngine's map.
// If there is match, immediately return DENY.
// If there are unknown conditions, return UNKNOWN.
// If all non-match, then iterate through allowEngine.
if (denyEngine != null) {
AuthorizationDecision authzDecision = evaluateEngine(denyEngine.conditions.entrySet(),
AuthorizationDecision.Output.DENY, unknownPolicyNames, activation);
if (authzDecision != null) {
return authzDecision;
}
if (unknownPolicyNames.size() > 0) {
return new AuthorizationDecision(
AuthorizationDecision.Output.UNKNOWN, unknownPolicyNames);
}
}
// Once we enter allowEngine, if there is a match, immediately return ALLOW.
// In the end of iteration, if there are unknown conditions, return UNKNOWN.
// If all non-match, return DENY.
if (allowEngine != null) {
AuthorizationDecision authzDecision = evaluateEngine(allowEngine.conditions.entrySet(),
AuthorizationDecision.Output.ALLOW, unknownPolicyNames, activation);
if (authzDecision != null) {
return authzDecision;
}
if (unknownPolicyNames.size() > 0) {
return new AuthorizationDecision(
AuthorizationDecision.Output.UNKNOWN, unknownPolicyNames);
}
}
// Return ALLOW if it only has a denyEngine and its unmatched.
if (this.allowEngine == null && this.denyEngine != null) {
return new AuthorizationDecision(
AuthorizationDecision.Output.ALLOW, new ArrayList<String>());
}
// Return DENY if none of denyEngine and allowEngine matched,
// or the single allowEngine is unmatched when there is only one allowEngine.
return new AuthorizationDecision(AuthorizationDecision.Output.DENY, new ArrayList<String>());
}
/** Evaluate a single RbacEngine. */
protected AuthorizationDecision evaluateEngine(Set<Map.Entry<String, Expr>> entrySet,
AuthorizationDecision.Output decision, List<String> unknownPolicyNames,
Activation activation) {
for (Map.Entry<String, Expr> condition : entrySet) {
try {
if (matches(condition.getValue(), activation)) {
return new AuthorizationDecision(decision,
new ArrayList<String>(Arrays.asList(new String[] {condition.getKey()})));
}
} catch (InterpreterException e) {
unknownPolicyNames.add(condition.getKey());
}
}
return null;
}
/** Evaluate if a condition matches the given Enovy Attributes using CEL library. */
protected boolean matches(Expr condition, Activation activation) throws InterpreterException {
// Set up interpretable used in CEL library's eval function.
List<Descriptor> descriptors = new ArrayList<>();
RuntimeTypeProvider messageProvider = DescriptorMessageProvider.dynamicMessages(descriptors);
Dispatcher dispatcher = DefaultDispatcher.create();
Interpreter interpreter = new DefaultInterpreter(messageProvider, dispatcher);
Interpretable interpretable = interpreter.createInterpretable(condition);
// Parse the generated result object to a boolean variable.
try {
Object result = interpretable.eval(activation);
if (result instanceof Boolean) {
return Boolean.valueOf(result.toString());
}
// Throw an InterpreterException if there are missing Envoy Attributes.
if (result instanceof IncompleteData) {
throw new InterpreterException.Builder("Envoy Attributes gotten are incomplete.").build();
}
} catch (InterpreterException e) {
// If any InterpreterExceptions are catched, throw it and log the error.
log.log(Level.WARNING, e.toString(), e);
throw e;
}
return false;
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright 2020 The gRPC Authors
*
* Licensed 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 io.grpc.xds.internal.rbac.engine;
import com.google.common.collect.ImmutableMap;
import io.grpc.Grpc;
import io.grpc.Metadata;
import io.grpc.ServerCall;
/** The EvaluateArgs class holds evaluate arguments used in CEL-based Authorization Engine. */
public class EvaluateArgs {
private Metadata headers;
private ServerCall<?, ?> call;
/**
* Creates a new EvaluateArgs using the input {@code headers} for resolving headers
* and {@code call} for resolving gRPC call.
*/
public EvaluateArgs(Metadata headers, ServerCall<?, ?> call) {
this.headers = headers;
this.call = call;
}
/** Extract the request.url_path field. */
protected String getRequestUrlPath() {
String requestUrlPath = this.call.getMethodDescriptor().getFullMethodName();
return requestUrlPath;
}
/** Extract the request.host field. */
protected String getRequestHost() {
String requestHost = this.call.getAuthority();
return requestHost;
}
/** Extract the request.method field. */
protected String getRequestMethod() {
// TODO(@zhenlian): confirm extraction for request.method.
String requestMethod = this.call.getMethodDescriptor().getServiceName();
return requestMethod;
}
/** Extract the request.headers field. */
protected Metadata getRequestHeaders() {
// TODO(@zhenlian): convert request.headers from Metadata to a String Map.
Metadata requestHeaders = this.headers;
return requestHeaders;
}
/** Extract the source.address field. */
protected String getSourceAddress() {
String sourceAddress =
this.call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR).toString();
return sourceAddress;
}
/** Extract the source.port field. */
protected int getSourcePort() {
// TODO(@zhenlian): fill out extraction for source.port.
int sourcePort = 0;
return sourcePort;
}
/** Extract the destination.address field. */
protected String getDestinationAddress() {
String destinationAddress =
this.call.getAttributes().get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR).toString();
return destinationAddress;
}
/** Extract the destination.port field. */
protected int getDestinationPort() {
// TODO(@zhenlian): fill out extraction for destination.port.
int destinationPort = 0;
return destinationPort;
}
/** Extract the connection.uri_san_peer_certificate field. */
protected String getConnectionUriSanPeerCertificate() {
// TODO(@zhenlian): fill out extraction for connection.uri_san_peer_certificate.
String connectionUriSanPeerCertificate = "placeholder";
return connectionUriSanPeerCertificate;
}
/** Extract the source.principal field. */
protected String getSourcePrincipal() {
// TODO(@zhenlian): fill out extraction for source.principal.
String sourcePrincipal = "placeholder";
return sourcePrincipal;
}
/** Extract Envoy Attributes from EvaluateArgs. */
public ImmutableMap<String, Object> generateEnvoyAttributes() {
ImmutableMap<String, Object> attributes = ImmutableMap.<String, Object>builder()
.put("request.url_path", this.getRequestUrlPath())
.put("request.host", this.getRequestHost())
.put("request.method", this.getRequestMethod())
.put("request.headers", this.getRequestHeaders())
.put("source.address", this.getSourceAddress())
.put("source.port", this.getSourcePort())
.put("destination.address", this.getDestinationAddress())
.put("destination.port", this.getDestinationPort())
.put("connection.uri_san_peer_certificate",
this.getConnectionUriSanPeerCertificate())
.put("source.principal", this.getSourcePrincipal())
.build();
return attributes;
}
}

View File

@ -0,0 +1,475 @@
/*
* Copyright 2020 The gRPC Authors
*
* Licensed 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 io.grpc.xds.internal.rbac.engine;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import com.google.api.expr.v1alpha1.Expr;
import com.google.api.expr.v1alpha1.Expr.Ident;
import com.google.common.collect.ImmutableMap;
import io.envoyproxy.envoy.config.rbac.v2.Policy;
import io.envoyproxy.envoy.config.rbac.v2.RBAC;
import io.envoyproxy.envoy.config.rbac.v2.RBAC.Action;
import io.grpc.xds.internal.rbac.engine.cel.Activation;
import io.grpc.xds.internal.rbac.engine.cel.InterpreterException;
import java.lang.StringBuilder;
import java.util.Map;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Unit tests for evaluate function of CEL Evaluation Engine. */
@RunWith(JUnit4.class)
public class AuthzEngineEvaluationTest {
@Rule
public final MockitoRule mocks = MockitoJUnit.rule();
@Mock
private EvaluateArgs args;
@Mock
private Activation activation;
@Mock
private Map<String, Object> attributes;
private AuthorizationEngine engine;
private AuthorizationEngine spyEngine;
private AuthorizationDecision evaluateResult;
// Mock RBAC engine with ALLOW action.
private RBAC rbacAllow;
// Mock RBAC engine with DENY action.
private RBAC rbacDeny;
// Mock policies that will be used to construct RBAC Engine.
private Policy policy1;
private Policy policy2;
private Policy policy3;
private Policy policy4;
private Policy policy5;
private Policy policy6;
// Mock conditions that will be used to construct RBAC poilcies.
private Expr condition1;
private Expr condition2;
private Expr condition3;
private Expr condition4;
private Expr condition5;
private Expr condition6;
@Before
public void buildRbac() {
// Set up RBAC conditions.
condition1 = Expr.newBuilder()
.setIdentExpr(Ident.newBuilder().setName("Condition 1").build())
.build();
condition2 = Expr.newBuilder()
.setIdentExpr(Ident.newBuilder().setName("Condition 2").build())
.build();
condition3 = Expr.newBuilder()
.setIdentExpr(Ident.newBuilder().setName("Condition 3").build())
.build();
condition4 = Expr.newBuilder()
.setIdentExpr(Ident.newBuilder().setName("Condition 4").build())
.build();
condition5 = Expr.newBuilder()
.setIdentExpr(Ident.newBuilder().setName("Condition 5").build())
.build();
condition6 = Expr.newBuilder()
.setIdentExpr(Ident.newBuilder().setName("Condition 6").build())
.build();
// Set up RBAC policies.
policy1 = Policy.newBuilder().setCondition(condition1).build();
policy2 = Policy.newBuilder().setCondition(condition2).build();
policy3 = Policy.newBuilder().setCondition(condition3).build();
policy4 = Policy.newBuilder().setCondition(condition4).build();
policy5 = Policy.newBuilder().setCondition(condition5).build();
policy6 = Policy.newBuilder().setCondition(condition6).build();
// Set up RBACs.
rbacAllow = RBAC.newBuilder()
.setAction(Action.ALLOW)
.putPolicies("Policy 1", policy1)
.putPolicies("Policy 2", policy2)
.putPolicies("Policy 3", policy3)
.build();
rbacDeny = RBAC.newBuilder()
.setAction(Action.DENY)
.putPolicies("Policy 4", policy4)
.putPolicies("Policy 5", policy5)
.putPolicies("Policy 6", policy6)
.build();
}
/** Build an ALLOW engine from Policy 1, 2, 3. */
@Before
public void setupEngineSingleRbacAllow() {
buildRbac();
engine = new AuthorizationEngine(rbacAllow);
spyEngine = Mockito.spy(engine);
doReturn(ImmutableMap.copyOf(attributes)).when(args).generateEnvoyAttributes();
}
/** Build a DENY engine from Policy 4, 5, 6. */
@Before
public void setupEngineSingleRbacDeny() {
buildRbac();
engine = new AuthorizationEngine(rbacDeny);
spyEngine = Mockito.spy(engine);
doReturn(ImmutableMap.copyOf(attributes)).when(args).generateEnvoyAttributes();
}
/** Build a pair of engines with a DENY engine followed by an ALLOW engine. */
@Before
public void setupEngineRbacPair() {
buildRbac();
engine = new AuthorizationEngine(rbacDeny, rbacAllow);
spyEngine = Mockito.spy(engine);
doReturn(ImmutableMap.copyOf(attributes)).when(args).generateEnvoyAttributes();
}
/**
* Test on the ALLOW engine.
* The evaluation result of all the CEL expressions is set to true,
* so the gRPC authorization returns ALLOW.
*/
@Test
public void testAllowEngineWithAllMatchedPolicies() throws InterpreterException {
setupEngineSingleRbacAllow();
// Policy 1 - matched; Policy 2 - matched; Policy 3 - matched
doReturn(true).when(spyEngine).matches(eq(condition1), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition2), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition3), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.ALLOW);
assertEquals(evaluateResult.getPolicyNames().size(), 1);
assertTrue(evaluateResult.getPolicyNames().contains("Policy 1"));
}
/**
* Test on the ALLOW engine.
* The evaluation result of all the CEL expressions is set to false,
* so the gRPC authorization returns DENY.
*/
@Test
public void testAllowEngineWithAllUnmatchedPolicies() throws InterpreterException {
setupEngineSingleRbacAllow();
// Policy 1 - unmatched; Policy 2 - unmatched; Policy 3 - unmatched
doReturn(false).when(spyEngine).matches(eq(condition1), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition2), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition3), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.DENY);
assertEquals(evaluateResult.getPolicyNames().size(), 0);
assertEquals(evaluateResult.toString(),
new StringBuilder("Authorization Decision: DENY. \n").toString());
}
/**
* Test on the ALLOW engine.
* The evaluation result of two CEL expressions is set to true,
* and the evaluation result of one CEL expression is set to false,
* so the gRPC authorization returns ALLOW.
*/
@Test
public void testAllowEngineWithMatchedAndUnmatchedPolicies()
throws InterpreterException {
setupEngineSingleRbacAllow();
// Policy 1 - unmatched; Policy 2 - matched; Policy 3 - matched
doReturn(false).when(spyEngine).matches(eq(condition1), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition2), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition3), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.ALLOW);
assertEquals(evaluateResult.getPolicyNames().size(), 1);
assertTrue(evaluateResult.getPolicyNames().contains("Policy 2"));
}
/**
* Test on the ALLOW engine.
* The evaluation result of one CEL expression is set to unknown,
* so the gRPC authorization returns UNKNOWN.
*/
@Test
public void testAllowEngineWithUnknownAndUnmatchedPolicies()
throws InterpreterException {
setupEngineSingleRbacAllow();
// Policy 1 - unmatched; Policy 2 - unknown; Policy 3 - unknown
doReturn(false).when(spyEngine).matches(eq(condition1), any(Activation.class));
doThrow(new InterpreterException.Builder("Unknown result").build())
.when(spyEngine).matches(eq(condition2), any(Activation.class));
doThrow(new InterpreterException.Builder("Unknown result").build())
.when(spyEngine).matches(eq(condition3), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.UNKNOWN);
assertEquals(evaluateResult.getPolicyNames().size(), 2);
assertTrue(evaluateResult.getPolicyNames().contains("Policy 2"));
assertTrue(evaluateResult.getPolicyNames().contains("Policy 3"));
assertEquals(evaluateResult.toString(),
new StringBuilder("Authorization Decision: UNKNOWN. \n"
+ "Policy 2; \n" + "Policy 3; \n").toString());
}
/**
* Test on the ALLOW engine.
* The evaluation result of one CEL expression is set to unknown,
* so the gRPC authorization returns UNKNOWN.
*/
@Test
public void testAllowEngineWithMatchedUnmatchedAndUnknownPolicies()
throws InterpreterException {
setupEngineSingleRbacAllow();
// Policy 1 - unmatched; Policy 2 - matched; Policy 3 - unknown
doReturn(false).when(spyEngine).matches(eq(condition1), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition2), any(Activation.class));
doThrow(new InterpreterException.Builder("Unknown result").build())
.when(spyEngine).matches(eq(condition3), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.ALLOW);
assertEquals(evaluateResult.getPolicyNames().size(), 1);
assertTrue(evaluateResult.getPolicyNames().contains("Policy 2"));
assertEquals(evaluateResult.toString(),
new StringBuilder("Authorization Decision: ALLOW. \n" + "Policy 2; \n").toString());
}
/**
* Test on the DENY engine.
* The evaluation result of all the CEL expressions is set to true,
* so the gRPC authorization returns DENY.
*/
@Test
public void testDenyEngineWithAllMatchedPolicies() throws InterpreterException {
setupEngineSingleRbacDeny();
// Policy 4 - matched; Policy 5 - matched; Policy 6 - matched
doReturn(true).when(spyEngine).matches(eq(condition4), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition5), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition6), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.DENY);
assertEquals(evaluateResult.getPolicyNames().size(), 1);
assertTrue(evaluateResult.getPolicyNames().contains("Policy 4"));
}
/**
* Test on the DENY engine.
* The evaluation result of all the CEL expressions is set to false,
* so the gRPC authorization returns ALLOW.
*/
@Test
public void testDenyEngineWithAllUnmatchedPolicies() throws InterpreterException {
setupEngineSingleRbacDeny();
// Policy 4 - unmatched; Policy 5 - unmatched; Policy 6 - unmatched
doReturn(false).when(spyEngine).matches(eq(condition4), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition5), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition6), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.ALLOW);
assertEquals(evaluateResult.getPolicyNames().size(), 0);
}
/**
* Test on the DENY engine.
* The evaluation result of two CEL expressions is set to true,
* and the evaluation result of one CEL expression is set to false,
* so the gRPC authorization returns DENY.
*/
@Test
public void testDenyEngineWithMatchedAndUnmatchedPolicies()
throws InterpreterException {
setupEngineSingleRbacDeny();
// Policy 4 - unmatched; Policy 5 - matched; Policy 6 - matched
doReturn(false).when(spyEngine).matches(eq(condition4), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition5), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition6), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.DENY);
assertEquals(evaluateResult.getPolicyNames().size(), 1);
assertTrue(evaluateResult.getPolicyNames().contains("Policy 5"));
}
/**
* Test on the DENY engine.
* The evaluation result of one CEL expression is set to unknown,
* so the gRPC authorization returns UNKNOWN.
*/
@Test
public void testDenyEngineWithUnknownAndUnmatchedPolicies()
throws InterpreterException {
setupEngineSingleRbacDeny();
// Policy 4 - unmatched; Policy 5 - unknown; Policy 6 - unknown
doReturn(false).when(spyEngine).matches(eq(condition4), any(Activation.class));
doThrow(new InterpreterException.Builder("Unknown result").build())
.when(spyEngine).matches(eq(condition5), any(Activation.class));
doThrow(new InterpreterException.Builder("Unknown result").build())
.when(spyEngine).matches(eq(condition6), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.UNKNOWN);
assertEquals(evaluateResult.getPolicyNames().size(), 2);
assertTrue(evaluateResult.getPolicyNames().contains("Policy 5"));
assertTrue(evaluateResult.getPolicyNames().contains("Policy 6"));
}
/**
* Test on the DENY engine.
* The evaluation result of one CEL expression is set to unknown,
* so the gRPC authorization returns UNKNOWN.
*/
@Test
public void testDenyEngineWithMatchedUnmatchedAndUnknownPolicies()
throws InterpreterException {
setupEngineSingleRbacDeny();
// Policy 4 - unmatched; Policy 5 - matched; Policy 6 - unknown
doReturn(false).when(spyEngine).matches(eq(condition4), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition5), any(Activation.class));
doThrow(new InterpreterException.Builder("Unknown result").build())
.when(spyEngine).matches(eq(condition6), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.DENY);
assertEquals(evaluateResult.getPolicyNames().size(), 1);
assertTrue(evaluateResult.getPolicyNames().contains("Policy 5"));
}
/**
* Test on the DENY engine and ALLOW engine pair.
* The evaluation result of all the CEL expressions is set to true in DENY engine,
* so the gRPC authorization returns DENY.
*/
@Test
public void testEnginePairWithAllMatchedDenyEngine() throws InterpreterException {
setupEngineRbacPair();
// Policy 4 - matched; Policy 5 - matched; Policy 6 - matched
// Policy 1 - matched; Policy 2 - matched; Policy 3 - matched
doReturn(true).when(spyEngine).matches(eq(condition1), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition2), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition3), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition4), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition5), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition6), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.DENY);
assertEquals(evaluateResult.getPolicyNames().size(), 1);
assertTrue(evaluateResult.getPolicyNames().contains("Policy 4"));
}
/**
* Test on the DENY engine and ALLOW engine pair.
* The evaluation result of two CEL expressions is set to true,
* and the evaluation result of one CEL expression is set to false in DENY engine,
* so the gRPC authorization returns DENY.
*/
@Test
public void testEnginePairWithPartiallyMatchedDenyEngine()
throws InterpreterException {
setupEngineRbacPair();
// Policy 4 - unmatched; Policy 5 - matched; Policy 6 - unknown
// Policy 1 - matched; Policy 2 - matched; Policy 3 - matched
doReturn(true).when(spyEngine).matches(eq(condition1), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition2), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition3), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition4), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition5), any(Activation.class));
doThrow(new InterpreterException.Builder("Unknown result").build())
.when(spyEngine).matches(eq(condition6), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.DENY);
assertEquals(evaluateResult.getPolicyNames().size(), 1);
assertTrue(evaluateResult.getPolicyNames().contains("Policy 5"));
}
/**
* Test on the DENY engine and ALLOW engine pair.
* The DENY engine has unknown policies, so the gRPC authorization returns UNKNOWN.
*/
@Test
public void testEnginePairWithUnknownDenyEngine() throws InterpreterException {
setupEngineRbacPair();
// Policy 4 - unmatched; Policy 5 - unknown; Policy 6 - unknown
// Policy 1 - matched; Policy 2 - matched; Policy 3 - matched
doReturn(true).when(spyEngine).matches(eq(condition1), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition2), any(Activation.class));
doReturn(true).when(spyEngine).matches(eq(condition3), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition4), any(Activation.class));
doThrow(new InterpreterException.Builder("Unknown result").build())
.when(spyEngine).matches(eq(condition5), any(Activation.class));
doThrow(new InterpreterException.Builder("Unknown result").build())
.when(spyEngine).matches(eq(condition6), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.UNKNOWN);
assertEquals(evaluateResult.getPolicyNames().size(), 2);
assertTrue(evaluateResult.getPolicyNames().contains("Policy 5"));
assertTrue(evaluateResult.getPolicyNames().contains("Policy 6"));
}
/**
* Test on the DENY engine and ALLOW engine pair.
* The evaluation result of all the CEL expressions is set to false in DENY engine,
* and the ALLOW engine has unknown policies,
* so the gRPC authorization returns UNKNOWN.
*/
@Test
public void testEnginePairWithUnmatchedDenyEngineAndUnknownAllowEngine()
throws InterpreterException {
setupEngineRbacPair();
// Policy 4 - unmatched; Policy 5 - unmatched; Policy 6 - unmatched
// Policy 1 - unmatched; Policy 2 - unknown; Policy 3 - unknown
doReturn(false).when(spyEngine).matches(eq(condition1), any(Activation.class));
doThrow(new InterpreterException.Builder("Unknown result").build())
.when(spyEngine).matches(eq(condition2), any(Activation.class));
doThrow(new InterpreterException.Builder("Unknown result").build())
.when(spyEngine).matches(eq(condition3), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition4), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition5), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition6), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.UNKNOWN);
assertEquals(evaluateResult.getPolicyNames().size(), 2);
assertTrue(evaluateResult.getPolicyNames().contains("Policy 2"));
assertTrue(evaluateResult.getPolicyNames().contains("Policy 3"));
}
/**
* Test on the DENY engine and ALLOW engine pair.
* The evaluation result of all the CEL expressions is set to false in both engines,
* so the gRPC authorization returns DENY.
*/
@Test
public void testUnmatchedEnginePair() throws InterpreterException {
setupEngineRbacPair();
// Policy 4 - unmatched; Policy 5 - unmatched; Policy 6 - unmatched
// Policy 1 - unmatched; Policy 2 - unmatched; Policy 3 - unmatched
doReturn(false).when(spyEngine).matches(eq(condition1), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition2), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition3), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition4), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition5), any(Activation.class));
doReturn(false).when(spyEngine).matches(eq(condition6), any(Activation.class));
evaluateResult = spyEngine.evaluate(args);
assertEquals(evaluateResult.getDecision(), AuthorizationDecision.Output.DENY);
assertEquals(evaluateResult.getPolicyNames().size(), 0);
}
}

View File

@ -0,0 +1,141 @@
/*
* Copyright 2020 The gRPC Authors
*
* Licensed 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 io.grpc.xds.internal.rbac.engine;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import com.google.api.expr.v1alpha1.Expr;
import io.envoyproxy.envoy.config.rbac.v2.RBAC;
import io.envoyproxy.envoy.config.rbac.v2.RBAC.Action;
import io.grpc.xds.internal.rbac.engine.cel.Activation;
import io.grpc.xds.internal.rbac.engine.cel.Dispatcher;
import io.grpc.xds.internal.rbac.engine.cel.Interpretable;
import io.grpc.xds.internal.rbac.engine.cel.Interpreter;
import io.grpc.xds.internal.rbac.engine.cel.InterpreterException;
import io.grpc.xds.internal.rbac.engine.cel.RuntimeTypeProvider;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Unit tests for constructor of CEL-based Authorization Engine. */
@RunWith(JUnit4.class)
public class AuthzEngineTest {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Rule
public final MockitoRule mocks = MockitoJUnit.rule();
@Mock
private Activation activation;
@Mock
private RuntimeTypeProvider messageProvider;
@Mock
private Dispatcher dispatcher;
@Mock
private Interpreter interpreter;
@Mock
private Interpretable interpretable;
private AuthorizationEngine engine;
private RBAC rbacDeny;
private RBAC rbacAllow;
private Expr expr;
private Object result;
@Before
public void setup() {
rbacAllow = RBAC.newBuilder()
.setAction(Action.ALLOW)
.build();
rbacDeny = RBAC.newBuilder()
.setAction(Action.DENY)
.build();
}
@Test
public void createEngineAllowPolicy() {
engine = new AuthorizationEngine(rbacAllow);
assertNotNull(engine);
}
@Test
public void createEngineDenyPolicy() {
engine = new AuthorizationEngine(rbacDeny);
assertNotNull(engine);
}
@Test
public void createEngineDenyAllowPolicies() {
engine = new AuthorizationEngine(rbacDeny, rbacAllow);
assertNotNull(engine);
}
@Test
public void failToCreateEngineIfRbacPairOfAllowAllow() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Invalid RBAC list, "
+ "must provide a RBAC with DENY action followed by a RBAC with ALLOW action. ");
engine = new AuthorizationEngine(rbacAllow, rbacAllow);
assertNull(engine);
}
@Test
public void failToCreateEngineIfRbacPairOfAllowDeny() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Invalid RBAC list, "
+ "must provide a RBAC with DENY action followed by a RBAC with ALLOW action. ");
engine = new AuthorizationEngine(rbacAllow, rbacDeny);
assertNull(engine);
}
@Test
public void failToCreateEngineIfRbacPairOfDenyDeny() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Invalid RBAC list, "
+ "must provide a RBAC with DENY action followed by a RBAC with ALLOW action. ");
engine = new AuthorizationEngine(rbacDeny, rbacDeny);
assertNull(engine);
}
@Test
public void testCelInterface() throws InterpreterException {
engine = new AuthorizationEngine(rbacAllow);
when(interpretable.eval(any(Activation.class))).thenReturn(true);
expr = Expr.newBuilder().build();
result = engine.matches(expr, activation);
assertThat(messageProvider).isNotNull();
assertThat(dispatcher).isNotNull();
assertThat(interpreter).isNotNull();
assertThat(activation).isNotNull();
assertThat(result).isNotNull();
}
}

View File

@ -0,0 +1,133 @@
/*
* Copyright 2020 The gRPC Authors
*
* Licensed 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 io.grpc.xds.internal.rbac.engine;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import io.grpc.Attributes;
import io.grpc.Grpc;
import io.grpc.Metadata;
import io.grpc.SecurityLevel;
import io.grpc.ServerCall;
import io.grpc.internal.GrpcAttributes;
import io.netty.channel.local.LocalAddress;
import java.net.SocketAddress;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Unit tests for evaluate argument. */
@RunWith(JUnit4.class)
public class EvaluateArgsTest<ReqT,RespT> {
@Rule
public final MockitoRule mocks = MockitoJUnit.rule();
@Mock
private ServerCall<ReqT,RespT> call;
private EvaluateArgs args;
private EvaluateArgs spyArgs;
private Metadata metadata;
private ImmutableMap<String, Object> attributesMap;
@Before
public void setup() {
// Set up metadata.
metadata = new Metadata();
// Set up spyArgs.
args = new EvaluateArgs(metadata, call);
spyArgs = Mockito.spy(args);
// Set up attributes map.
attributesMap = ImmutableMap.<String, Object>builder()
.put("request.url_path", "package.service/method")
.put("request.host", "fooapi.googleapis.com")
.put("request.method", "GET")
.put("request.headers", metadata)
.put("source.address", "1.2.3.4")
.put("source.port", 5050)
.put("destination.address", "4.3.2.1")
.put("destination.port", 8080)
.put("connection.uri_san_peer_certificate", "foo")
.put("source.principal", "spiffe")
.build();
// Set up evaluate args.
doReturn("package.service/method").when(spyArgs).getRequestUrlPath();
doReturn("fooapi.googleapis.com").when(spyArgs).getRequestHost();
doReturn("GET").when(spyArgs).getRequestMethod();
doReturn(metadata).when(spyArgs).getRequestHeaders();
doReturn("1.2.3.4").when(spyArgs).getSourceAddress();
doReturn(5050).when(spyArgs).getSourcePort();
doReturn("4.3.2.1").when(spyArgs).getDestinationAddress();
doReturn(8080).when(spyArgs).getDestinationPort();
doReturn("foo").when(spyArgs).getConnectionUriSanPeerCertificate();
doReturn("spiffe").when(spyArgs).getSourcePrincipal();
}
@Test
public void testGenerateEnvoyAttributes() {
setup();
ImmutableMap<String, Object> attributes = spyArgs.generateEnvoyAttributes();
assertEquals(attributesMap, attributes);
verify(spyArgs, times(1)).getRequestUrlPath();
verify(spyArgs, times(1)).getRequestHost();
verify(spyArgs, times(1)).getRequestMethod();
verify(spyArgs, times(1)).getRequestHeaders();
verify(spyArgs, times(1)).getSourceAddress();
verify(spyArgs, times(1)).getSourcePort();
verify(spyArgs, times(1)).getDestinationAddress();
verify(spyArgs, times(1)).getDestinationPort();
verify(spyArgs, times(1)).getConnectionUriSanPeerCertificate();
verify(spyArgs, times(1)).getSourcePrincipal();
}
@Test
public void testEvaluateArgsAccessorFunctions() {
// Set up args and call.
args = new EvaluateArgs(new Metadata(), call);
SocketAddress localAddr = new LocalAddress("local_addr");
SocketAddress remoteAddr = new LocalAddress("remote_addr");
Attributes attrs = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, localAddr)
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, remoteAddr)
.set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.NONE)
.build();
when(call.getAttributes()).thenReturn(attrs);
when(call.getAuthority()).thenReturn("fooapi.googleapis.com");
// Check the behavior of accessor functions.
assertEquals(args.getRequestHost(), "fooapi.googleapis.com");
assertNotNull(args.getRequestHeaders());
assertEquals(args.getSourcePort(), 0);
assertEquals(args.getDestinationPort(), 0);
assertEquals(args.getSourceAddress(), "local:remote_addr");
assertEquals(args.getDestinationAddress(), "local:local_addr");
assertEquals(args.getConnectionUriSanPeerCertificate(), "placeholder");
assertEquals(args.getSourcePrincipal(), "placeholder");
}
}