Remove PUT/DELETE methods from RemoteConfigController

This commit is contained in:
Chris Eager 2023-11-29 16:18:57 -06:00 committed by Jon Chambers
parent 664f9f36e1
commit e084a9f2b6
6 changed files with 15 additions and 370 deletions

View File

@ -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

View File

@ -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<WhisperServerConfiguration
new AccountControllerV2(accountsManager, changeNumberManager, phoneVerificationTokenManager,
registrationLockVerificationManager, rateLimiters),
new ArtController(rateLimiters, artCredentialsGenerator),
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().accessKey().value(), config.getAwsAttachmentsConfiguration().accessSecret().value(), config.getAwsAttachmentsConfiguration().region(), config.getAwsAttachmentsConfiguration().bucket()),
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().accessKey().value(),
config.getAwsAttachmentsConfiguration().accessSecret().value(),
config.getAwsAttachmentsConfiguration().region(), config.getAwsAttachmentsConfiguration().bucket()),
new AttachmentControllerV3(rateLimiters, gcsAttachmentGenerator),
new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, new TusAttachmentGenerator(config.getTus()), experimentEnrollmentManager),
new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, new TusAttachmentGenerator(config.getTus()),
experimentEnrollmentManager),
new ArchiveController(backupAuthManager, backupManager),
new CallLinkController(rateLimiters, callingGenericZkSecretParams),
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(), config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()), zkAuthOperations, callingGenericZkSecretParams, clock),
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(),
config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()),
zkAuthOperations, callingGenericZkSecretParams, clock),
new ChallengeController(rateLimitChallengeManager, useRemoteAddress),
new DeviceController(config.getLinkDeviceSecretConfiguration().secret().value(), accountsManager, messagesManager, keysManager, rateLimiters,
new DeviceController(config.getLinkDeviceSecretConfiguration().secret().value(), accountsManager,
messagesManager, keysManager, rateLimiters,
rateLimitersCluster, config.getMaxDevices(), clock),
new DirectoryV2Controller(directoryV2CredentialsGenerator),
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
@ -831,13 +834,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new ProvisioningController(rateLimiters, provisioningManager),
new RegistrationController(accountsManager, phoneVerificationTokenManager, registrationLockVerificationManager,
rateLimiters),
new RemoteConfigController(remoteConfigsManager, adminEventLogger,
config.getRemoteConfigConfiguration().authorizedUsers(),
config.getRemoteConfigConfiguration().requiredHostedDomain(),
config.getRemoteConfigConfiguration().audiences(),
new GoogleIdTokenVerifier.Builder(new ApacheHttpTransport(), new GsonFactory()),
config.getRemoteConfigConfiguration().globalConfig(),
clock),
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig(), clock),
new SecureStorageController(storageCredentialsGenerator),
new SecureValueRecovery2Controller(svr2CredentialsGenerator, accountsManager),
new SecureValueRecovery3Controller(svr3CredentialsGenerator, accountsManager),

View File

@ -5,15 +5,9 @@
package org.whispersystems.textsecuregcm.configuration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public record RemoteConfigConfiguration(@NotNull Set<String> authorizedUsers,
@NotNull String requiredHostedDomain,
@NotNull @NotEmpty List<String> audiences,
@NotNull Map<String, String> globalConfig) {
public record RemoteConfigConfiguration(@NotNull Map<String, String> globalConfig) {
}

View File

@ -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<String> configAuthUsers;
private final Map<String, String> 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<String> configAuthUsers, String requiredHostedDomain, List<String> audience,
final GoogleIdTokenVerifier.Builder googleIdTokenVerifierBuilder, Map<String, String> globalConfig,
public RemoteConfigController(RemoteConfigsManager remoteConfigsManager,
Map<String, String> 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<String> getAuthIdentity(String token) {
return getAuthorizedGoogleIdentity(token)
.map(googleIdToken -> googleIdToken.getPayload().getEmail());
}
private Optional<GoogleIdToken> 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<UUID> uuidsInBucket) {

View File

@ -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<String> 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<String, GoogleIdToken> 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<RemoteConfig> 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<RemoteConfig> 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<RemoteConfig> 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<RemoteConfig> remoteConfigList = remoteConfigsManager.getAll();

View File

@ -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);
}
}