1 /* 2 * Copyright 2021 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.xds; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static org.mockito.ArgumentMatchers.any; 21 import static org.mockito.ArgumentMatchers.eq; 22 import static org.mockito.Mockito.mock; 23 import static org.mockito.Mockito.never; 24 import static org.mockito.Mockito.verify; 25 import static org.mockito.Mockito.verifyNoMoreInteractions; 26 import static org.mockito.Mockito.when; 27 28 import com.google.api.expr.v1alpha1.Expr; 29 import com.google.protobuf.Any; 30 import com.google.protobuf.Message; 31 import com.google.protobuf.UInt32Value; 32 import io.envoyproxy.envoy.config.core.v3.CidrRange; 33 import io.envoyproxy.envoy.config.rbac.v3.Permission; 34 import io.envoyproxy.envoy.config.rbac.v3.Policy; 35 import io.envoyproxy.envoy.config.rbac.v3.Principal; 36 import io.envoyproxy.envoy.config.rbac.v3.Principal.Authenticated; 37 import io.envoyproxy.envoy.config.rbac.v3.RBAC; 38 import io.envoyproxy.envoy.config.rbac.v3.RBAC.Action; 39 import io.envoyproxy.envoy.config.route.v3.HeaderMatcher; 40 import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBACPerRoute; 41 import io.envoyproxy.envoy.type.matcher.v3.MetadataMatcher; 42 import io.envoyproxy.envoy.type.matcher.v3.PathMatcher; 43 import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; 44 import io.envoyproxy.envoy.type.v3.Int32Range; 45 import io.grpc.Attributes; 46 import io.grpc.Grpc; 47 import io.grpc.Metadata; 48 import io.grpc.MethodDescriptor; 49 import io.grpc.MethodDescriptor.MethodType; 50 import io.grpc.ServerCall; 51 import io.grpc.ServerCallHandler; 52 import io.grpc.ServerInterceptor; 53 import io.grpc.Status; 54 import io.grpc.testing.TestMethodDescriptors; 55 import io.grpc.xds.Filter.FilterConfig; 56 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine; 57 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AlwaysTrueMatcher; 58 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthConfig; 59 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthDecision; 60 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationPortMatcher; 61 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.OrMatcher; 62 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.PolicyMatcher; 63 import java.net.InetSocketAddress; 64 import java.security.cert.X509Certificate; 65 import java.util.Arrays; 66 import java.util.Collections; 67 import java.util.List; 68 import javax.net.ssl.SSLSession; 69 import org.junit.Test; 70 import org.junit.runner.RunWith; 71 import org.junit.runners.JUnit4; 72 import org.mockito.ArgumentCaptor; 73 74 /** Tests for {@link RbacFilter}. */ 75 @RunWith(JUnit4.class) 76 public class RbacFilterTest { 77 private static final String PATH = "auth"; 78 private static final StringMatcher STRING_MATCHER = 79 StringMatcher.newBuilder().setExact("/" + PATH).setIgnoreCase(true).build(); 80 81 @Test 82 @SuppressWarnings({"unchecked", "deprecation"}) ipPortParser()83 public void ipPortParser() { 84 CidrRange cidrRange = CidrRange.newBuilder().setAddressPrefix("10.10.10.0") 85 .setPrefixLen(UInt32Value.of(24)).build(); 86 List<Permission> permissionList = Arrays.asList( 87 Permission.newBuilder().setAndRules(Permission.Set.newBuilder() 88 .addRules(Permission.newBuilder().setDestinationIp(cidrRange).build()) 89 .addRules(Permission.newBuilder().setDestinationPort(9090).build()).build() 90 ).build()); 91 List<Principal> principalList = Arrays.asList( 92 Principal.newBuilder().setAndIds(Principal.Set.newBuilder() 93 .addIds(Principal.newBuilder().setDirectRemoteIp(cidrRange).build()) 94 .addIds(Principal.newBuilder().setRemoteIp(cidrRange).build()) 95 .addIds(Principal.newBuilder().setSourceIp(cidrRange).build()) 96 .build()).build()); 97 ConfigOrError<?> result = parseRaw(permissionList, principalList); 98 assertThat(result.errorDetail).isNull(); 99 ServerCall<Void,Void> serverCall = mock(ServerCall.class); 100 Attributes attributes = Attributes.newBuilder() 101 .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress("10.10.10.0", 1)) 102 .set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, new InetSocketAddress("10.10.10.0",9090)) 103 .build(); 104 when(serverCall.getAttributes()).thenReturn(attributes); 105 when(serverCall.getMethodDescriptor()).thenReturn(method().build()); 106 GrpcAuthorizationEngine engine = 107 new GrpcAuthorizationEngine(((RbacConfig)result.config).authConfig()); 108 AuthDecision decision = engine.evaluate(new Metadata(), serverCall); 109 assertThat(decision.decision()).isEqualTo(GrpcAuthorizationEngine.Action.DENY); 110 } 111 112 @Test 113 @SuppressWarnings({"unchecked", "deprecation"}) portRangeParser()114 public void portRangeParser() { 115 List<Permission> permissionList = Arrays.asList( 116 Permission.newBuilder().setDestinationPortRange( 117 Int32Range.newBuilder().setStart(1010).setEnd(65535).build() 118 ).build()); 119 List<Principal> principalList = Arrays.asList( 120 Principal.newBuilder().setRemoteIp( 121 CidrRange.newBuilder().setAddressPrefix("10.10.10.0") 122 .setPrefixLen(UInt32Value.of(24)).build() 123 ).build()); 124 ConfigOrError<?> result = parse(permissionList, principalList); 125 assertThat(result.errorDetail).isNull(); 126 ServerCall<Void,Void> serverCall = mock(ServerCall.class); 127 Attributes attributes = Attributes.newBuilder() 128 .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress("10.10.10.0", 1)) 129 .set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, new InetSocketAddress("10.10.10.0",9090)) 130 .build(); 131 when(serverCall.getAttributes()).thenReturn(attributes); 132 when(serverCall.getMethodDescriptor()).thenReturn(method().build()); 133 GrpcAuthorizationEngine engine = 134 new GrpcAuthorizationEngine(((RbacConfig)result.config).authConfig()); 135 AuthDecision decision = engine.evaluate(new Metadata(), serverCall); 136 assertThat(decision.decision()).isEqualTo(GrpcAuthorizationEngine.Action.DENY); 137 } 138 139 @Test 140 @SuppressWarnings("unchecked") pathParser()141 public void pathParser() { 142 PathMatcher pathMatcher = PathMatcher.newBuilder().setPath(STRING_MATCHER).build(); 143 List<Permission> permissionList = Arrays.asList( 144 Permission.newBuilder().setUrlPath(pathMatcher).build()); 145 List<Principal> principalList = Arrays.asList( 146 Principal.newBuilder().setUrlPath(pathMatcher).build()); 147 ConfigOrError<RbacConfig> result = parse(permissionList, principalList); 148 assertThat(result.errorDetail).isNull(); 149 ServerCall<Void,Void> serverCall = mock(ServerCall.class); 150 when(serverCall.getMethodDescriptor()).thenReturn(method().build()); 151 GrpcAuthorizationEngine engine = 152 new GrpcAuthorizationEngine(result.config.authConfig()); 153 AuthDecision decision = engine.evaluate(new Metadata(), serverCall); 154 assertThat(decision.decision()).isEqualTo(GrpcAuthorizationEngine.Action.DENY); 155 } 156 157 @Test 158 @SuppressWarnings("unchecked") authenticatedParser()159 public void authenticatedParser() throws Exception { 160 List<Permission> permissionList = Arrays.asList( 161 Permission.newBuilder().setNotRule( 162 Permission.newBuilder().setRequestedServerName(STRING_MATCHER).build()).build()); 163 List<Principal> principalList = Arrays.asList( 164 Principal.newBuilder().setAuthenticated(Authenticated.newBuilder() 165 .setPrincipalName(STRING_MATCHER).build()).build()); 166 ConfigOrError<?> result = parse(permissionList, principalList); 167 assertThat(result.errorDetail).isNull(); 168 SSLSession sslSession = mock(SSLSession.class); 169 X509Certificate mockCert = mock(X509Certificate.class); 170 when(sslSession.getPeerCertificates()).thenReturn(new X509Certificate[]{mockCert}); 171 when(mockCert.getSubjectAlternativeNames()).thenReturn( 172 Arrays.<List<?>>asList(Arrays.asList(2, "/" + PATH))); 173 Attributes attributes = Attributes.newBuilder() 174 .set(Grpc.TRANSPORT_ATTR_SSL_SESSION, sslSession) 175 .build(); 176 ServerCall<Void,Void> serverCall = mock(ServerCall.class); 177 when(serverCall.getAttributes()).thenReturn(attributes); 178 GrpcAuthorizationEngine engine = 179 new GrpcAuthorizationEngine(((RbacConfig)result.config).authConfig()); 180 AuthDecision decision = engine.evaluate(new Metadata(), serverCall); 181 assertThat(decision.decision()).isEqualTo(GrpcAuthorizationEngine.Action.DENY); 182 } 183 184 @Test 185 @SuppressWarnings({"unchecked", "deprecation"}) headerParser()186 public void headerParser() { 187 HeaderMatcher headerMatcher = HeaderMatcher.newBuilder() 188 .setName("party").setExactMatch("win").build(); 189 List<Permission> permissionList = Arrays.asList( 190 Permission.newBuilder().setHeader(headerMatcher).build()); 191 List<Principal> principalList = Arrays.asList( 192 Principal.newBuilder().setHeader(headerMatcher).build()); 193 ConfigOrError<RbacConfig> result = parseOverride(permissionList, principalList); 194 assertThat(result.errorDetail).isNull(); 195 ServerCall<Void,Void> serverCall = mock(ServerCall.class); 196 GrpcAuthorizationEngine engine = 197 new GrpcAuthorizationEngine(result.config.authConfig()); 198 AuthDecision decision = engine.evaluate(metadata("party", "win"), serverCall); 199 assertThat(decision.decision()).isEqualTo(GrpcAuthorizationEngine.Action.DENY); 200 } 201 202 @Test 203 @SuppressWarnings("deprecation") headerParser_headerName()204 public void headerParser_headerName() { 205 HeaderMatcher headerMatcher = HeaderMatcher.newBuilder() 206 .setName("grpc--feature").setExactMatch("win").build(); 207 List<Permission> permissionList = Arrays.asList( 208 Permission.newBuilder().setHeader(headerMatcher).build()); 209 HeaderMatcher headerMatcher2 = HeaderMatcher.newBuilder() 210 .setName(":scheme").setExactMatch("win").build(); 211 List<Principal> principalList = Arrays.asList( 212 Principal.newBuilder().setHeader(headerMatcher2).build()); 213 ConfigOrError<RbacConfig> result = parseOverride(permissionList, principalList); 214 assertThat(result.errorDetail).isNotNull(); 215 } 216 217 @Test 218 @SuppressWarnings("unchecked") compositeRules()219 public void compositeRules() { 220 MetadataMatcher metadataMatcher = MetadataMatcher.newBuilder().build(); 221 List<Permission> permissionList = Arrays.asList( 222 Permission.newBuilder().setOrRules(Permission.Set.newBuilder().addRules( 223 Permission.newBuilder().setMetadata(metadataMatcher).build() 224 ).build()).build()); 225 List<Principal> principalList = Arrays.asList( 226 Principal.newBuilder().setNotId( 227 Principal.newBuilder().setMetadata(metadataMatcher).build() 228 ).build()); 229 ConfigOrError<? extends FilterConfig> result = parse(permissionList, principalList); 230 assertThat(result.errorDetail).isNull(); 231 assertThat(result.config).isInstanceOf(RbacConfig.class); 232 ServerCall<Void,Void> serverCall = mock(ServerCall.class); 233 GrpcAuthorizationEngine engine = 234 new GrpcAuthorizationEngine(((RbacConfig)result.config).authConfig()); 235 AuthDecision decision = engine.evaluate(new Metadata(), serverCall); 236 assertThat(decision.decision()).isEqualTo(GrpcAuthorizationEngine.Action.ALLOW); 237 } 238 239 @SuppressWarnings("unchecked") 240 @Test testAuthorizationInterceptor()241 public void testAuthorizationInterceptor() { 242 ServerCallHandler<Void, Void> mockHandler = mock(ServerCallHandler.class); 243 ServerCall<Void, Void> mockServerCall = mock(ServerCall.class); 244 Attributes attr = Attributes.newBuilder() 245 .set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, new InetSocketAddress("1::", 20)) 246 .build(); 247 when(mockServerCall.getAttributes()).thenReturn(attr); 248 PolicyMatcher policyMatcher = PolicyMatcher.create("policy-matcher", 249 OrMatcher.create(DestinationPortMatcher.create(99999)), 250 OrMatcher.create(AlwaysTrueMatcher.INSTANCE)); 251 AuthConfig authconfig = AuthConfig.create(Collections.singletonList(policyMatcher), 252 GrpcAuthorizationEngine.Action.ALLOW); 253 new RbacFilter().buildServerInterceptor(RbacConfig.create(authconfig), null) 254 .interceptCall(mockServerCall, new Metadata(), mockHandler); 255 verify(mockHandler, never()).startCall(eq(mockServerCall), any(Metadata.class)); 256 ArgumentCaptor<Status> captor = ArgumentCaptor.forClass(Status.class); 257 verify(mockServerCall).close(captor.capture(), any(Metadata.class)); 258 assertThat(captor.getValue().getCode()).isEqualTo(Status.PERMISSION_DENIED.getCode()); 259 assertThat(captor.getValue().getDescription()).isEqualTo("Access Denied"); 260 verify(mockServerCall).getAttributes(); 261 verifyNoMoreInteractions(mockServerCall); 262 263 authconfig = AuthConfig.create(Collections.singletonList(policyMatcher), 264 GrpcAuthorizationEngine.Action.DENY); 265 new RbacFilter().buildServerInterceptor(RbacConfig.create(authconfig), null) 266 .interceptCall(mockServerCall, new Metadata(), mockHandler); 267 verify(mockHandler).startCall(eq(mockServerCall), any(Metadata.class)); 268 } 269 270 @Test handleException()271 public void handleException() { 272 PathMatcher pathMatcher = PathMatcher.newBuilder() 273 .setPath(StringMatcher.newBuilder().build()).build(); 274 List<Permission> permissionList = Arrays.asList( 275 Permission.newBuilder().setUrlPath(pathMatcher).build()); 276 List<Principal> principalList = Arrays.asList( 277 Principal.newBuilder().setUrlPath(pathMatcher).build()); 278 ConfigOrError<?> result = parse(permissionList, principalList); 279 assertThat(result.errorDetail).isNotNull(); 280 281 permissionList = Arrays.asList(Permission.newBuilder().build()); 282 principalList = Arrays.asList(Principal.newBuilder().build()); 283 result = parse(permissionList, principalList); 284 assertThat(result.errorDetail).isNotNull(); 285 286 Message rawProto = io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC.newBuilder() 287 .setRules(RBAC.newBuilder().setAction(Action.DENY) 288 .putPolicies("policy-name", 289 Policy.newBuilder().setCondition(Expr.newBuilder().build()).build()) 290 .build()).build(); 291 result = new RbacFilter().parseFilterConfig(Any.pack(rawProto)); 292 assertThat(result.errorDetail).isNotNull(); 293 } 294 295 @Test 296 @SuppressWarnings("unchecked") overrideConfig()297 public void overrideConfig() { 298 ServerCallHandler<Void, Void> mockHandler = mock(ServerCallHandler.class); 299 ServerCall<Void, Void> mockServerCall = mock(ServerCall.class); 300 Attributes attr = Attributes.newBuilder() 301 .set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, new InetSocketAddress("1::", 20)) 302 .build(); 303 when(mockServerCall.getAttributes()).thenReturn(attr); 304 305 PolicyMatcher policyMatcher = PolicyMatcher.create("policy-matcher", 306 OrMatcher.create(DestinationPortMatcher.create(99999)), 307 OrMatcher.create(AlwaysTrueMatcher.INSTANCE)); 308 AuthConfig authconfig = AuthConfig.create(Collections.singletonList(policyMatcher), 309 GrpcAuthorizationEngine.Action.ALLOW); 310 RbacConfig original = RbacConfig.create(authconfig); 311 312 RBACPerRoute rbacPerRoute = RBACPerRoute.newBuilder().build(); 313 RbacConfig override = 314 new RbacFilter().parseFilterConfigOverride(Any.pack(rbacPerRoute)).config; 315 assertThat(override).isEqualTo(RbacConfig.create(null)); 316 ServerInterceptor interceptor = new RbacFilter().buildServerInterceptor(original, override); 317 assertThat(interceptor).isNull(); 318 319 policyMatcher = PolicyMatcher.create("policy-matcher-override", 320 OrMatcher.create(DestinationPortMatcher.create(20)), 321 OrMatcher.create(AlwaysTrueMatcher.INSTANCE)); 322 authconfig = AuthConfig.create(Collections.singletonList(policyMatcher), 323 GrpcAuthorizationEngine.Action.ALLOW); 324 override = RbacConfig.create(authconfig); 325 326 new RbacFilter().buildServerInterceptor(original, override) 327 .interceptCall(mockServerCall, new Metadata(), mockHandler); 328 verify(mockHandler).startCall(eq(mockServerCall), any(Metadata.class)); 329 verify(mockServerCall).getAttributes(); 330 verifyNoMoreInteractions(mockServerCall); 331 } 332 333 @Test ignoredConfig()334 public void ignoredConfig() { 335 Message rawProto = io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC.newBuilder() 336 .setRules(RBAC.newBuilder().setAction(Action.LOG) 337 .putPolicies("policy-name", Policy.newBuilder().build()).build()).build(); 338 ConfigOrError<RbacConfig> result = new RbacFilter().parseFilterConfig(Any.pack(rawProto)); 339 assertThat(result.config).isEqualTo(RbacConfig.create(null)); 340 } 341 metadata(String key, String value)342 private static Metadata metadata(String key, String value) { 343 Metadata metadata = new Metadata(); 344 metadata.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), value); 345 return metadata; 346 } 347 method()348 private MethodDescriptor.Builder<Void, Void> method() { 349 return MethodDescriptor.<Void,Void>newBuilder() 350 .setType(MethodType.BIDI_STREAMING) 351 .setFullMethodName(PATH) 352 .setRequestMarshaller(TestMethodDescriptors.voidMarshaller()) 353 .setResponseMarshaller(TestMethodDescriptors.voidMarshaller()); 354 } 355 parse(List<Permission> permissionList, List<Principal> principalList)356 private ConfigOrError<RbacConfig> parse(List<Permission> permissionList, 357 List<Principal> principalList) { 358 359 return RbacFilter.parseRbacConfig(buildRbac(permissionList, principalList)); 360 } 361 parseRaw(List<Permission> permissionList, List<Principal> principalList)362 private ConfigOrError<RbacConfig> parseRaw(List<Permission> permissionList, 363 List<Principal> principalList) { 364 Message rawProto = buildRbac(permissionList, principalList); 365 Any proto = Any.pack(rawProto); 366 return new RbacFilter().parseFilterConfig(proto); 367 } 368 buildRbac( List<Permission> permissionList, List<Principal> principalList)369 private io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC buildRbac( 370 List<Permission> permissionList, List<Principal> principalList) { 371 return io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC.newBuilder() 372 .setRules(RBAC.newBuilder().setAction(Action.DENY) 373 .putPolicies("policy-name", Policy.newBuilder() 374 .addAllPermissions(permissionList) 375 .addAllPrincipals(principalList).build()).build()).build(); 376 377 } 378 parseOverride(List<Permission> permissionList, List<Principal> principalList)379 private ConfigOrError<RbacConfig> parseOverride(List<Permission> permissionList, 380 List<Principal> principalList) { 381 RBACPerRoute rbacPerRoute = RBACPerRoute.newBuilder().setRbac( 382 buildRbac(permissionList, principalList)).build(); 383 Any proto = Any.pack(rbacPerRoute); 384 return new RbacFilter().parseFilterConfigOverride(proto); 385 } 386 } 387