diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java index 00cb5ea4b..406a7f102 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java @@ -31,7 +31,7 @@ import org.whispersystems.textsecuregcm.configuration.DeviceCheckConfiguration; import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration; import org.whispersystems.textsecuregcm.configuration.DynamoDbClientFactory; import org.whispersystems.textsecuregcm.configuration.DynamoDbTables; -import org.whispersystems.textsecuregcm.configuration.GrpcAllowListConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicGrpcAllowListConfiguration; import org.whispersystems.textsecuregcm.configuration.ExternalRequestFilterConfiguration; import org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClientFactory; import org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClusterFactory; @@ -352,7 +352,7 @@ public class WhisperServerConfiguration extends Configuration { @NotNull @Valid @JsonProperty - private GrpcAllowListConfiguration grpcAllowList = new GrpcAllowListConfiguration(); + private DynamicGrpcAllowListConfiguration grpcAllowList = new DynamicGrpcAllowListConfiguration(); @Valid @NotNull @@ -595,7 +595,7 @@ public class WhisperServerConfiguration extends Configuration { return grpc; } - public GrpcAllowListConfiguration getGrpcAllowList() { + public DynamicGrpcAllowListConfiguration getGrpcAllowList() { return grpcAllowList; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 89b3ba974..cfadf7035 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -882,8 +882,7 @@ public class WhisperServerService extends Application getExperimentEnrollmentConfiguration( final String experimentName) { return Optional.ofNullable(experiments.get(experimentName)); @@ -129,4 +133,8 @@ public class DynamicConfiguration { public DynamicCarrierDataLookupConfiguration getCarrierDataLookupConfiguration() { return carrierDataLookup; } + + public DynamicGrpcAllowListConfiguration getGrpcAllowList() { + return grpcAllowList; + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GrpcAllowListConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicGrpcAllowListConfiguration.java similarity index 69% rename from service/src/main/java/org/whispersystems/textsecuregcm/configuration/GrpcAllowListConfiguration.java rename to service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicGrpcAllowListConfiguration.java index bf8ee3d9f..1ad34de60 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GrpcAllowListConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicGrpcAllowListConfiguration.java @@ -2,10 +2,11 @@ * Copyright 2026 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ -package org.whispersystems.textsecuregcm.configuration; +package org.whispersystems.textsecuregcm.configuration.dynamic; import java.util.Collections; import java.util.List; +import java.util.Set; /// Configure which gRPC methods are enabled /// @@ -16,22 +17,22 @@ import java.util.List; /// @param enabledMethods A list of fully qualified method names of RPCs that should be enabled. For example, /// `org.signal.chat.account.AccountsAnonymous/LookupUsernameHash` would enable the /// `LookupUsernameHash` RPC method -public record GrpcAllowListConfiguration( +public record DynamicGrpcAllowListConfiguration( boolean enableAll, - List enabledServices, - List enabledMethods) { + Set enabledServices, + Set enabledMethods) { - public GrpcAllowListConfiguration { + public DynamicGrpcAllowListConfiguration { if (enabledServices == null) { - enabledServices = Collections.emptyList(); + enabledServices = Collections.emptySet(); } if (enabledMethods == null) { - enabledMethods = Collections.emptyList(); + enabledMethods = Collections.emptySet(); } } - public GrpcAllowListConfiguration() { + public DynamicGrpcAllowListConfiguration() { // By default, no GRPC methods are accessible - this(false, Collections.emptyList(), Collections.emptyList()); + this(false, Collections.emptySet(), Collections.emptySet()); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/GrpcAllowListInterceptor.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/GrpcAllowListInterceptor.java index a4b43a750..afcdaad65 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/GrpcAllowListInterceptor.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/GrpcAllowListInterceptor.java @@ -10,31 +10,28 @@ import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Status; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicGrpcAllowListConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; public class GrpcAllowListInterceptor implements ServerInterceptor { - private final boolean enableAll; - private final Set enabledServices; - private final Set enabledMethods; + private final DynamicConfigurationManager dynamicConfigurationManager; public GrpcAllowListInterceptor( - final boolean enableAll, - final List enabledServices, - final List enabledMethods) { - this.enableAll = enableAll; - this.enabledServices = new HashSet<>(enabledServices); - this.enabledMethods = new HashSet<>(enabledMethods); + final DynamicConfigurationManager dynamicConfigurationManager) { + this.dynamicConfigurationManager = dynamicConfigurationManager; } @Override public ServerCall.Listener interceptCall(final ServerCall serverCall, final Metadata metadata, final ServerCallHandler next) { + final DynamicGrpcAllowListConfiguration allowList = this.dynamicConfigurationManager.getConfiguration().getGrpcAllowList(); final MethodDescriptor methodDescriptor = serverCall.getMethodDescriptor(); - if (!enableAll && !enabledServices.contains(methodDescriptor.getServiceName()) && !enabledMethods.contains(methodDescriptor.getFullMethodName())) { + if (!allowList.enableAll() && + !allowList.enabledServices().contains(methodDescriptor.getServiceName()) && + !allowList.enabledMethods().contains(methodDescriptor.getFullMethodName())) { return ServerInterceptorUtil.closeWithStatus(serverCall, Status.UNIMPLEMENTED); } return next.startCall(serverCall, metadata); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcAllowListInterceptorTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcAllowListInterceptorTest.java index c1c60643c..813b49367 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcAllowListInterceptorTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcAllowListInterceptorTest.java @@ -5,6 +5,8 @@ package org.whispersystems.textsecuregcm.grpc; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.google.protobuf.ByteString; import io.grpc.ManagedChannel; @@ -14,7 +16,7 @@ import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import java.io.IOException; import java.util.Collections; -import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -22,6 +24,10 @@ import org.junit.jupiter.api.Test; import org.signal.chat.rpc.EchoRequest; import org.signal.chat.rpc.EchoResponse; import org.signal.chat.rpc.EchoServiceGrpc; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicGrpcAllowListConfiguration; +import org.whispersystems.textsecuregcm.tests.util.FakeDynamicConfigurationManager; + class GrpcAllowListInterceptorTest { private Server server; @@ -45,7 +51,7 @@ class GrpcAllowListInterceptorTest { @Test public void disableAll() throws Exception { final EchoServiceGrpc.EchoServiceBlockingStub client = - setup(false, Collections.emptyList(), Collections.emptyList()); + setup(false, Collections.emptySet(), Collections.emptySet()); GrpcTestUtils.assertStatusException(Status.UNIMPLEMENTED, () -> client.echo(EchoRequest.newBuilder().setPayload(ByteString.empty()).build())); } @@ -53,7 +59,7 @@ class GrpcAllowListInterceptorTest { @Test public void enableAll() throws Exception { final EchoServiceGrpc.EchoServiceBlockingStub client = - setup(true, Collections.emptyList(), Collections.emptyList()); + setup(true, Collections.emptySet(), Collections.emptySet()); final EchoResponse echo = client.echo(EchoRequest.newBuilder().setPayload(ByteString.empty()).build()); assertThat(echo.getPayload()).isEqualTo(ByteString.empty()); } @@ -61,7 +67,7 @@ class GrpcAllowListInterceptorTest { @Test public void enableByMethod() throws Exception { final EchoServiceGrpc.EchoServiceBlockingStub client = - setup(false, Collections.emptyList(), List.of("org.signal.chat.rpc.EchoService/echo")); + setup(false, Collections.emptySet(), Set.of("org.signal.chat.rpc.EchoService/echo")); final EchoResponse echo = client.echo(EchoRequest.newBuilder().setPayload(ByteString.empty()).build()); assertThat(echo.getPayload()).isEqualTo(ByteString.empty()); @@ -73,7 +79,7 @@ class GrpcAllowListInterceptorTest { @Test public void enableByService() throws Exception { final EchoServiceGrpc.EchoServiceBlockingStub client = - setup(false, List.of("org.signal.chat.rpc.EchoService"), Collections.emptyList()); + setup(false, Set.of("org.signal.chat.rpc.EchoService"), Collections.emptySet()); final EchoResponse echo = client.echo(EchoRequest.newBuilder().setPayload(ByteString.empty()).build()); assertThat(echo.getPayload()).isEqualTo(ByteString.empty()); @@ -85,7 +91,7 @@ class GrpcAllowListInterceptorTest { @Test public void enableByServiceWrongService() throws Exception { final EchoServiceGrpc.EchoServiceBlockingStub client = - setup(false, List.of("org.signal.chat.rpc.NotEchoService"), Collections.emptyList()); + setup(false, Set.of("org.signal.chat.rpc.NotEchoService"), Collections.emptySet()); GrpcTestUtils.assertStatusException(Status.UNIMPLEMENTED, () -> client.echo(EchoRequest.newBuilder().setPayload(ByteString.empty()).build())); @@ -93,16 +99,19 @@ class GrpcAllowListInterceptorTest { private EchoServiceGrpc.EchoServiceBlockingStub setup( boolean enableAll, - List enabledServices, - List enabledMethods) + Set enabledServices, + Set enabledMethods) throws IOException { if (server != null) { server.shutdownNow(); } + final DynamicConfiguration configuration = mock(DynamicConfiguration.class); + when(configuration.getGrpcAllowList()) + .thenReturn(new DynamicGrpcAllowListConfiguration(enableAll, enabledServices, enabledMethods)); server = InProcessServerBuilder.forName("GrpcAllowListInterceptorTest") .directExecutor() .addService(new EchoServiceImpl()) - .intercept(new GrpcAllowListInterceptor(enableAll, enabledServices, enabledMethods)) + .intercept(new GrpcAllowListInterceptor(new FakeDynamicConfigurationManager<>(configuration))) .build() .start();