mirror of https://github.com/grpc/grpc-java.git
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:
parent
6593fc8d35
commit
cd0cc95553
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 it’s 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue