diff --git a/service/config/sample.yml b/service/config/sample.yml index c068a9886..1ceedd6be 100644 --- a/service/config/sample.yml +++ b/service/config/sample.yml @@ -337,17 +337,6 @@ appConfig: configuration: example remoteConfig: - authorizedUsers: - - # 1st authorized user - - # 2nd authorized user - - # ... - - # Nth authorized user - requiredHostedDomain: example.com - audiences: - - # 1st audience - - # 2nd audience - - # ... - - # Nth audience globalConfig: # keys and values that are given to clients on GET /v1/config EXAMPLE_KEY: VALUE diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 5cfc83c39..5b4b348b8 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -7,9 +7,6 @@ package org.whispersystems.textsecuregcm; import static com.codahale.metrics.MetricRegistry.name; import static java.util.Objects.requireNonNull; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; -import com.google.api.client.http.apache.v2.ApacheHttpTransport; -import com.google.api.client.json.gson.GsonFactory; import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.logging.LoggingOptions; import com.google.common.collect.ImmutableMap; @@ -808,14 +805,20 @@ public class WhisperServerService extends Application authorizedUsers, - @NotNull String requiredHostedDomain, - @NotNull @NotEmpty List audiences, - @NotNull Map globalConfig) { +public record RemoteConfigConfiguration(@NotNull Map globalConfig) { } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java index dd970a0f7..65ea8e2e2 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java @@ -5,8 +5,6 @@ package org.whispersystems.textsecuregcm.controllers; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; import com.google.common.annotations.VisibleForTesting; import io.dropwizard.auth.Auth; import io.swagger.v3.oas.annotations.tags.Tag; @@ -15,35 +13,18 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Clock; -import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.annotation.Nullable; -import javax.validation.Valid; -import javax.validation.constraints.NotNull; -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; import javax.ws.rs.GET; -import javax.ws.rs.HeaderParam; -import javax.ws.rs.PUT; import javax.ws.rs.Path; -import javax.ws.rs.PathParam; import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import org.signal.event.AdminEventLogger; -import org.signal.event.RemoteConfigDeleteEvent; -import org.signal.event.RemoteConfigSetEvent; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.entities.UserRemoteConfig; import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList; -import org.whispersystems.textsecuregcm.storage.RemoteConfig; import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager; import org.whispersystems.textsecuregcm.util.Conversions; import org.whispersystems.textsecuregcm.util.Util; @@ -53,30 +34,18 @@ import org.whispersystems.textsecuregcm.util.Util; public class RemoteConfigController { private final RemoteConfigsManager remoteConfigsManager; - private final AdminEventLogger adminEventLogger; - private final Set configAuthUsers; private final Map globalConfig; - private final String requiredHostedDomain; - - private final GoogleIdTokenVerifier googleIdTokenVerifier; - private final Clock clock; private static final String GLOBAL_CONFIG_PREFIX = "global."; - public RemoteConfigController(RemoteConfigsManager remoteConfigsManager, AdminEventLogger adminEventLogger, - Set configAuthUsers, String requiredHostedDomain, List audience, - final GoogleIdTokenVerifier.Builder googleIdTokenVerifierBuilder, Map globalConfig, + public RemoteConfigController(RemoteConfigsManager remoteConfigsManager, + Map globalConfig, final Clock clock) { this.remoteConfigsManager = remoteConfigsManager; - this.adminEventLogger = Objects.requireNonNull(adminEventLogger); - this.configAuthUsers = configAuthUsers; this.globalConfig = globalConfig; - this.requiredHostedDomain = requiredHostedDomain; - this.googleIdTokenVerifier = googleIdTokenVerifierBuilder.setAudience(audience).build(); - this.clock = clock; } @@ -101,68 +70,6 @@ public class RemoteConfigController { } } - @PUT - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public void set(@HeaderParam("Config-Token") String configToken, @NotNull @Valid RemoteConfig config) { - - final String authIdentity = getAuthIdentity(configToken) - .orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED)); - - if (config.getName().startsWith(GLOBAL_CONFIG_PREFIX)) { - throw new WebApplicationException(Response.Status.FORBIDDEN); - } - - adminEventLogger.logEvent( - new RemoteConfigSetEvent( - authIdentity, - config.getName(), - config.getPercentage(), - config.getDefaultValue(), - config.getValue(), - config.getHashKey(), - config.getUuids().stream().map(UUID::toString).collect(Collectors.toList()))); - remoteConfigsManager.set(config); - } - - @DELETE - @Path("/{name}") - public void delete(@HeaderParam("Config-Token") String configToken, @PathParam("name") String name) { - final String authIdentity = getAuthIdentity(configToken) - .orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED)); - - if (name.startsWith(GLOBAL_CONFIG_PREFIX)) { - throw new WebApplicationException(Response.Status.FORBIDDEN); - } - - adminEventLogger.logEvent(new RemoteConfigDeleteEvent(authIdentity, name)); - remoteConfigsManager.delete(name); - } - - private Optional getAuthIdentity(String token) { - return getAuthorizedGoogleIdentity(token) - .map(googleIdToken -> googleIdToken.getPayload().getEmail()); - } - - private Optional getAuthorizedGoogleIdentity(String token) { - try { - final @Nullable GoogleIdToken googleIdToken = googleIdTokenVerifier.verify(token); - - if (googleIdToken != null - && googleIdToken.getPayload().getHostedDomain().equals(requiredHostedDomain) - && googleIdToken.getPayload().getEmailVerified() - && configAuthUsers.contains(googleIdToken.getPayload().getEmail())) { - - return Optional.of(googleIdToken); - } - - return Optional.empty(); - - } catch (final Exception ignored) { - return Optional.empty(); - } - } - @VisibleForTesting public static boolean isInBucket(MessageDigest digest, UUID uid, byte[] hashKey, int configPercentage, Set uuidsInBucket) { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigControllerTest.java index a24ab4531..e5957899d 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigControllerTest.java @@ -6,8 +6,6 @@ package org.whispersystems.textsecuregcm.controllers; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; @@ -15,8 +13,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; import com.google.common.collect.ImmutableSet; import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; @@ -24,7 +20,6 @@ import io.dropwizard.testing.junit5.ResourceExtension; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Instant; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -32,8 +27,6 @@ import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.assertj.core.api.Assertions; import org.assertj.core.api.InstanceOfAssertFactory; @@ -42,8 +35,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.signal.event.NoOpAdminEventLogger; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; import org.whispersystems.textsecuregcm.entities.UserRemoteConfig; @@ -59,18 +50,6 @@ class RemoteConfigControllerTest { private static final RemoteConfigsManager remoteConfigsManager = mock(RemoteConfigsManager.class); - private static final Set remoteConfigsUsers = Set.of("user1@example.com", "user2@example.com"); - - private static final String requiredHostedDomain = "example.com"; - private static final GoogleIdTokenVerifier.Builder googleIdVerificationTokenBuilder = mock( - GoogleIdTokenVerifier.Builder.class); - private static final GoogleIdTokenVerifier googleIdTokenVerifier = mock(GoogleIdTokenVerifier.class); - - static { - when(googleIdVerificationTokenBuilder.setAudience(any())).thenReturn(googleIdVerificationTokenBuilder); - when(googleIdVerificationTokenBuilder.build()).thenReturn(googleIdTokenVerifier); - } - private static final long PINNED_EPOCH_SECONDS = 1701287216L; private static final TestClock TEST_CLOCK = TestClock.pinned(Instant.ofEpochSecond(PINNED_EPOCH_SECONDS)); @@ -81,9 +60,7 @@ class RemoteConfigControllerTest { ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) .addProvider(new DeviceLimitExceededExceptionMapper()) - .addResource(new RemoteConfigController(remoteConfigsManager, new NoOpAdminEventLogger(), remoteConfigsUsers, - requiredHostedDomain, Collections.singletonList("aud.example.com"), - googleIdVerificationTokenBuilder, Map.of("maxGroupSize", "42"), TEST_CLOCK)) + .addResource(new RemoteConfigController(remoteConfigsManager, Map.of("maxGroupSize", "42"), TEST_CLOCK)) .build(); @@ -103,23 +80,6 @@ class RemoteConfigControllerTest { add(new RemoteConfig("unlinked.config", 50, Set.of(), null, null, null)); }}); - final Map googleIdTokens = new HashMap<>(); - - for (int i = 1; i <= 3; i++) { - final String user = "user" + i; - final GoogleIdToken googleIdToken = mock(GoogleIdToken.class); - final GoogleIdToken.Payload payload = mock(GoogleIdToken.Payload.class); - when(googleIdToken.getPayload()).thenReturn(payload); - - when(payload.getEmail()).thenReturn(user + "@" + requiredHostedDomain); - when(payload.getEmailVerified()).thenReturn(true); - when(payload.getHostedDomain()).thenReturn(requiredHostedDomain); - - googleIdTokens.put(user + ".valid", googleIdToken); - } - - when(googleIdTokenVerifier.verify(anyString())) - .thenAnswer(answer -> googleIdTokens.get(answer.getArgument(0, String.class))); } @AfterEach @@ -244,188 +204,6 @@ class RemoteConfigControllerTest { verifyNoMoreInteractions(remoteConfigsManager); } - @Test - void testSetConfig() { - Response response = resources.getJerseyTest() - .target("/v1/config") - .request() - .header("Config-Token", "user1.valid") - .put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(204); - - ArgumentCaptor captor = ArgumentCaptor.forClass(RemoteConfig.class); - - verify(remoteConfigsManager, times(1)).set(captor.capture()); - - assertThat(captor.getValue().getName()).isEqualTo("android.stickers"); - assertThat(captor.getValue().getPercentage()).isEqualTo(88); - assertThat(captor.getValue().getUuids()).isEmpty(); - } - - @Test - void testSetConfigValued() { - Response response = resources.getJerseyTest() - .target("/v1/config") - .request() - .header("Config-Token", "user1.valid") - .put(Entity.entity(new RemoteConfig("value.sometimes", 50, Set.of(), "a", "b", null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(204); - - ArgumentCaptor captor = ArgumentCaptor.forClass(RemoteConfig.class); - - verify(remoteConfigsManager, times(1)).set(captor.capture()); - - assertThat(captor.getValue().getName()).isEqualTo("value.sometimes"); - assertThat(captor.getValue().getPercentage()).isEqualTo(50); - assertThat(captor.getValue().getUuids()).isEmpty(); - } - - @Test - void testSetConfigWithHashKey() { - final String configToken = "user1.valid"; - Response response1 = resources.getJerseyTest() - .target("/v1/config") - .request() - .header("Config-Token", configToken) - .put(Entity.entity(new RemoteConfig("linked.config.0", 50, Set.of(), "FALSE", "TRUE", null), - MediaType.APPLICATION_JSON_TYPE)); - assertThat(response1.getStatus()).isEqualTo(204); - - Response response2 = resources.getJerseyTest() - .target("/v1/config") - .request() - .header("Config-Token", configToken) - .put(Entity.entity(new RemoteConfig("linked.config.1", 50, Set.of(), "FALSE", "TRUE", "linked.config.0"), MediaType.APPLICATION_JSON_TYPE)); - assertThat(response2.getStatus()).isEqualTo(204); - - ArgumentCaptor captor = ArgumentCaptor.forClass(RemoteConfig.class); - - verify(remoteConfigsManager, times(2)).set(captor.capture()); - assertThat(captor.getAllValues()).hasSize(2); - - final RemoteConfig capture1 = captor.getAllValues().get(0); - assertThat(capture1).isNotNull(); - assertThat(capture1.getName()).isEqualTo("linked.config.0"); - assertThat(capture1.getPercentage()).isEqualTo(50); - assertThat(capture1.getUuids()).isEmpty(); - assertThat(capture1.getHashKey()).isNull(); - - final RemoteConfig capture2 = captor.getAllValues().get(1); - assertThat(capture2).isNotNull(); - assertThat(capture2.getName()).isEqualTo("linked.config.1"); - assertThat(capture2.getPercentage()).isEqualTo(50); - assertThat(capture2.getUuids()).isEmpty(); - assertThat(capture2.getHashKey()).isEqualTo("linked.config.0"); - } - - @Test - void testSetConfigUnauthorized() { - Response response = resources.getJerseyTest() - .target("/v1/config") - .request() - .header("Config-Token", "user3.valid") - .put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(401); - - verifyNoMoreInteractions(remoteConfigsManager); - } - - @Test - void testSetConfigMissingUnauthorized() { - Response response = resources.getJerseyTest() - .target("/v1/config") - .request() - .put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(401); - - verifyNoMoreInteractions(remoteConfigsManager); - } - - @Test - void testSetConfigBadName() { - Response response = resources.getJerseyTest() - .target("/v1/config") - .request() - .header("Config-Token", "user1.valid") - .put(Entity.entity(new RemoteConfig("android-stickers", 88, Set.of(), "FALSE", "TRUE", null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(422); - - verifyNoMoreInteractions(remoteConfigsManager); - } - - @Test - void testSetConfigEmptyName() { - Response response = resources.getJerseyTest() - .target("/v1/config") - .request() - .header("Config-Token", "user1.valid") - .put(Entity.entity(new RemoteConfig("", 88, Set.of(), "FALSE", "TRUE", null), MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(422); - - verifyNoMoreInteractions(remoteConfigsManager); - } - - @Test - void testSetGlobalConfig() { - Response response = resources.getJerseyTest() - .target("/v1/config") - .request() - .header("Config-Token", "user1.valid") - .put(Entity.entity(new RemoteConfig("global.maxGroupSize", 88, Set.of(), "FALSE", "TRUE", null), - MediaType.APPLICATION_JSON_TYPE)); - assertThat(response.getStatus()).isEqualTo(403); - verifyNoMoreInteractions(remoteConfigsManager); - } - - @Test - void testDelete() { - Response response = resources.getJerseyTest() - .target("/v1/config/android.stickers") - .request() - .header("Config-Token", "user1.valid") - .delete(); - - assertThat(response.getStatus()).isEqualTo(204); - - verify(remoteConfigsManager, times(1)).delete("android.stickers"); - verifyNoMoreInteractions(remoteConfigsManager); - } - - @Test - void testDeleteUnauthorized() { - Response response = resources.getJerseyTest() - .target("/v1/config/android.stickers") - .request() - .header("Config-Token", "baz") - .delete(); - - assertThat(response.getStatus()).isEqualTo(401); - - verifyNoMoreInteractions(remoteConfigsManager); - } - - @Test - void testDeleteGlobalConfig() { - Response response = resources.getJerseyTest() - .target("/v1/config/global.maxGroupSize") - .request() - .header("Config-Token", "user1.valid") - .delete(); - assertThat(response.getStatus()).isEqualTo(403); - verifyNoMoreInteractions(remoteConfigsManager); - } - @Test void testMath() throws NoSuchAlgorithmException { List remoteConfigList = remoteConfigsManager.getAll(); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsManagerTest.java index 1cb849d8f..403069ddb 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsManagerTest.java @@ -31,24 +31,4 @@ class RemoteConfigsManagerTest { // A memoized supplier should prevent multiple calls to the underlying data source verify(remoteConfigs, times(1)).getAll(); } - - @Test - void testSet() { - final RemoteConfig remoteConfig = mock(RemoteConfig.class); - - remoteConfigsManager.set(remoteConfig); - remoteConfigsManager.set(remoteConfig); - - verify(remoteConfigs, times(2)).set(remoteConfig); - } - - @Test - void testDelete() { - final String name = "name"; - - remoteConfigsManager.delete(name); - remoteConfigsManager.delete(name); - - verify(remoteConfigs, times(2)).delete(name); - } }