diff --git a/authz/build.gradle b/authz/build.gradle index 50084752d0e..f2fd149be3c 100644 --- a/authz/build.gradle +++ b/authz/build.gradle @@ -17,7 +17,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' } diff --git a/authz/src/main/java/io/grpc/authz/AuthorizationServerInterceptor.java b/authz/src/main/java/io/grpc/authz/AuthorizationServerInterceptor.java index 2e398666093..2c01e356f59 100644 --- a/authz/src/main/java/io/grpc/authz/AuthorizationServerInterceptor.java +++ b/authz/src/main/java/io/grpc/authz/AuthorizationServerInterceptor.java @@ -35,7 +35,8 @@ * * gRPC Authorization policy 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 { diff --git a/authz/src/main/java/io/grpc/authz/FileWatcherAuthorizationServerInterceptor.java b/authz/src/main/java/io/grpc/authz/FileWatcherAuthorizationServerInterceptor.java new file mode 100644 index 00000000000..c474ead5aa5 --- /dev/null +++ b/authz/src/main/java/io/grpc/authz/FileWatcherAuthorizationServerInterceptor.java @@ -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 + * gRPC Authorization policy 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 ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, + ServerCallHandler 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); + } +} diff --git a/authz/src/test/java/io/grpc/authz/AuthorizationEnd2EndTest.java b/authz/src/test/java/io/grpc/authz/AuthorizationEnd2EndTest.java index 28c17718d11..949c41c56c3 100644 --- a/authz/src/test/java/io/grpc/authz/AuthorizationEnd2EndTest.java +++ b/authz/src/test/java/io/grpc/authz/AuthorizationEnd2EndTest.java @@ -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.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 @@ private void initServerWithStaticAuthz( .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 @@ private SimpleServiceGrpc.SimpleServiceBlockingStub getStub( @After public void tearDown() { + if (policyFile != null) { + policyFile.delete(); + } if (server != null) { server.shutdown(); } @@ -116,7 +156,8 @@ public void staticAuthzAllowsRpcNoMatchInDenyMatchInAllowTest() throws Exception + " }" + " ]" + "}"; - initServerWithStaticAuthz(policy, InsecureServerCredentials.create()); + AuthorizationServerInterceptor interceptor = createStaticAuthorizationInterceptor(policy); + initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create()); getStub().unaryRpc(SimpleRequest.getDefaultInstance()); } @@ -145,15 +186,14 @@ public void staticAuthzDeniesRpcNoMatchInDenyAndAllowTest() throws Exception { + " }" + " ]" + "}"; - 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 void staticAuthzDeniesRpcMatchInDenyAndAllowTest() throws Exception { + " }" + " ]" + "}"; - 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 void staticAuthzDeniesRpcMatchInDenyNoMatchInAllowTest() throws Exception + " }" + " ]" + "}"; - 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 void staticAuthzAllowsRpcEmptyDenyMatchInAllowTest() throws Exception { + " }" + " ]" + "}"; - initServerWithStaticAuthz(policy, InsecureServerCredentials.create()); + AuthorizationServerInterceptor interceptor = createStaticAuthorizationInterceptor(policy); + initServerWithAuthzInterceptor(interceptor, InsecureServerCredentials.create()); getStub().unaryRpc(SimpleRequest.getDefaultInstance()); } @@ -265,15 +304,14 @@ public void staticAuthzDeniesRpcEmptyDenyNoMatchInAllowTest() throws Exception { + " }" + " ]" + "}"; - 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 void staticAuthzDeniesRpcWithPrincipalsFieldOnUnauthenticatedConnectionTe + " }" + " ]" + "}"; - 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 void staticAuthzAllowsRpcWithPrincipalsFieldOnMtlsAuthenticatedConnection + " }" + " ]" + "}"; + 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 void staticAuthzAllowsRpcWithPrincipalsFieldOnTlsAuthenticatedConnectionT + " }" + " ]" + "}"; + 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 respOb) { diff --git a/authz/src/test/java/io/grpc/authz/AuthorizationPolicyTranslatorTest.java b/authz/src/test/java/io/grpc/authz/AuthorizationPolicyTranslatorTest.java index b957bf283e7..557458e97d7 100644 --- a/authz/src/test/java/io/grpc/authz/AuthorizationPolicyTranslatorTest.java +++ b/authz/src/test/java/io/grpc/authz/AuthorizationPolicyTranslatorTest.java @@ -48,8 +48,6 @@ public void invalidPolicy() throws Exception { 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 void missingAuthorizationPolicyName() throws Exception { 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 void incorrectAuthorizationPolicyName() throws Exception { } 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 void missingAllowRules() throws Exception { 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 void missingRuleName() throws Exception { 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 void incorrectRulesType() throws Exception { } 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 void unsupportedPseudoHeaders() throws Exception { 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 void unsupportedGrpcPrefixHeaders() throws Exception { 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 void unsupportedHostHeaders() throws Exception { 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 void missingHeaderKey() throws Exception { 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 void missingHeaderValues() throws Exception { 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 void emptyHeaderValues() throws Exception { 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); } } diff --git a/authz/src/test/java/io/grpc/authz/AuthorizationServerInterceptorTest.java b/authz/src/test/java/io/grpc/authz/AuthorizationServerInterceptorTest.java index 990228e2c96..b07a71bfb9f 100644 --- a/authz/src/test/java/io/grpc/authz/AuthorizationServerInterceptorTest.java +++ b/authz/src/test/java/io/grpc/authz/AuthorizationServerInterceptorTest.java @@ -38,8 +38,6 @@ public void invalidPolicyFailsStaticAuthzInterceptorCreation() throws Exception 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); } } diff --git a/authz/src/test/java/io/grpc/authz/FileWatcherAuthorizationServerInterceptorTest.java b/authz/src/test/java/io/grpc/authz/FileWatcherAuthorizationServerInterceptorTest.java new file mode 100644 index 00000000000..0ed63541fa6 --- /dev/null +++ b/authz/src/test/java/io/grpc/authz/FileWatcherAuthorizationServerInterceptorTest.java @@ -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"); + } + } +}