diff --git a/xds/src/main/java/io/grpc/xds/internal/rbac/engine/AuthorizationDecision.java b/xds/src/main/java/io/grpc/xds/internal/rbac/engine/AuthorizationDecision.java new file mode 100644 index 0000000000..474ff7c180 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/rbac/engine/AuthorizationDecision.java @@ -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 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 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 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(); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/rbac/engine/AuthorizationEngine.java b/xds/src/main/java/io/grpc/xds/internal/rbac/engine/AuthorizationEngine.java new file mode 100644 index 0000000000..bb3005883f --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/rbac/engine/AuthorizationEngine.java @@ -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. + * + *

Use as in: + * + *

+ *  AuthorizationEngine engine = new AuthorizationEngine(rbacPolicy);
+ *  AuthorizationDecision result = engine.evaluate(new EvaluateArgs(call, headers));
+ * 
+ */ +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 conditions; + + public RbacEngine(Action action, ImmutableMap 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 conditions = new LinkedHashMap<>(); + for (Map.Entry 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 denyConditions = new LinkedHashMap<>(); + for (Map.Entry policy: denyPolicy.getPolicies().entrySet()) { + denyConditions.put(policy.getKey(), policy.getValue().getCondition()); + } + denyEngine = new RbacEngine(Action.DENY, ImmutableMap.copyOf(denyConditions)); + Map allowConditions = new LinkedHashMap<>(); + for (Map.Entry 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 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()); + } + // 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()); + } + + /** Evaluate a single RbacEngine. */ + protected AuthorizationDecision evaluateEngine(Set> entrySet, + AuthorizationDecision.Output decision, List unknownPolicyNames, + Activation activation) { + for (Map.Entry condition : entrySet) { + try { + if (matches(condition.getValue(), activation)) { + return new AuthorizationDecision(decision, + new ArrayList(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 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; + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/rbac/engine/EvaluateArgs.java b/xds/src/main/java/io/grpc/xds/internal/rbac/engine/EvaluateArgs.java new file mode 100644 index 0000000000..e8ed1828a2 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/rbac/engine/EvaluateArgs.java @@ -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 generateEnvoyAttributes() { + ImmutableMap attributes = ImmutableMap.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; + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/rbac/engine/AuthzEngineEvaluationTest.java b/xds/src/test/java/io/grpc/xds/internal/rbac/engine/AuthzEngineEvaluationTest.java new file mode 100644 index 0000000000..61172d576c --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/rbac/engine/AuthzEngineEvaluationTest.java @@ -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 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); + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/rbac/engine/AuthzEngineTest.java b/xds/src/test/java/io/grpc/xds/internal/rbac/engine/AuthzEngineTest.java new file mode 100644 index 0000000000..17407d3ce1 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/rbac/engine/AuthzEngineTest.java @@ -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(); + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/rbac/engine/EvaluateArgsTest.java b/xds/src/test/java/io/grpc/xds/internal/rbac/engine/EvaluateArgsTest.java new file mode 100644 index 0000000000..3fa09bda45 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/rbac/engine/EvaluateArgsTest.java @@ -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 { + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + + @Mock + private ServerCall call; + + private EvaluateArgs args; + private EvaluateArgs spyArgs; + + private Metadata metadata; + private ImmutableMap 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.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 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"); + } +}