mirror of https://github.com/grpc/grpc-java.git
authz: File Watcher Authorization Server Interceptor (#9775)
This commit is contained in:
parent
c9fcd46e84
commit
b43ddc2886
|
|
@ -18,7 +18,8 @@ dependencies {
|
|||
compileOnly libraries.javax.annotation
|
||||
|
||||
testImplementation project(':grpc-testing'),
|
||||
project(':grpc-testing-proto')
|
||||
project(':grpc-testing-proto'),
|
||||
project(':grpc-core').sourceSets.test.output // for FakeClock
|
||||
testImplementation (libraries.guava.testlib) {
|
||||
exclude group: 'junit', module: 'junit'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ import java.util.List;
|
|||
* <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.
|
||||
* authorization decisions. The policy cannot be changed once created. To
|
||||
* change the policy after creation, see FileWatcherAuthorizationServerInterceptor.
|
||||
*/
|
||||
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/9746")
|
||||
public final class AuthorizationServerInterceptor implements ServerInterceptor {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.ServerCall;
|
||||
import io.grpc.ServerCallHandler;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Authorization server interceptor for policy from file with refresh capability.
|
||||
* 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> from a JSON file during initialization.
|
||||
*/
|
||||
public final class FileWatcherAuthorizationServerInterceptor implements ServerInterceptor {
|
||||
private static final Logger logger =
|
||||
Logger.getLogger(FileWatcherAuthorizationServerInterceptor.class.getName());
|
||||
|
||||
private volatile AuthorizationServerInterceptor internalAuthzServerInterceptor;
|
||||
|
||||
private final File policyFile;
|
||||
private String policyContents;
|
||||
|
||||
private FileWatcherAuthorizationServerInterceptor(File policyFile) throws IOException {
|
||||
this.policyFile = policyFile;
|
||||
updateInternalInterceptor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
|
||||
ServerCall<ReqT, RespT> call, Metadata headers,
|
||||
ServerCallHandler<ReqT, RespT> next) {
|
||||
return internalAuthzServerInterceptor.interceptCall(call, headers, next);
|
||||
}
|
||||
|
||||
void updateInternalInterceptor() throws IOException {
|
||||
String currentPolicyContents = new String(Files.readAllBytes(policyFile.toPath()), UTF_8);
|
||||
if (currentPolicyContents.equals(policyContents)) {
|
||||
return;
|
||||
}
|
||||
policyContents = currentPolicyContents;
|
||||
internalAuthzServerInterceptor = AuthorizationServerInterceptor.create(policyContents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Policy is reloaded periodically as per the provided refresh interval. Unlike the
|
||||
* constructor, exception thrown during reload will be caught and logged and the
|
||||
* previous AuthorizationServerInterceptor will be used to make authorization
|
||||
* decisions.
|
||||
*
|
||||
* @param period the period between successive file load executions.
|
||||
* @param unit the time unit for period parameter
|
||||
* @param executor the execute service we use to read and update authorization policy
|
||||
* @return an object that caller should close when the file refreshes are not needed
|
||||
*/
|
||||
public Closeable scheduleRefreshes(
|
||||
long period, TimeUnit unit, ScheduledExecutorService executor) throws IOException {
|
||||
checkNotNull(executor, "scheduledExecutorService");
|
||||
if (period <= 0) {
|
||||
throw new IllegalArgumentException("Refresh interval must be greater than 0");
|
||||
}
|
||||
final ScheduledFuture<?> future =
|
||||
executor.scheduleWithFixedDelay(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
updateInternalInterceptor();
|
||||
} catch (Exception e) {
|
||||
logger.log(Level.WARNING, "Authorization Policy file reload failed", e);
|
||||
}
|
||||
}
|
||||
}, period, period, unit);
|
||||
return new Closeable() {
|
||||
@Override public void close() {
|
||||
future.cancel(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static FileWatcherAuthorizationServerInterceptor create(File policyFile)
|
||||
throws IOException {
|
||||
checkNotNull(policyFile, "policyFile");
|
||||
return new FileWatcherAuthorizationServerInterceptor(policyFile);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,9 @@
|
|||
package io.grpc.authz;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import io.grpc.ChannelCredentials;
|
||||
|
|
@ -26,16 +29,22 @@ import io.grpc.InsecureServerCredentials;
|
|||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerCredentials;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import io.grpc.TlsChannelCredentials;
|
||||
import io.grpc.TlsServerCredentials;
|
||||
import io.grpc.TlsServerCredentials.ClientAuth;
|
||||
import io.grpc.internal.FakeClock;
|
||||
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.Closeable;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
|
@ -52,10 +61,27 @@ public class AuthorizationEnd2EndTest {
|
|||
private Server server;
|
||||
private ManagedChannel channel;
|
||||
|
||||
private void initServerWithStaticAuthz(
|
||||
String authorizationPolicy, ServerCredentials serverCredentials) throws Exception {
|
||||
AuthorizationServerInterceptor authzInterceptor =
|
||||
private FakeClock fakeClock = new FakeClock();
|
||||
private File policyFile;
|
||||
|
||||
private AuthorizationServerInterceptor createStaticAuthorizationInterceptor(
|
||||
String authorizationPolicy) throws Exception {
|
||||
AuthorizationServerInterceptor interceptor =
|
||||
AuthorizationServerInterceptor.create(authorizationPolicy);
|
||||
assertNotNull(interceptor);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
private FileWatcherAuthorizationServerInterceptor
|
||||
createFileWatcherAuthorizationInterceptor(File policyFile) throws Exception {
|
||||
FileWatcherAuthorizationServerInterceptor interceptor =
|
||||
FileWatcherAuthorizationServerInterceptor.create(policyFile);
|
||||
assertNotNull(interceptor);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
private void initServerWithAuthzInterceptor(
|
||||
ServerInterceptor authzInterceptor, ServerCredentials serverCredentials) throws Exception {
|
||||
server = Grpc.newServerBuilderForPort(0, serverCredentials)
|
||||
.addService(new SimpleServiceImpl())
|
||||
.intercept(authzInterceptor)
|
||||
|
|
@ -63,11 +89,22 @@ public class AuthorizationEnd2EndTest {
|
|||
.start();
|
||||
}
|
||||
|
||||
private void createTempAuthorizationPolicy(String authorizationPolicy) throws Exception {
|
||||
policyFile = File.createTempFile("temp", "json");
|
||||
Files.write(Paths.get(policyFile.getAbsolutePath()), authorizationPolicy.getBytes(UTF_8));
|
||||
}
|
||||
|
||||
private void rewriteAuthorizationPolicy(String newPolicy) throws Exception {
|
||||
assertNotNull(policyFile);
|
||||
Files.write(Paths.get(policyFile.getAbsolutePath()), newPolicy.getBytes(UTF_8));
|
||||
}
|
||||
|
||||
private SimpleServiceGrpc.SimpleServiceBlockingStub getStub() {
|
||||
channel =
|
||||
Grpc.newChannelBuilderForAddress(
|
||||
"localhost", server.getPort(), InsecureChannelCredentials.create())
|
||||
.build();
|
||||
if (channel == null) {
|
||||
channel = Grpc.newChannelBuilderForAddress(
|
||||
"localhost", server.getPort(), InsecureChannelCredentials.create())
|
||||
.build();
|
||||
}
|
||||
return SimpleServiceGrpc.newBlockingStub(channel);
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +119,9 @@ public class AuthorizationEnd2EndTest {
|
|||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (policyFile != null) {
|
||||
policyFile.delete();
|
||||
}
|
||||
if (server != null) {
|
||||
server.shutdown();
|
||||
}
|
||||
|
|
@ -116,7 +156,8 @@ public class AuthorizationEnd2EndTest {
|
|||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||
AuthorizationServerInterceptor interceptor = createStaticAuthorizationInterceptor(policy);
|
||||
initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create());
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
}
|
||||
|
||||
|
|
@ -145,15 +186,14 @@ public class AuthorizationEnd2EndTest {
|
|||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||
AuthorizationServerInterceptor interceptor = createStaticAuthorizationInterceptor(policy);
|
||||
initServerWithAuthzInterceptor(interceptor, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,15 +222,14 @@ public class AuthorizationEnd2EndTest {
|
|||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||
AuthorizationServerInterceptor interceptor = createStaticAuthorizationInterceptor(policy);
|
||||
initServerWithAuthzInterceptor(interceptor, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -219,15 +258,14 @@ public class AuthorizationEnd2EndTest {
|
|||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||
AuthorizationServerInterceptor interceptor = createStaticAuthorizationInterceptor(policy);
|
||||
initServerWithAuthzInterceptor(interceptor, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,7 +284,8 @@ public class AuthorizationEnd2EndTest {
|
|||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||
AuthorizationServerInterceptor interceptor = createStaticAuthorizationInterceptor(policy);
|
||||
initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create());
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
}
|
||||
|
||||
|
|
@ -265,15 +304,14 @@ public class AuthorizationEnd2EndTest {
|
|||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||
AuthorizationServerInterceptor interceptor = createStaticAuthorizationInterceptor(policy);
|
||||
initServerWithAuthzInterceptor(interceptor, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -291,15 +329,14 @@ public class AuthorizationEnd2EndTest {
|
|||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
|
||||
AuthorizationServerInterceptor interceptor = createStaticAuthorizationInterceptor(policy);
|
||||
initServerWithAuthzInterceptor(interceptor, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -322,12 +359,13 @@ public class AuthorizationEnd2EndTest {
|
|||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
AuthorizationServerInterceptor interceptor = createStaticAuthorizationInterceptor(policy);
|
||||
ServerCredentials serverCredentials = TlsServerCredentials.newBuilder()
|
||||
.keyManager(serverCert0File, serverKey0File)
|
||||
.trustManager(caCertFile)
|
||||
.clientAuth(ClientAuth.REQUIRE)
|
||||
.build();
|
||||
initServerWithStaticAuthz(policy, serverCredentials);
|
||||
initServerWithAuthzInterceptor(interceptor, serverCredentials);
|
||||
ChannelCredentials channelCredentials = TlsChannelCredentials.newBuilder()
|
||||
.keyManager(clientCert0File, clientKey0File)
|
||||
.trustManager(caCertFile)
|
||||
|
|
@ -352,18 +390,405 @@ public class AuthorizationEnd2EndTest {
|
|||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
AuthorizationServerInterceptor interceptor = createStaticAuthorizationInterceptor(policy);
|
||||
ServerCredentials serverCredentials = TlsServerCredentials.newBuilder()
|
||||
.keyManager(serverCert0File, serverKey0File)
|
||||
.trustManager(caCertFile)
|
||||
.clientAuth(ClientAuth.OPTIONAL)
|
||||
.build();
|
||||
initServerWithStaticAuthz(policy, serverCredentials);
|
||||
initServerWithAuthzInterceptor(interceptor, serverCredentials);
|
||||
ChannelCredentials channelCredentials = TlsChannelCredentials.newBuilder()
|
||||
.trustManager(caCertFile)
|
||||
.build();
|
||||
getStub(channelCredentials).unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileWatcherAuthzAllowsRpcNoMatchInDenyMatchInAllowTest() 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\""
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
createTempAuthorizationPolicy(policy);
|
||||
FileWatcherAuthorizationServerInterceptor interceptor =
|
||||
createFileWatcherAuthorizationInterceptor(policyFile);
|
||||
Closeable closeable = interceptor.scheduleRefreshes(
|
||||
100, TimeUnit.MILLISECONDS, fakeClock.getScheduledExecutorService());
|
||||
initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create());
|
||||
closeable.close();
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileWatcherAuthzDeniesRpcNoMatchInDenyAndAllowTest() throws Exception {
|
||||
String policy = "{"
|
||||
+ " \"name\" : \"authz\" ,"
|
||||
+ " \"deny_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"deny_foo\","
|
||||
+ " \"source\": {"
|
||||
+ " \"principals\": ["
|
||||
+ " \"foo\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ],"
|
||||
+ " \"allow_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"allow_ClientStreamingRpc\","
|
||||
+ " \"request\": {"
|
||||
+ " \"paths\": ["
|
||||
+ " \"*/ClientStreamingRpc\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
createTempAuthorizationPolicy(policy);
|
||||
FileWatcherAuthorizationServerInterceptor interceptor =
|
||||
createFileWatcherAuthorizationInterceptor(policyFile);
|
||||
Closeable closeable = interceptor.scheduleRefreshes(
|
||||
100, TimeUnit.MILLISECONDS, fakeClock.getScheduledExecutorService());
|
||||
initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create());
|
||||
closeable.close();
|
||||
try {
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
fail("exception expected");
|
||||
} catch (StatusRuntimeException sre) {
|
||||
assertThat(sre).hasMessageThat().isEqualTo(
|
||||
"PERMISSION_DENIED: Access Denied");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileWatcherAuthzDeniesRpcMatchInDenyAndAllowTest() throws Exception {
|
||||
String policy = "{"
|
||||
+ " \"name\" : \"authz\" ,"
|
||||
+ " \"deny_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"deny_UnaryRpc\","
|
||||
+ " \"request\": {"
|
||||
+ " \"paths\": ["
|
||||
+ " \"*/UnaryRpc\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ],"
|
||||
+ " \"allow_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"allow_UnaryRpc\","
|
||||
+ " \"request\": {"
|
||||
+ " \"paths\": ["
|
||||
+ " \"*/UnaryRpc\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
createTempAuthorizationPolicy(policy);
|
||||
FileWatcherAuthorizationServerInterceptor interceptor =
|
||||
createFileWatcherAuthorizationInterceptor(policyFile);
|
||||
Closeable closeable = interceptor.scheduleRefreshes(
|
||||
100, TimeUnit.MILLISECONDS, fakeClock.getScheduledExecutorService());
|
||||
initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create());
|
||||
closeable.close();
|
||||
try {
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
fail("exception expected");
|
||||
} catch (StatusRuntimeException sre) {
|
||||
assertThat(sre).hasMessageThat().isEqualTo(
|
||||
"PERMISSION_DENIED: Access Denied");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileWatcherAuthzDeniesRpcMatchInDenyNoMatchInAllowTest() throws Exception {
|
||||
String policy = "{"
|
||||
+ " \"name\" : \"authz\" ,"
|
||||
+ " \"deny_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"deny_UnaryRpc\","
|
||||
+ " \"request\": {"
|
||||
+ " \"paths\": ["
|
||||
+ " \"*/UnaryRpc\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ],"
|
||||
+ " \"allow_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"allow_ClientStreamingRpc\","
|
||||
+ " \"request\": {"
|
||||
+ " \"paths\": ["
|
||||
+ " \"*/ClientStreamingRpc\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
createTempAuthorizationPolicy(policy);
|
||||
FileWatcherAuthorizationServerInterceptor interceptor =
|
||||
createFileWatcherAuthorizationInterceptor(policyFile);
|
||||
Closeable closeable = interceptor.scheduleRefreshes(
|
||||
100, TimeUnit.MILLISECONDS, fakeClock.getScheduledExecutorService());
|
||||
initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create());
|
||||
closeable.close();
|
||||
try {
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
fail("exception expected");
|
||||
} catch (StatusRuntimeException sre) {
|
||||
assertThat(sre).hasMessageThat().isEqualTo(
|
||||
"PERMISSION_DENIED: Access Denied");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileWatcherAuthzAllowsRpcEmptyDenyMatchInAllowTest() throws Exception {
|
||||
String policy = "{"
|
||||
+ " \"name\" : \"authz\" ,"
|
||||
+ " \"allow_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"allow_UnaryRpc\","
|
||||
+ " \"request\": {"
|
||||
+ " \"paths\": ["
|
||||
+ " \"*/UnaryRpc\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
createTempAuthorizationPolicy(policy);
|
||||
FileWatcherAuthorizationServerInterceptor interceptor =
|
||||
createFileWatcherAuthorizationInterceptor(policyFile);
|
||||
Closeable closeable = interceptor.scheduleRefreshes(
|
||||
100, TimeUnit.MILLISECONDS, fakeClock.getScheduledExecutorService());
|
||||
initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create());
|
||||
closeable.close();
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileWatcherAuthzDeniesRpcEmptyDenyNoMatchInAllowTest() throws Exception {
|
||||
String policy = "{"
|
||||
+ " \"name\" : \"authz\" ,"
|
||||
+ " \"allow_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"allow_ClientStreamingRpc\","
|
||||
+ " \"request\": {"
|
||||
+ " \"paths\": ["
|
||||
+ " \"*/ClientStreamingRpc\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
createTempAuthorizationPolicy(policy);
|
||||
FileWatcherAuthorizationServerInterceptor interceptor =
|
||||
createFileWatcherAuthorizationInterceptor(policyFile);
|
||||
Closeable closeable = interceptor.scheduleRefreshes(
|
||||
100, TimeUnit.MILLISECONDS, fakeClock.getScheduledExecutorService());
|
||||
initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create());
|
||||
closeable.close();
|
||||
try {
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
fail("exception expected");
|
||||
} catch (StatusRuntimeException sre) {
|
||||
assertThat(sre).hasMessageThat().isEqualTo(
|
||||
"PERMISSION_DENIED: Access Denied");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileWatcherAuthzValidPolicyRefreshTest() throws Exception {
|
||||
String policy1 = "{"
|
||||
+ " \"name\" : \"authz\" ,"
|
||||
+ " \"deny_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"deny_UnaryRpc\","
|
||||
+ " \"request\": {"
|
||||
+ " \"paths\": ["
|
||||
+ " \"*/UnaryRpc\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ],"
|
||||
+ " \"allow_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"allow_all\""
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
createTempAuthorizationPolicy(policy1);
|
||||
FileWatcherAuthorizationServerInterceptor interceptor =
|
||||
createFileWatcherAuthorizationInterceptor(policyFile);
|
||||
Closeable closeable = interceptor.scheduleRefreshes(
|
||||
100, TimeUnit.NANOSECONDS, fakeClock.getScheduledExecutorService());
|
||||
initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create());
|
||||
String policy2 = "{"
|
||||
+ " \"name\" : \"authz\" ,"
|
||||
+ " \"allow_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"allow_UnaryRpc\","
|
||||
+ " \"request\": {"
|
||||
+ " \"paths\": ["
|
||||
+ " \"*/UnaryRpc\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
rewriteAuthorizationPolicy(policy2);
|
||||
// Reload is yet to take place at 100ns. policy1 will be active here.
|
||||
assertEquals(0, fakeClock.forwardNanos(99));
|
||||
try {
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
fail("exception expected");
|
||||
} catch (StatusRuntimeException sre) {
|
||||
assertThat(sre).hasMessageThat().isEqualTo(
|
||||
"PERMISSION_DENIED: Access Denied");
|
||||
}
|
||||
// Reload will take place making policy2 the active policy.
|
||||
assertEquals(1, fakeClock.forwardNanos(2));
|
||||
closeable.close();
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileWatcherAuthzInvalidPolicySkipRefreshTest() throws Exception {
|
||||
String validPolicy = "{"
|
||||
+ " \"name\" : \"authz\" ,"
|
||||
+ " \"allow_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"allow_ClientStreamingRpc\","
|
||||
+ " \"request\": {"
|
||||
+ " \"paths\": ["
|
||||
+ " \"*/ClientStreamingRpc\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
createTempAuthorizationPolicy(validPolicy);
|
||||
FileWatcherAuthorizationServerInterceptor interceptor =
|
||||
createFileWatcherAuthorizationInterceptor(policyFile);
|
||||
Closeable closeable = interceptor.scheduleRefreshes(
|
||||
100, TimeUnit.NANOSECONDS, fakeClock.getScheduledExecutorService());
|
||||
initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create());
|
||||
String invalidPolicy = "{}";
|
||||
rewriteAuthorizationPolicy(invalidPolicy);
|
||||
// Reload is yet to take place at 100ns. validPolicy will be active here.
|
||||
assertEquals(0, fakeClock.forwardNanos(99));
|
||||
try {
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
fail("exception expected");
|
||||
} catch (StatusRuntimeException sre) {
|
||||
assertThat(sre).hasMessageThat().isEqualTo(
|
||||
"PERMISSION_DENIED: Access Denied");
|
||||
}
|
||||
// Reload will take place which skips the invalidPolicy. validPolicy remains
|
||||
// the active policy.
|
||||
assertEquals(1, fakeClock.forwardNanos(2));
|
||||
closeable.close();
|
||||
try {
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
fail("exception expected");
|
||||
} catch (StatusRuntimeException sre) {
|
||||
assertThat(sre).hasMessageThat().isEqualTo(
|
||||
"PERMISSION_DENIED: Access Denied");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileWatcherAuthzRecoversFromReloadTest() throws Exception {
|
||||
String validPolicy1 = "{"
|
||||
+ " \"name\" : \"authz\" ,"
|
||||
+ " \"allow_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"allow_ClientStreamingRpc\","
|
||||
+ " \"request\": {"
|
||||
+ " \"paths\": ["
|
||||
+ " \"*/ClientStreamingRpc\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
createTempAuthorizationPolicy(validPolicy1);
|
||||
FileWatcherAuthorizationServerInterceptor interceptor =
|
||||
createFileWatcherAuthorizationInterceptor(policyFile);
|
||||
Closeable closeable = interceptor.scheduleRefreshes(
|
||||
100, TimeUnit.NANOSECONDS, fakeClock.getScheduledExecutorService());
|
||||
initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create());
|
||||
String invalidPolicy = "{}";
|
||||
rewriteAuthorizationPolicy(invalidPolicy);
|
||||
// Reload is yet to take place at 100ns. validPolicy1 will be active here.
|
||||
assertEquals(0, fakeClock.forwardNanos(99));
|
||||
try {
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
fail("exception expected");
|
||||
} catch (StatusRuntimeException sre) {
|
||||
assertThat(sre).hasMessageThat().isEqualTo(
|
||||
"PERMISSION_DENIED: Access Denied");
|
||||
}
|
||||
// Reload will take place which skips the invalidPolicy. validPolicy1 remains
|
||||
// the active policy.
|
||||
assertEquals(1, fakeClock.forwardNanos(2));
|
||||
try {
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
fail("exception expected");
|
||||
} catch (StatusRuntimeException sre) {
|
||||
assertThat(sre).hasMessageThat().isEqualTo(
|
||||
"PERMISSION_DENIED: Access Denied");
|
||||
}
|
||||
String validPolicy2 = "{"
|
||||
+ " \"name\" : \"authz\" ,"
|
||||
+ " \"allow_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"allow_UnaryRpc\","
|
||||
+ " \"request\": {"
|
||||
+ " \"paths\": ["
|
||||
+ " \"*/UnaryRpc\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
rewriteAuthorizationPolicy(validPolicy2);
|
||||
// Next reload is yet to take place. validPolicy1 remains the active policy.
|
||||
assertEquals(0, fakeClock.forwardNanos(98));
|
||||
try {
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
fail("exception expected");
|
||||
} catch (StatusRuntimeException sre) {
|
||||
assertThat(sre).hasMessageThat().isEqualTo(
|
||||
"PERMISSION_DENIED: Access Denied");
|
||||
}
|
||||
// Reload occurs making validPolicy2 the active policy.
|
||||
assertEquals(1, fakeClock.forwardNanos(2));
|
||||
closeable.close();
|
||||
getStub().unaryRpc(SimpleRequest.getDefaultInstance());
|
||||
}
|
||||
|
||||
private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase {
|
||||
@Override
|
||||
public void unaryRpc(SimpleRequest req, StreamObserver<SimpleResponse> respOb) {
|
||||
|
|
|
|||
|
|
@ -48,8 +48,6 @@ public class AuthorizationPolicyTranslatorTest {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,8 +59,6 @@ public class AuthorizationPolicyTranslatorTest {
|
|||
fail("exception expected");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertThat(iae).hasMessageThat().isEqualTo("\"name\" is absent or empty");
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("the test failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,8 +71,6 @@ public class AuthorizationPolicyTranslatorTest {
|
|||
} catch (ClassCastException cce) {
|
||||
assertThat(cce).hasMessageThat().isEqualTo(
|
||||
"value '[abc]' for key 'name' in '{name=[abc]}' is not String");
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("the test failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,8 +82,6 @@ public class AuthorizationPolicyTranslatorTest {
|
|||
fail("exception expected");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertThat(iae).hasMessageThat().isEqualTo("\"allow_rules\" is absent");
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("the test failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,8 +98,6 @@ public class AuthorizationPolicyTranslatorTest {
|
|||
fail("exception expected");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertThat(iae).hasMessageThat().isEqualTo("rule \"name\" is absent or empty");
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("the test failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -173,8 +163,6 @@ public class AuthorizationPolicyTranslatorTest {
|
|||
} catch (ClassCastException cce) {
|
||||
assertThat(cce).hasMessageThat().isEqualTo(
|
||||
"value '{}' for key 'allow_rules' in '{name=abc, allow_rules={}}' is not List");
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("the test failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -281,8 +269,6 @@ public class AuthorizationPolicyTranslatorTest {
|
|||
fail("exception expected");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertThat(iae).hasMessageThat().isEqualTo("Unsupported \"key\" :method");
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("the test failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -311,8 +297,6 @@ public class AuthorizationPolicyTranslatorTest {
|
|||
fail("exception expected");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertThat(iae).hasMessageThat().isEqualTo("Unsupported \"key\" grpc-xxx");
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("the test failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -341,8 +325,6 @@ public class AuthorizationPolicyTranslatorTest {
|
|||
fail("exception expected");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertThat(iae).hasMessageThat().isEqualTo("Unsupported \"key\" Host");
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("the test failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -366,8 +348,6 @@ public class AuthorizationPolicyTranslatorTest {
|
|||
fail("exception expected");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertThat(iae).hasMessageThat().isEqualTo("\"key\" is absent or empty");
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("the test failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -393,8 +373,6 @@ public class AuthorizationPolicyTranslatorTest {
|
|||
fail("exception expected");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertThat(iae).hasMessageThat().isEqualTo("\"values\" is absent or empty");
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("the test failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -421,8 +399,6 @@ public class AuthorizationPolicyTranslatorTest {
|
|||
fail("exception expected");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertThat(iae).hasMessageThat().isEqualTo("\"values\" is absent or empty");
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("the test failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@ public class AuthorizationServerInterceptorTest {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class FileWatcherAuthorizationServerInterceptorTest {
|
||||
@Test
|
||||
public void invalidPolicyFailsAuthzInterceptorCreation() throws Exception {
|
||||
File policyFile = File.createTempFile("temp", "json");
|
||||
policyFile.deleteOnExit();
|
||||
String policy = "{ \"name\": \"abc\",, }";
|
||||
Files.write(Paths.get(policyFile.getAbsolutePath()), policy.getBytes(UTF_8));
|
||||
try {
|
||||
FileWatcherAuthorizationServerInterceptor.create(policyFile);
|
||||
fail("exception expected");
|
||||
} catch (IOException ioe) {
|
||||
assertThat(ioe).hasMessageThat().contains("malformed");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validPolicyCreatesFileWatcherAuthzInterceptor() throws Exception {
|
||||
File policyFile = File.createTempFile("temp", "json");
|
||||
policyFile.deleteOnExit();
|
||||
String policy = "{"
|
||||
+ " \"name\" : \"authz\","
|
||||
+ " \"deny_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"deny_foo\","
|
||||
+ " \"source\": {"
|
||||
+ " \"principals\": ["
|
||||
+ " \"spiffe://foo.com\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " }"
|
||||
+ " ],"
|
||||
+ " \"allow_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"allow_all\""
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
Files.write(Paths.get(policyFile.getAbsolutePath()), policy.getBytes(UTF_8));
|
||||
FileWatcherAuthorizationServerInterceptor interceptor =
|
||||
FileWatcherAuthorizationServerInterceptor.create(policyFile);
|
||||
assertNotNull(interceptor);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidRefreshIntervalFailsScheduleRefreshes() throws Exception {
|
||||
File policyFile = File.createTempFile("temp", "json");
|
||||
policyFile.deleteOnExit();
|
||||
String policy = "{"
|
||||
+ " \"name\" : \"authz\","
|
||||
+ " \"allow_rules\": ["
|
||||
+ " {"
|
||||
+ " \"name\": \"allow_all\""
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
Files.write(Paths.get(policyFile.getAbsolutePath()), policy.getBytes(UTF_8));
|
||||
FileWatcherAuthorizationServerInterceptor interceptor =
|
||||
FileWatcherAuthorizationServerInterceptor.create(policyFile);
|
||||
assertNotNull(interceptor);
|
||||
try {
|
||||
interceptor.scheduleRefreshes(0, TimeUnit.SECONDS,
|
||||
Executors.newSingleThreadScheduledExecutor());
|
||||
fail("exception expected");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertThat(iae).hasMessageThat().isEqualTo(
|
||||
"Refresh interval must be greater than 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue