From b97158bf7b75759f6a17279cc6c6b45da1f938ac Mon Sep 17 00:00:00 2001 From: Ehren Kret Date: Mon, 10 Aug 2020 16:31:15 -0500 Subject: [PATCH] Create global remote config controllable in the signal server configuration (#127) * Add global config controller through file rather than database * Do no permit attempting to set or delete global config entries --- service/config/sample.yml | 8 +++++ .../textsecuregcm/WhisperServerService.java | 2 +- .../RemoteConfigConfiguration.java | 10 ++++++ .../controllers/RemoteConfigController.java | 21 ++++++++++--- .../RemoteConfigControllerTest.java | 31 ++++++++++++++++--- 5 files changed, 63 insertions(+), 9 deletions(-) diff --git a/service/config/sample.yml b/service/config/sample.yml index ada3b37af..2aacea159 100644 --- a/service/config/sample.yml +++ b/service/config/sample.yml @@ -127,3 +127,11 @@ micrometer: # Micrometer metrics config - uri: "https://metrics.example.com/" - apiKey: - accountId: + +remoteConfig: + authorizedTokens: + - # 1st authorized token + - # 2nd authorized token + - # ... + - # Nth authorized token + globalConfig: # keys and values that are given to clients on GET /v1/config diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index fd59249f9..bac4774cc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -417,7 +417,7 @@ public class WhisperServerService extends Application accountAuthFilter = new BasicCredentialAuthFilter.Builder().setAuthenticator(accountAuthenticator).buildAuthFilter (); AuthFilter disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder().setAuthenticator(disabledPermittedAccountAuthenticator).buildAuthFilter(); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RemoteConfigConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RemoteConfigConfiguration.java index c55c2ab89..478aa5858 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RemoteConfigConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RemoteConfigConfiguration.java @@ -3,8 +3,10 @@ package org.whispersystems.textsecuregcm.configuration; import com.fasterxml.jackson.annotation.JsonProperty; import javax.validation.constraints.NotNull; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; public class RemoteConfigConfiguration { @@ -12,7 +14,15 @@ public class RemoteConfigConfiguration { @NotNull private List authorizedTokens = new LinkedList<>(); + @NotNull + @JsonProperty + private Map globalConfig = new HashMap<>(); + public List getAuthorizedTokens() { return authorizedTokens; } + + public Map getGlobalConfig() { + return 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 b75a90860..2277e6515 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java @@ -27,19 +27,23 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import java.util.stream.Stream; @Path("/v1/config") public class RemoteConfigController { private final RemoteConfigsManager remoteConfigsManager; private final List configAuthTokens; + private final Map globalConfig; - public RemoteConfigController(RemoteConfigsManager remoteConfigsManager, List configAuthTokens) { + public RemoteConfigController(RemoteConfigsManager remoteConfigsManager, List configAuthTokens, Map globalConfig) { this.remoteConfigsManager = remoteConfigsManager; - this.configAuthTokens = configAuthTokens; + this.configAuthTokens = configAuthTokens; + this.globalConfig = globalConfig; } @Timed @@ -50,11 +54,12 @@ public class RemoteConfigController { try { MessageDigest digest = MessageDigest.getInstance("SHA1"); - return new UserRemoteConfigList(remoteConfigsManager.getAll().stream().map(config -> { + final Stream globalConfigStream = globalConfig.entrySet().stream().map(entry -> new UserRemoteConfig("g." + entry.getKey(), true, entry.getValue())); + return new UserRemoteConfigList(Stream.concat(remoteConfigsManager.getAll().stream().map(config -> { final byte[] hashKey = config.getHashKey() != null ? config.getHashKey().getBytes(StandardCharsets.UTF_8) : config.getName().getBytes(StandardCharsets.UTF_8); boolean inBucket = isInBucket(digest, account.getUuid(), hashKey, config.getPercentage(), config.getUuids()); return new UserRemoteConfig(config.getName(), inBucket, inBucket ? config.getValue() : config.getDefaultValue()); - }).collect(Collectors.toList())); + }), globalConfigStream).collect(Collectors.toList())); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } @@ -69,6 +74,10 @@ public class RemoteConfigController { throw new WebApplicationException(Response.Status.UNAUTHORIZED); } + if (config.getName().startsWith("g.")) { + throw new WebApplicationException(Response.Status.FORBIDDEN); + } + remoteConfigsManager.set(config); } @@ -80,6 +89,10 @@ public class RemoteConfigController { throw new WebApplicationException(Response.Status.UNAUTHORIZED); } + if (name.startsWith("g.")) { + throw new WebApplicationException(Response.Status.FORBIDDEN); + } + remoteConfigsManager.delete(name); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/RemoteConfigControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/RemoteConfigControllerTest.java index 0341efe30..608c5f087 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/RemoteConfigControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/RemoteConfigControllerTest.java @@ -49,7 +49,7 @@ public class RemoteConfigControllerTest { .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class))) .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) .addProvider(new DeviceLimitExceededExceptionMapper()) - .addResource(new RemoteConfigController(remoteConfigsManager, remoteConfigsAuth)) + .addResource(new RemoteConfigController(remoteConfigsManager, remoteConfigsAuth, Map.of("maxGroupSize", "42"))) .build(); @@ -79,7 +79,7 @@ public class RemoteConfigControllerTest { verify(remoteConfigsManager, times(1)).getAll(); - assertThat(configuration.getConfig()).hasSize(10); + assertThat(configuration.getConfig()).hasSize(11); assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers"); assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers"); assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true"); @@ -100,6 +100,7 @@ public class RemoteConfigControllerTest { assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0"); assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1"); assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config"); + assertThat(configuration.getConfig().get(10).getName()).isEqualTo("g.maxGroupSize"); } @Test @@ -112,7 +113,7 @@ public class RemoteConfigControllerTest { verify(remoteConfigsManager, times(1)).getAll(); - assertThat(configuration.getConfig()).hasSize(10); + assertThat(configuration.getConfig()).hasSize(11); assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers"); assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers"); assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true"); @@ -133,6 +134,7 @@ public class RemoteConfigControllerTest { assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0"); assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1"); assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config"); + assertThat(configuration.getConfig().get(10).getName()).isEqualTo("g.maxGroupSize"); } @Test @@ -140,7 +142,7 @@ public class RemoteConfigControllerTest { boolean allUnlinkedConfigsMatched = true; for (AuthHelper.TestAccount testAccount : AuthHelper.TEST_ACCOUNTS) { UserRemoteConfigList configuration = resources.getJerseyTest().target("/v1/config/").request().header("Authorization", testAccount.getAuthHeader()).get(UserRemoteConfigList.class); - assertThat(configuration.getConfig()).hasSize(10); + assertThat(configuration.getConfig()).hasSize(11); final UserRemoteConfig linkedConfig0 = configuration.getConfig().get(7); assertThat(linkedConfig0.getName()).isEqualTo("linked.config.0"); @@ -297,6 +299,16 @@ public class RemoteConfigControllerTest { verifyNoMoreInteractions(remoteConfigsManager); } + @Test + public void testSetGlobalConfig() { + Response response = resources.getJerseyTest() + .target("/v1/config") + .request() + .header("Config-Token", "foo") + .put(Entity.entity(new RemoteConfig("g.maxGroupSize", 88, Set.of(), "FALSE", "TRUE", null), MediaType.APPLICATION_JSON_TYPE)); + assertThat(response.getStatus()).isEqualTo(403); + verifyNoMoreInteractions(remoteConfigsManager); + } @Test public void testDelete() { @@ -325,6 +337,17 @@ public class RemoteConfigControllerTest { verifyNoMoreInteractions(remoteConfigsManager); } + @Test + public void testDeleteGlobalConfig() { + Response response = resources.getJerseyTest() + .target("/v1/config/g.maxGroupSize") + .request() + .header("Config-Token", "foo") + .delete(); + assertThat(response.getStatus()).isEqualTo(403); + verifyNoMoreInteractions(remoteConfigsManager); + } + @Test public void testMath() throws NoSuchAlgorithmException { List remoteConfigList = remoteConfigsManager.getAll();