mirror of https://github.com/grpc/grpc-java.git
Static authorization server interceptor implementation (#8934)
* Static authorization server interceptor implementation * Resolving comments * Remove RbacParser file * update error logs * checkstyle fixes * Add InternalRbacFilter * formatting * javadoc * format test file * resolving comments * minor formatting * Update comment
This commit is contained in:
parent
530cf905b1
commit
0194ae9a41
|
|
@ -33,12 +33,6 @@ tasks.named("jar").configure {
|
||||||
classifier = 'original'
|
classifier = 'original'
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(ashithasantosh): Remove javadoc exclusion on adding authorization
|
|
||||||
// interceptor implementations.
|
|
||||||
tasks.named("javadoc").configure {
|
|
||||||
exclude "io/grpc/authz/*"
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("shadowJar").configure {
|
tasks.named("shadowJar").configure {
|
||||||
classifier = null
|
classifier = null
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
@ -52,6 +46,12 @@ tasks.named("shadowJar").configure {
|
||||||
relocate 'com.google.api.expr', 'io.grpc.xds.shaded.com.google.api.expr'
|
relocate 'com.google.api.expr', 'io.grpc.xds.shaded.com.google.api.expr'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named("compileJava").configure {
|
||||||
|
it.options.compilerArgs += [
|
||||||
|
"-Xlint:-processing",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
publications {
|
publications {
|
||||||
maven(MavenPublication) {
|
maven(MavenPublication) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 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.authz;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
import io.envoyproxy.envoy.config.rbac.v3.RBAC;
|
||||||
|
import io.grpc.ExperimentalApi;
|
||||||
|
import io.grpc.InternalServerInterceptors;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.ServerCall;
|
||||||
|
import io.grpc.ServerCallHandler;
|
||||||
|
import io.grpc.ServerInterceptor;
|
||||||
|
import io.grpc.xds.InternalRbacFilter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorization server interceptor for static policy. The class will get
|
||||||
|
* <a href="https://github.com/grpc/proposal/blob/master/A43-grpc-authorization-api.md#user-facing-authorization-policy">
|
||||||
|
* gRPC Authorization policy</a> as a JSON string during initialization.
|
||||||
|
* This policy will be translated to Envoy RBAC policies to make
|
||||||
|
* authorization decisions. The policy cannot be changed once created.
|
||||||
|
*/
|
||||||
|
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/9746")
|
||||||
|
public final class AuthorizationServerInterceptor implements ServerInterceptor {
|
||||||
|
private final List<ServerInterceptor> interceptors = new ArrayList<>();
|
||||||
|
|
||||||
|
private AuthorizationServerInterceptor(String authorizationPolicy)
|
||||||
|
throws IOException {
|
||||||
|
List<RBAC> rbacs = AuthorizationPolicyTranslator.translate(authorizationPolicy);
|
||||||
|
if (rbacs == null || rbacs.isEmpty() || rbacs.size() > 2) {
|
||||||
|
throw new IllegalArgumentException("Failed to translate authorization policy");
|
||||||
|
}
|
||||||
|
for (RBAC rbac: rbacs) {
|
||||||
|
interceptors.add(
|
||||||
|
InternalRbacFilter.createInterceptor(
|
||||||
|
io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC.newBuilder()
|
||||||
|
.setRules(rbac).build()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
|
||||||
|
ServerCall<ReqT, RespT> call, Metadata headers,
|
||||||
|
ServerCallHandler<ReqT, RespT> next) {
|
||||||
|
for (ServerInterceptor interceptor: interceptors) {
|
||||||
|
next = InternalServerInterceptors.interceptCallHandlerCreate(interceptor, next);
|
||||||
|
}
|
||||||
|
return next.startCall(call, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static method that creates an AuthorizationServerInterceptor.
|
||||||
|
public static AuthorizationServerInterceptor create(String authorizationPolicy)
|
||||||
|
throws IOException {
|
||||||
|
checkNotNull(authorizationPolicy, "authorizationPolicy");
|
||||||
|
return new AuthorizationServerInterceptor(authorizationPolicy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,375 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 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.authz;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import io.grpc.ChannelCredentials;
|
||||||
|
import io.grpc.Grpc;
|
||||||
|
import io.grpc.InsecureChannelCredentials;
|
||||||
|
import io.grpc.InsecureServerCredentials;
|
||||||
|
import io.grpc.ManagedChannel;
|
||||||
|
import io.grpc.Server;
|
||||||
|
import io.grpc.ServerCredentials;
|
||||||
|
import io.grpc.StatusRuntimeException;
|
||||||
|
import io.grpc.TlsChannelCredentials;
|
||||||
|
import io.grpc.TlsServerCredentials;
|
||||||
|
import io.grpc.TlsServerCredentials.ClientAuth;
|
||||||
|
import io.grpc.internal.testing.TestUtils;
|
||||||
|
import io.grpc.stub.StreamObserver;
|
||||||
|
import io.grpc.testing.protobuf.SimpleRequest;
|
||||||
|
import io.grpc.testing.protobuf.SimpleResponse;
|
||||||
|
import io.grpc.testing.protobuf.SimpleServiceGrpc;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class AuthorizationEnd2EndTest {
|
||||||
|
public static final String SERVER_0_KEY_FILE = "server0.key";
|
||||||
|
public static final String SERVER_0_PEM_FILE = "server0.pem";
|
||||||
|
public static final String CLIENT_0_KEY_FILE = "client.key";
|
||||||
|
public static final String CLIENT_0_PEM_FILE = "client.pem";
|
||||||
|
public static final String CA_PEM_FILE = "ca.pem";
|
||||||
|
|
||||||
|
private Server server;
|
||||||
|
private ManagedChannel channel;
|
||||||
|
|
||||||
|
private void initServerWithStaticAuthz(
|
||||||
|
String authorizationPolicy, ServerCredentials serverCredentials) throws Exception {
|
||||||
|
AuthorizationServerInterceptor authzInterceptor =
|
||||||
|
AuthorizationServerInterceptor.create(authorizationPolicy);
|
||||||
|
server = Grpc.newServerBuilderForPort(0, serverCredentials)
|
||||||
|
.addService(new SimpleServiceImpl())
|
||||||
|
.intercept(authzInterceptor)
|
||||||
|
.build()
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimpleServiceGrpc.SimpleServiceBlockingStub getStub() {
|
||||||
|
channel =
|
||||||
|
Grpc.newChannelBuilderForAddress(
|
||||||
|
"localhost", server.getPort(), InsecureChannelCredentials.create())
|
||||||
|
.build();
|
||||||
|
return SimpleServiceGrpc.newBlockingStub(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimpleServiceGrpc.SimpleServiceBlockingStub getStub(
|
||||||
|
ChannelCredentials channelCredentials) {
|
||||||
|
channel = Grpc.newChannelBuilderForAddress(
|
||||||
|
"localhost", server.getPort(), channelCredentials)
|
||||||
|
.overrideAuthority("foo.test.google.com.au")
|
||||||
|
.build();
|
||||||
|
return SimpleServiceGrpc.newBlockingStub(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
if (server != null) {
|
||||||
|
server.shutdown();
|
||||||
|
}
|
||||||
|
if (channel != null) {
|
||||||
|
channel.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staticAuthzAllowsRpcNoMatchInDenyMatchInAllowTest() throws Exception {
|
||||||
|
String policy = "{"
|
||||||
|
+ " \"name\" : \"authz\" ,"
|
||||||
|
+ " \"deny_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"deny_UnaryRpc\","
|
||||||
|
+ " \"request\": {"
|
||||||
|
+ " \"paths\": ["
|
||||||
|
+ " \"*/UnaryRpc\""
|
||||||
|
+ " ],"
|
||||||
|
+ " \"headers\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"key\": \"dev-path\","
|
||||||
|
+ " \"values\": [\"/dev/path/*\"]"
|
||||||
|
+ " }"
|
||||||
|
+ " ]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ],"
|
||||||
|
+ " \"allow_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"allow_all\""
|
||||||
|
+ " }"
|
||||||
|
+ " ]"
|
||||||
|
+ "}";
|
||||||
|
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||||
|
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staticAuthzDeniesRpcNoMatchInDenyAndAllowTest() throws Exception {
|
||||||
|
String policy = "{"
|
||||||
|
+ " \"name\" : \"authz\" ,"
|
||||||
|
+ " \"deny_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"deny_foo\","
|
||||||
|
+ " \"source\": {"
|
||||||
|
+ " \"principals\": ["
|
||||||
|
+ " \"foo\""
|
||||||
|
+ " ]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ],"
|
||||||
|
+ " \"allow_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"allow_ClientStreamingRpc\","
|
||||||
|
+ " \"request\": {"
|
||||||
|
+ " \"paths\": ["
|
||||||
|
+ " \"*/ClientStreamingRpc\""
|
||||||
|
+ " ]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ]"
|
||||||
|
+ "}";
|
||||||
|
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||||
|
try {
|
||||||
|
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||||
|
fail("exception expected");
|
||||||
|
} catch (StatusRuntimeException sre) {
|
||||||
|
assertThat(sre).hasMessageThat().isEqualTo(
|
||||||
|
"PERMISSION_DENIED: Access Denied");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AssertionError("the test failed ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staticAuthzDeniesRpcMatchInDenyAndAllowTest() throws Exception {
|
||||||
|
String policy = "{"
|
||||||
|
+ " \"name\" : \"authz\" ,"
|
||||||
|
+ " \"deny_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"deny_UnaryRpc\","
|
||||||
|
+ " \"request\": {"
|
||||||
|
+ " \"paths\": ["
|
||||||
|
+ " \"*/UnaryRpc\""
|
||||||
|
+ " ]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ],"
|
||||||
|
+ " \"allow_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"allow_UnaryRpc\","
|
||||||
|
+ " \"request\": {"
|
||||||
|
+ " \"paths\": ["
|
||||||
|
+ " \"*/UnaryRpc\""
|
||||||
|
+ " ]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ]"
|
||||||
|
+ "}";
|
||||||
|
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||||
|
try {
|
||||||
|
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||||
|
fail("exception expected");
|
||||||
|
} catch (StatusRuntimeException sre) {
|
||||||
|
assertThat(sre).hasMessageThat().isEqualTo(
|
||||||
|
"PERMISSION_DENIED: Access Denied");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AssertionError("the test failed ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staticAuthzDeniesRpcMatchInDenyNoMatchInAllowTest() throws Exception {
|
||||||
|
String policy = "{"
|
||||||
|
+ " \"name\" : \"authz\" ,"
|
||||||
|
+ " \"deny_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"deny_UnaryRpc\","
|
||||||
|
+ " \"request\": {"
|
||||||
|
+ " \"paths\": ["
|
||||||
|
+ " \"*/UnaryRpc\""
|
||||||
|
+ " ]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ],"
|
||||||
|
+ " \"allow_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"allow_ClientStreamingRpc\","
|
||||||
|
+ " \"request\": {"
|
||||||
|
+ " \"paths\": ["
|
||||||
|
+ " \"*/ClientStreamingRpc\""
|
||||||
|
+ " ]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ]"
|
||||||
|
+ "}";
|
||||||
|
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||||
|
try {
|
||||||
|
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||||
|
fail("exception expected");
|
||||||
|
} catch (StatusRuntimeException sre) {
|
||||||
|
assertThat(sre).hasMessageThat().isEqualTo(
|
||||||
|
"PERMISSION_DENIED: Access Denied");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AssertionError("the test failed ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staticAuthzAllowsRpcEmptyDenyMatchInAllowTest() throws Exception {
|
||||||
|
String policy = "{"
|
||||||
|
+ " \"name\" : \"authz\" ,"
|
||||||
|
+ " \"allow_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"allow_UnaryRpc\","
|
||||||
|
+ " \"request\": {"
|
||||||
|
+ " \"paths\": ["
|
||||||
|
+ " \"*/UnaryRpc\""
|
||||||
|
+ " ]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ]"
|
||||||
|
+ "}";
|
||||||
|
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||||
|
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staticAuthzDeniesRpcEmptyDenyNoMatchInAllowTest() throws Exception {
|
||||||
|
String policy = "{"
|
||||||
|
+ " \"name\" : \"authz\" ,"
|
||||||
|
+ " \"allow_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"allow_ClientStreamingRpc\","
|
||||||
|
+ " \"request\": {"
|
||||||
|
+ " \"paths\": ["
|
||||||
|
+ " \"*/ClientStreamingRpc\""
|
||||||
|
+ " ]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ]"
|
||||||
|
+ "}";
|
||||||
|
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||||
|
try {
|
||||||
|
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||||
|
fail("exception expected");
|
||||||
|
} catch (StatusRuntimeException sre) {
|
||||||
|
assertThat(sre).hasMessageThat().isEqualTo(
|
||||||
|
"PERMISSION_DENIED: Access Denied");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AssertionError("the test failed ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staticAuthzDeniesRpcWithPrincipalsFieldOnUnauthenticatedConnectionTest()
|
||||||
|
throws Exception {
|
||||||
|
String policy = "{"
|
||||||
|
+ " \"name\" : \"authz\" ,"
|
||||||
|
+ " \"allow_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"allow_authenticated\","
|
||||||
|
+ " \"source\": {"
|
||||||
|
+ " \"principals\": [\"*\", \"\"]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ]"
|
||||||
|
+ "}";
|
||||||
|
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||||
|
try {
|
||||||
|
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||||
|
fail("exception expected");
|
||||||
|
} catch (StatusRuntimeException sre) {
|
||||||
|
assertThat(sre).hasMessageThat().isEqualTo(
|
||||||
|
"PERMISSION_DENIED: Access Denied");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AssertionError("the test failed ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staticAuthzAllowsRpcWithPrincipalsFieldOnMtlsAuthenticatedConnectionTest()
|
||||||
|
throws Exception {
|
||||||
|
File caCertFile = TestUtils.loadCert(CA_PEM_FILE);
|
||||||
|
File serverKey0File = TestUtils.loadCert(SERVER_0_KEY_FILE);
|
||||||
|
File serverCert0File = TestUtils.loadCert(SERVER_0_PEM_FILE);
|
||||||
|
File clientKey0File = TestUtils.loadCert(CLIENT_0_KEY_FILE);
|
||||||
|
File clientCert0File = TestUtils.loadCert(CLIENT_0_PEM_FILE);
|
||||||
|
String policy = "{"
|
||||||
|
+ " \"name\" : \"authz\" ,"
|
||||||
|
+ " \"allow_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"allow_mtls\","
|
||||||
|
+ " \"source\": {"
|
||||||
|
+ " \"principals\": [\"*\"]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ]"
|
||||||
|
+ "}";
|
||||||
|
ServerCredentials serverCredentials = TlsServerCredentials.newBuilder()
|
||||||
|
.keyManager(serverCert0File, serverKey0File)
|
||||||
|
.trustManager(caCertFile)
|
||||||
|
.clientAuth(ClientAuth.REQUIRE)
|
||||||
|
.build();
|
||||||
|
initServerWithStaticAuthz(policy, serverCredentials);
|
||||||
|
ChannelCredentials channelCredentials = TlsChannelCredentials.newBuilder()
|
||||||
|
.keyManager(clientCert0File, clientKey0File)
|
||||||
|
.trustManager(caCertFile)
|
||||||
|
.build();
|
||||||
|
getStub(channelCredentials).unaryRpc(SimpleRequest.getDefaultInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void staticAuthzAllowsRpcWithPrincipalsFieldOnTlsAuthenticatedConnectionTest()
|
||||||
|
throws Exception {
|
||||||
|
File caCertFile = TestUtils.loadCert(CA_PEM_FILE);
|
||||||
|
File serverKey0File = TestUtils.loadCert(SERVER_0_KEY_FILE);
|
||||||
|
File serverCert0File = TestUtils.loadCert(SERVER_0_PEM_FILE);
|
||||||
|
String policy = "{"
|
||||||
|
+ " \"name\" : \"authz\" ,"
|
||||||
|
+ " \"allow_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"allow_tls\","
|
||||||
|
+ " \"source\": {"
|
||||||
|
+ " \"principals\": [\"\"]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ]"
|
||||||
|
+ "}";
|
||||||
|
ServerCredentials serverCredentials = TlsServerCredentials.newBuilder()
|
||||||
|
.keyManager(serverCert0File, serverKey0File)
|
||||||
|
.trustManager(caCertFile)
|
||||||
|
.clientAuth(ClientAuth.OPTIONAL)
|
||||||
|
.build();
|
||||||
|
initServerWithStaticAuthz(policy, serverCredentials);
|
||||||
|
ChannelCredentials channelCredentials = TlsChannelCredentials.newBuilder()
|
||||||
|
.trustManager(caCertFile)
|
||||||
|
.build();
|
||||||
|
getStub(channelCredentials).unaryRpc(SimpleRequest.getDefaultInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase {
|
||||||
|
@Override
|
||||||
|
public void unaryRpc(SimpleRequest req, StreamObserver<SimpleResponse> respOb) {
|
||||||
|
respOb.onNext(SimpleResponse.getDefaultInstance());
|
||||||
|
respOb.onCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 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.authz;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class AuthorizationServerInterceptorTest {
|
||||||
|
@Test
|
||||||
|
public void invalidPolicyFailsStaticAuthzInterceptorCreation() throws Exception {
|
||||||
|
String policy = "{ \"name\": \"abc\",, }";
|
||||||
|
try {
|
||||||
|
AuthorizationServerInterceptor.create(policy);
|
||||||
|
fail("exception expected");
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
assertThat(ioe).hasMessageThat().isEqualTo(
|
||||||
|
"Use JsonReader.setLenient(true) to accept malformed JSON"
|
||||||
|
+ " at line 1 column 18 path $.name");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AssertionError("the test failed ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validPolicyCreatesStaticAuthzInterceptor() throws Exception {
|
||||||
|
String policy = "{"
|
||||||
|
+ " \"name\" : \"authz\","
|
||||||
|
+ " \"deny_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"deny_foo\","
|
||||||
|
+ " \"source\": {"
|
||||||
|
+ " \"principals\": ["
|
||||||
|
+ " \"spiffe://foo.com\""
|
||||||
|
+ " ]"
|
||||||
|
+ " }"
|
||||||
|
+ " }"
|
||||||
|
+ " ],"
|
||||||
|
+ " \"allow_rules\": ["
|
||||||
|
+ " {"
|
||||||
|
+ " \"name\": \"allow_all\""
|
||||||
|
+ " }"
|
||||||
|
+ " ]"
|
||||||
|
+ "}";
|
||||||
|
assertNotNull(AuthorizationServerInterceptor.create(policy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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;
|
||||||
|
|
||||||
|
import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC;
|
||||||
|
import io.grpc.Internal;
|
||||||
|
import io.grpc.ServerInterceptor;
|
||||||
|
import io.grpc.xds.RbacConfig;
|
||||||
|
import io.grpc.xds.RbacFilter;
|
||||||
|
|
||||||
|
/** This class exposes some functionality in RbacFilter to other packages. */
|
||||||
|
@Internal
|
||||||
|
public final class InternalRbacFilter {
|
||||||
|
|
||||||
|
private InternalRbacFilter() {}
|
||||||
|
|
||||||
|
/** Parses RBAC filter config and creates AuthorizationServerInterceptor. */
|
||||||
|
public static ServerInterceptor createInterceptor(RBAC rbac) {
|
||||||
|
ConfigOrError<RbacConfig> filterConfig = RbacFilter.parseRbacConfig(rbac);
|
||||||
|
if (filterConfig.errorDetail != null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format("Failed to parse Rbac policy: %s", filterConfig.errorDetail));
|
||||||
|
}
|
||||||
|
return new RbacFilter().buildServerInterceptor(filterConfig.config, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -54,6 +54,12 @@ public final class MatcherParser {
|
||||||
case SUFFIX_MATCH:
|
case SUFFIX_MATCH:
|
||||||
return Matchers.HeaderMatcher.forSuffix(
|
return Matchers.HeaderMatcher.forSuffix(
|
||||||
proto.getName(), proto.getSuffixMatch(), proto.getInvertMatch());
|
proto.getName(), proto.getSuffixMatch(), proto.getInvertMatch());
|
||||||
|
case CONTAINS_MATCH:
|
||||||
|
return Matchers.HeaderMatcher.forContains(
|
||||||
|
proto.getName(), proto.getContainsMatch(), proto.getInvertMatch());
|
||||||
|
case STRING_MATCH:
|
||||||
|
return Matchers.HeaderMatcher.forString(
|
||||||
|
proto.getName(), parseStringMatcher(proto.getStringMatch()), proto.getInvertMatch());
|
||||||
case HEADERMATCHSPECIFIER_NOT_SET:
|
case HEADERMATCHSPECIFIER_NOT_SET:
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,14 @@ public final class Matchers {
|
||||||
@Nullable
|
@Nullable
|
||||||
public abstract String suffix();
|
public abstract String suffix();
|
||||||
|
|
||||||
|
// Matches header value with the substring.
|
||||||
|
@Nullable
|
||||||
|
public abstract String contains();
|
||||||
|
|
||||||
|
// Matches header value with the string matcher.
|
||||||
|
@Nullable
|
||||||
|
public abstract StringMatcher stringMatcher();
|
||||||
|
|
||||||
// Whether the matching semantics is inverted. E.g., present && !inverted -> !present
|
// Whether the matching semantics is inverted. E.g., present && !inverted -> !present
|
||||||
public abstract boolean inverted();
|
public abstract boolean inverted();
|
||||||
|
|
||||||
|
|
@ -69,50 +77,71 @@ public final class Matchers {
|
||||||
public static HeaderMatcher forExactValue(String name, String exactValue, boolean inverted) {
|
public static HeaderMatcher forExactValue(String name, String exactValue, boolean inverted) {
|
||||||
checkNotNull(name, "name");
|
checkNotNull(name, "name");
|
||||||
checkNotNull(exactValue, "exactValue");
|
checkNotNull(exactValue, "exactValue");
|
||||||
return HeaderMatcher.create(name, exactValue, null, null, null, null, null, inverted);
|
return HeaderMatcher.create(
|
||||||
|
name, exactValue, null, null, null, null, null, null, null, inverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The request header value should match the regular expression pattern. */
|
/** The request header value should match the regular expression pattern. */
|
||||||
public static HeaderMatcher forSafeRegEx(String name, Pattern safeRegEx, boolean inverted) {
|
public static HeaderMatcher forSafeRegEx(String name, Pattern safeRegEx, boolean inverted) {
|
||||||
checkNotNull(name, "name");
|
checkNotNull(name, "name");
|
||||||
checkNotNull(safeRegEx, "safeRegEx");
|
checkNotNull(safeRegEx, "safeRegEx");
|
||||||
return HeaderMatcher.create(name, null, safeRegEx, null, null, null, null, inverted);
|
return HeaderMatcher.create(
|
||||||
|
name, null, safeRegEx, null, null, null, null, null, null, inverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The request header value should be within the range. */
|
/** The request header value should be within the range. */
|
||||||
public static HeaderMatcher forRange(String name, Range range, boolean inverted) {
|
public static HeaderMatcher forRange(String name, Range range, boolean inverted) {
|
||||||
checkNotNull(name, "name");
|
checkNotNull(name, "name");
|
||||||
checkNotNull(range, "range");
|
checkNotNull(range, "range");
|
||||||
return HeaderMatcher.create(name, null, null, range, null, null, null, inverted);
|
return HeaderMatcher.create(name, null, null, range, null, null, null, null, null, inverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The request header value should exist. */
|
/** The request header value should exist. */
|
||||||
public static HeaderMatcher forPresent(String name, boolean present, boolean inverted) {
|
public static HeaderMatcher forPresent(String name, boolean present, boolean inverted) {
|
||||||
checkNotNull(name, "name");
|
checkNotNull(name, "name");
|
||||||
return HeaderMatcher.create(name, null, null, null, present, null, null, inverted);
|
return HeaderMatcher.create(
|
||||||
|
name, null, null, null, present, null, null, null, null, inverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The request header value should have this prefix. */
|
/** The request header value should have this prefix. */
|
||||||
public static HeaderMatcher forPrefix(String name, String prefix, boolean inverted) {
|
public static HeaderMatcher forPrefix(String name, String prefix, boolean inverted) {
|
||||||
checkNotNull(name, "name");
|
checkNotNull(name, "name");
|
||||||
checkNotNull(prefix, "prefix");
|
checkNotNull(prefix, "prefix");
|
||||||
return HeaderMatcher.create(name, null, null, null, null, prefix, null, inverted);
|
return HeaderMatcher.create(name, null, null, null, null, prefix, null, null, null, inverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The request header value should have this suffix. */
|
/** The request header value should have this suffix. */
|
||||||
public static HeaderMatcher forSuffix(String name, String suffix, boolean inverted) {
|
public static HeaderMatcher forSuffix(String name, String suffix, boolean inverted) {
|
||||||
checkNotNull(name, "name");
|
checkNotNull(name, "name");
|
||||||
checkNotNull(suffix, "suffix");
|
checkNotNull(suffix, "suffix");
|
||||||
return HeaderMatcher.create(name, null, null, null, null, null, suffix, inverted);
|
return HeaderMatcher.create(name, null, null, null, null, null, suffix, null, null, inverted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The request header value should have this substring. */
|
||||||
|
public static HeaderMatcher forContains(String name, String contains, boolean inverted) {
|
||||||
|
checkNotNull(name, "name");
|
||||||
|
checkNotNull(contains, "contains");
|
||||||
|
return HeaderMatcher.create(
|
||||||
|
name, null, null, null, null, null, null, contains, null, inverted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The request header value should match this stringMatcher. */
|
||||||
|
public static HeaderMatcher forString(
|
||||||
|
String name, StringMatcher stringMatcher, boolean inverted) {
|
||||||
|
checkNotNull(name, "name");
|
||||||
|
checkNotNull(stringMatcher, "stringMatcher");
|
||||||
|
return HeaderMatcher.create(
|
||||||
|
name, null, null, null, null, null, null, null, stringMatcher, inverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HeaderMatcher create(String name, @Nullable String exactValue,
|
private static HeaderMatcher create(String name, @Nullable String exactValue,
|
||||||
@Nullable Pattern safeRegEx, @Nullable Range range,
|
@Nullable Pattern safeRegEx, @Nullable Range range,
|
||||||
@Nullable Boolean present, @Nullable String prefix,
|
@Nullable Boolean present, @Nullable String prefix,
|
||||||
@Nullable String suffix, boolean inverted) {
|
@Nullable String suffix, @Nullable String contains,
|
||||||
|
@Nullable StringMatcher stringMatcher, boolean inverted) {
|
||||||
checkNotNull(name, "name");
|
checkNotNull(name, "name");
|
||||||
return new AutoValue_Matchers_HeaderMatcher(name, exactValue, safeRegEx, range, present,
|
return new AutoValue_Matchers_HeaderMatcher(name, exactValue, safeRegEx, range, present,
|
||||||
prefix, suffix, inverted);
|
prefix, suffix, contains, stringMatcher, inverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the matching result. */
|
/** Returns the matching result. */
|
||||||
|
|
@ -138,8 +167,12 @@ public final class Matchers {
|
||||||
baseMatch = value.startsWith(prefix());
|
baseMatch = value.startsWith(prefix());
|
||||||
} else if (present() != null) {
|
} else if (present() != null) {
|
||||||
baseMatch = present();
|
baseMatch = present();
|
||||||
} else {
|
} else if (suffix() != null) {
|
||||||
baseMatch = value.endsWith(suffix());
|
baseMatch = value.endsWith(suffix());
|
||||||
|
} else if (contains() != null) {
|
||||||
|
baseMatch = value.contains(contains());
|
||||||
|
} else {
|
||||||
|
baseMatch = stringMatcher().matches(value);
|
||||||
}
|
}
|
||||||
return baseMatch != inverted();
|
return baseMatch != inverted();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,15 @@ public class MatcherTest {
|
||||||
assertThat(matcher.matches("1v2")).isFalse();
|
assertThat(matcher.matches("1v2")).isFalse();
|
||||||
assertThat(matcher.matches(null)).isFalse();
|
assertThat(matcher.matches(null)).isFalse();
|
||||||
|
|
||||||
|
matcher = HeaderMatcher.forContains("version", "v1", false);
|
||||||
|
assertThat(matcher.matches("xv1")).isTrue();
|
||||||
|
assertThat(matcher.matches("1vx")).isFalse();
|
||||||
|
assertThat(matcher.matches(null)).isFalse();
|
||||||
|
matcher = HeaderMatcher.forContains("version", "v1", true);
|
||||||
|
assertThat(matcher.matches("xv1")).isFalse();
|
||||||
|
assertThat(matcher.matches("1vx")).isTrue();
|
||||||
|
assertThat(matcher.matches(null)).isFalse();
|
||||||
|
|
||||||
matcher = HeaderMatcher.forSafeRegEx("version", Pattern.compile("v2.*"), false);
|
matcher = HeaderMatcher.forSafeRegEx("version", Pattern.compile("v2.*"), false);
|
||||||
assertThat(matcher.matches("v2..")).isTrue();
|
assertThat(matcher.matches("v2..")).isTrue();
|
||||||
assertThat(matcher.matches("v1")).isFalse();
|
assertThat(matcher.matches("v1")).isFalse();
|
||||||
|
|
@ -180,5 +189,14 @@ public class MatcherTest {
|
||||||
assertThat(matcher.matches("1")).isTrue();
|
assertThat(matcher.matches("1")).isTrue();
|
||||||
assertThat(matcher.matches("8080")).isFalse();
|
assertThat(matcher.matches("8080")).isFalse();
|
||||||
assertThat(matcher.matches(null)).isFalse();
|
assertThat(matcher.matches(null)).isFalse();
|
||||||
|
|
||||||
|
matcher = HeaderMatcher.forString("version", StringMatcher.forExact("v1", true), false);
|
||||||
|
assertThat(matcher.matches("v1")).isTrue();
|
||||||
|
assertThat(matcher.matches("v1x")).isFalse();
|
||||||
|
assertThat(matcher.matches(null)).isFalse();
|
||||||
|
matcher = HeaderMatcher.forString("version", StringMatcher.forExact("v1", true), true);
|
||||||
|
assertThat(matcher.matches("v1x")).isTrue();
|
||||||
|
assertThat(matcher.matches("v1")).isFalse();
|
||||||
|
assertThat(matcher.matches(null)).isFalse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue