Support ID token at `PUT /v1/config` and `DELETE /v1/config`

This commit is contained in:
Chris Eager 2023-05-16 10:28:41 -05:00 committed by Chris Eager
parent f17de58a71
commit d1e38737ce
6 changed files with 229 additions and 92 deletions

View File

@ -24,7 +24,7 @@ sealed interface Event
@Serializable @Serializable
data class RemoteConfigSetEvent( data class RemoteConfigSetEvent(
val token: String, val identity: String,
val name: String, val name: String,
val percentage: Int, val percentage: Int,
val defaultValue: String? = null, val defaultValue: String? = null,
@ -35,6 +35,6 @@ data class RemoteConfigSetEvent(
@Serializable @Serializable
data class RemoteConfigDeleteEvent( data class RemoteConfigDeleteEvent(
val token: String, val identity: String,
val name: String, val name: String,
) : Event ) : Event

View File

@ -302,6 +302,17 @@ appConfig:
remoteConfig: remoteConfig:
authorizedTokens: secret://remoteConfig.authorizedTokens authorizedTokens: secret://remoteConfig.authorizedTokens
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 globalConfig: # keys and values that are given to clients on GET /v1/config
EXAMPLE_KEY: VALUE EXAMPLE_KEY: VALUE

View File

@ -11,6 +11,9 @@ import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentialsProviderChain; import com.amazonaws.auth.AWSCredentialsProviderChain;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
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.auth.oauth2.GoogleCredentials;
import com.google.cloud.logging.LoggingOptions; import com.google.cloud.logging.LoggingOptions;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
@ -753,6 +756,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
keys, rateLimiters), keys, rateLimiters),
new RemoteConfigController(remoteConfigsManager, adminEventLogger, new RemoteConfigController(remoteConfigsManager, adminEventLogger,
config.getRemoteConfigConfiguration().authorizedTokens().value(), config.getRemoteConfigConfiguration().authorizedTokens().value(),
config.getRemoteConfigConfiguration().authorizedUsers(),
config.getRemoteConfigConfiguration().requiredHostedDomain(),
config.getRemoteConfigConfiguration().audiences(),
new GoogleIdTokenVerifier.Builder(new ApacheHttpTransport(), new GsonFactory()),
config.getRemoteConfigConfiguration().globalConfig()), config.getRemoteConfigConfiguration().globalConfig()),
new SecureBackupController(backupCredentialsGenerator, accountsManager), new SecureBackupController(backupCredentialsGenerator, accountsManager),
new SecureStorageController(storageCredentialsGenerator), new SecureStorageController(storageCredentialsGenerator),

View File

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

View File

@ -6,6 +6,8 @@
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed; import com.codahale.metrics.annotation.Timed;
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 com.google.common.annotations.VisibleForTesting;
import io.dropwizard.auth.Auth; import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@ -16,10 +18,12 @@ import java.security.NoSuchAlgorithmException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
@ -51,15 +55,26 @@ public class RemoteConfigController {
private final RemoteConfigsManager remoteConfigsManager; private final RemoteConfigsManager remoteConfigsManager;
private final AdminEventLogger adminEventLogger; private final AdminEventLogger adminEventLogger;
private final List<String> configAuthTokens; private final List<String> configAuthTokens;
private final Set<String> configAuthUsers;
private final Map<String, String> globalConfig; private final Map<String, String> globalConfig;
private final String requiredHostedDomain;
private final GoogleIdTokenVerifier googleIdTokenVerifier;
private static final String GLOBAL_CONFIG_PREFIX = "global."; private static final String GLOBAL_CONFIG_PREFIX = "global.";
public RemoteConfigController(RemoteConfigsManager remoteConfigsManager, AdminEventLogger adminEventLogger, List<String> configAuthTokens, Map<String, String> globalConfig) { public RemoteConfigController(RemoteConfigsManager remoteConfigsManager, AdminEventLogger adminEventLogger,
List<String> configAuthTokens, Set<String> configAuthUsers, String requiredHostedDomain, List<String> audience,
final GoogleIdTokenVerifier.Builder googleIdTokenVerifierBuilder, Map<String, String> globalConfig) {
this.remoteConfigsManager = remoteConfigsManager; this.remoteConfigsManager = remoteConfigsManager;
this.adminEventLogger = Objects.requireNonNull(adminEventLogger); this.adminEventLogger = Objects.requireNonNull(adminEventLogger);
this.configAuthTokens = configAuthTokens; this.configAuthTokens = configAuthTokens;
this.configAuthUsers = configAuthUsers;
this.globalConfig = globalConfig; this.globalConfig = globalConfig;
this.requiredHostedDomain = requiredHostedDomain;
this.googleIdTokenVerifier = googleIdTokenVerifierBuilder.setAudience(audience).build();
} }
@Timed @Timed
@ -89,9 +104,9 @@ public class RemoteConfigController {
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public void set(@HeaderParam("Config-Token") String configToken, @NotNull @Valid RemoteConfig config) { public void set(@HeaderParam("Config-Token") String configToken, @NotNull @Valid RemoteConfig config) {
if (!isAuthorized(configToken)) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED); final String authIdentity = getAuthIdentity(configToken)
} .orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));
if (config.getName().startsWith(GLOBAL_CONFIG_PREFIX)) { if (config.getName().startsWith(GLOBAL_CONFIG_PREFIX)) {
throw new WebApplicationException(Response.Status.FORBIDDEN); throw new WebApplicationException(Response.Status.FORBIDDEN);
@ -99,7 +114,7 @@ public class RemoteConfigController {
adminEventLogger.logEvent( adminEventLogger.logEvent(
new RemoteConfigSetEvent( new RemoteConfigSetEvent(
configToken, authIdentity,
config.getName(), config.getName(),
config.getPercentage(), config.getPercentage(),
config.getDefaultValue(), config.getDefaultValue(),
@ -113,21 +128,48 @@ public class RemoteConfigController {
@DELETE @DELETE
@Path("/{name}") @Path("/{name}")
public void delete(@HeaderParam("Config-Token") String configToken, @PathParam("name") String name) { public void delete(@HeaderParam("Config-Token") String configToken, @PathParam("name") String name) {
if (!isAuthorized(configToken)) { final String authIdentity = getAuthIdentity(configToken)
throw new WebApplicationException(Response.Status.UNAUTHORIZED); .orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));
}
if (name.startsWith(GLOBAL_CONFIG_PREFIX)) { if (name.startsWith(GLOBAL_CONFIG_PREFIX)) {
throw new WebApplicationException(Response.Status.FORBIDDEN); throw new WebApplicationException(Response.Status.FORBIDDEN);
} }
adminEventLogger.logEvent(new RemoteConfigDeleteEvent(configToken, name)); adminEventLogger.logEvent(new RemoteConfigDeleteEvent(authIdentity, name));
remoteConfigsManager.delete(name); remoteConfigsManager.delete(name);
} }
private Optional<String> getAuthIdentity(String token) {
return getAuthorizedGoogleIdentity(token)
.map(googleIdToken -> googleIdToken.getPayload().getEmail())
.or(() -> Optional.ofNullable(isAuthorized(token) ? token : null));
}
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 @VisibleForTesting
public static boolean isInBucket(MessageDigest digest, UUID uid, byte[] hashKey, int configPercentage, Set<UUID> uuidsInBucket) { public static boolean isInBucket(MessageDigest digest, UUID uid, byte[] hashKey, int configPercentage,
if (uuidsInBucket.contains(uid)) return true; Set<UUID> uuidsInBucket) {
if (uuidsInBucket.contains(uid)) {
return true;
}
ByteBuffer bb = ByteBuffer.wrap(new byte[16]); ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(uid.getMostSignificantBits()); bb.putLong(uid.getMostSignificantBits());
@ -136,7 +178,7 @@ public class RemoteConfigController {
digest.update(bb.array()); digest.update(bb.array());
byte[] hash = digest.digest(hashKey); byte[] hash = digest.digest(hashKey);
int bucket = (int)(Util.ensureNonNegativeLong(Conversions.byteArrayToLong(hash)) % 100); int bucket = (int) (Util.ensureNonNegativeLong(Conversions.byteArrayToLong(hash)) % 100);
return bucket < configPercentage; return bucket < configPercentage;
} }

View File

@ -6,6 +6,8 @@
package org.whispersystems.textsecuregcm.tests.controllers; package org.whispersystems.textsecuregcm.tests.controllers;
import static org.assertj.core.api.Assertions.assertThat; 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.mock;
import static org.mockito.Mockito.reset; import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@ -13,12 +15,15 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; 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 com.google.common.collect.ImmutableSet;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension; import io.dropwizard.testing.junit5.ResourceExtension;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
@ -26,6 +31,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.stream.Stream;
import javax.ws.rs.client.Entity; import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -34,6 +40,9 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.signal.event.NoOpAdminEventLogger; import org.signal.event.NoOpAdminEventLogger;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
@ -52,20 +61,35 @@ class RemoteConfigControllerTest {
private static final RemoteConfigsManager remoteConfigsManager = mock(RemoteConfigsManager.class); private static final RemoteConfigsManager remoteConfigsManager = mock(RemoteConfigsManager.class);
private static final List<String> remoteConfigsAuth = List.of("foo", "bar"); private static final List<String> remoteConfigsAuth = List.of("foo", "bar");
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 ResourceExtension resources = ResourceExtension.builder() private static final ResourceExtension resources = ResourceExtension.builder()
.addProvider(AuthHelper.getAuthFilter()) .addProvider(AuthHelper.getAuthFilter())
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory()) .setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addProvider(new DeviceLimitExceededExceptionMapper()) .addProvider(new DeviceLimitExceededExceptionMapper())
.addResource(new RemoteConfigController(remoteConfigsManager, new NoOpAdminEventLogger(), remoteConfigsAuth, Map.of("maxGroupSize", "42"))) .addResource(new RemoteConfigController(remoteConfigsManager, new NoOpAdminEventLogger(), remoteConfigsAuth,
remoteConfigsUsers, requiredHostedDomain, Collections.singletonList("aud.example.com"),
googleIdVerificationTokenBuilder, Map.of("maxGroupSize", "42")))
.build(); .build();
@BeforeEach @BeforeEach
void setup() { void setup() throws Exception {
when(remoteConfigsManager.getAll()).thenReturn(new LinkedList<>() {{ when(remoteConfigsManager.getAll()).thenReturn(new LinkedList<>() {{
add(new RemoteConfig("android.stickers", 25, Set.of(AuthHelper.DISABLED_UUID, AuthHelper.INVALID_UUID), null, null, null)); add(new RemoteConfig("android.stickers", 25, Set.of(AuthHelper.DISABLED_UUID, AuthHelper.INVALID_UUID), null,
null, null));
add(new RemoteConfig("ios.stickers", 50, Set.of(), null, null, null)); add(new RemoteConfig("ios.stickers", 50, Set.of(), null, null, null));
add(new RemoteConfig("always.true", 100, Set.of(), null, null, null)); add(new RemoteConfig("always.true", 100, Set.of(), null, null, null));
add(new RemoteConfig("only.special", 0, Set.of(AuthHelper.VALID_UUID), null, null, null)); add(new RemoteConfig("only.special", 0, Set.of(AuthHelper.VALID_UUID), null, null, null));
@ -76,6 +100,24 @@ class RemoteConfigControllerTest {
add(new RemoteConfig("linked.config.1", 50, Set.of(), null, null, "linked.config.0")); add(new RemoteConfig("linked.config.1", 50, Set.of(), null, null, "linked.config.0"));
add(new RemoteConfig("unlinked.config", 50, Set.of(), null, null, null)); 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 @AfterEach
@ -173,7 +215,6 @@ class RemoteConfigControllerTest {
assertThat(allUnlinkedConfigsMatched).isFalse(); assertThat(allUnlinkedConfigsMatched).isFalse();
} }
@Test @Test
void testRetrieveConfigUnauthorized() { void testRetrieveConfigUnauthorized() {
Response response = resources.getJerseyTest() Response response = resources.getJerseyTest()
@ -187,14 +228,15 @@ class RemoteConfigControllerTest {
verifyNoMoreInteractions(remoteConfigsManager); verifyNoMoreInteractions(remoteConfigsManager);
} }
@ParameterizedTest
@Test @MethodSource("authorizedTokens")
void testSetConfig() { void testSetConfig(final String configToken) {
Response response = resources.getJerseyTest() Response response = resources.getJerseyTest()
.target("/v1/config") .target("/v1/config")
.request() .request()
.header("Config-Token", "foo") .header("Config-Token", configToken)
.put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null), MediaType.APPLICATION_JSON_TYPE)); .put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(204); assertThat(response.getStatus()).isEqualTo(204);
@ -207,13 +249,15 @@ class RemoteConfigControllerTest {
assertThat(captor.getValue().getUuids()).isEmpty(); assertThat(captor.getValue().getUuids()).isEmpty();
} }
@Test @ParameterizedTest
void testSetConfigValued() { @MethodSource("authorizedTokens")
void testSetConfigValued(final String configToken) {
Response response = resources.getJerseyTest() Response response = resources.getJerseyTest()
.target("/v1/config") .target("/v1/config")
.request() .request()
.header("Config-Token", "foo") .header("Config-Token", configToken)
.put(Entity.entity(new RemoteConfig("value.sometimes", 50, Set.of(), "a", "b", null), MediaType.APPLICATION_JSON_TYPE)); .put(Entity.entity(new RemoteConfig("value.sometimes", 50, Set.of(), "a", "b", null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(204); assertThat(response.getStatus()).isEqualTo(204);
@ -226,19 +270,21 @@ class RemoteConfigControllerTest {
assertThat(captor.getValue().getUuids()).isEmpty(); assertThat(captor.getValue().getUuids()).isEmpty();
} }
@Test @ParameterizedTest
void testSetConfigWithHashKey() { @MethodSource("authorizedTokens")
void testSetConfigWithHashKey(final String configToken) {
Response response1 = resources.getJerseyTest() Response response1 = resources.getJerseyTest()
.target("/v1/config") .target("/v1/config")
.request() .request()
.header("Config-Token", "foo") .header("Config-Token", configToken)
.put(Entity.entity(new RemoteConfig("linked.config.0", 50, Set.of(), "FALSE", "TRUE", null), MediaType.APPLICATION_JSON_TYPE)); .put(Entity.entity(new RemoteConfig("linked.config.0", 50, Set.of(), "FALSE", "TRUE", null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response1.getStatus()).isEqualTo(204); assertThat(response1.getStatus()).isEqualTo(204);
Response response2 = resources.getJerseyTest() Response response2 = resources.getJerseyTest()
.target("/v1/config") .target("/v1/config")
.request() .request()
.header("Config-Token", "foo") .header("Config-Token", configToken)
.put(Entity.entity(new RemoteConfig("linked.config.1", 50, Set.of(), "FALSE", "TRUE", "linked.config.0"), MediaType.APPLICATION_JSON_TYPE)); .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); assertThat(response2.getStatus()).isEqualTo(204);
@ -262,13 +308,15 @@ class RemoteConfigControllerTest {
assertThat(capture2.getHashKey()).isEqualTo("linked.config.0"); assertThat(capture2.getHashKey()).isEqualTo("linked.config.0");
} }
@Test @ParameterizedTest
void testSetConfigUnauthorized() { @MethodSource("unauthorizedTokens")
void testSetConfigUnauthorized(final String configToken) {
Response response = resources.getJerseyTest() Response response = resources.getJerseyTest()
.target("/v1/config") .target("/v1/config")
.request() .request()
.header("Config-Token", "baz") .header("Config-Token", configToken)
.put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null), MediaType.APPLICATION_JSON_TYPE)); .put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(401); assertThat(response.getStatus()).isEqualTo(401);
@ -280,32 +328,36 @@ class RemoteConfigControllerTest {
Response response = resources.getJerseyTest() Response response = resources.getJerseyTest()
.target("/v1/config") .target("/v1/config")
.request() .request()
.put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null), MediaType.APPLICATION_JSON_TYPE)); .put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(401); assertThat(response.getStatus()).isEqualTo(401);
verifyNoMoreInteractions(remoteConfigsManager); verifyNoMoreInteractions(remoteConfigsManager);
} }
@Test @ParameterizedTest
void testSetConfigBadName() { @MethodSource("authorizedTokens")
void testSetConfigBadName(final String configToken) {
Response response = resources.getJerseyTest() Response response = resources.getJerseyTest()
.target("/v1/config") .target("/v1/config")
.request() .request()
.header("Config-Token", "foo") .header("Config-Token", configToken)
.put(Entity.entity(new RemoteConfig("android-stickers", 88, Set.of(), "FALSE", "TRUE", null), MediaType.APPLICATION_JSON_TYPE)); .put(Entity.entity(new RemoteConfig("android-stickers", 88, Set.of(), "FALSE", "TRUE", null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(422); assertThat(response.getStatus()).isEqualTo(422);
verifyNoMoreInteractions(remoteConfigsManager); verifyNoMoreInteractions(remoteConfigsManager);
} }
@Test @ParameterizedTest
void testSetConfigEmptyName() { @MethodSource("authorizedTokens")
void testSetConfigEmptyName(final String configToken) {
Response response = resources.getJerseyTest() Response response = resources.getJerseyTest()
.target("/v1/config") .target("/v1/config")
.request() .request()
.header("Config-Token", "foo") .header("Config-Token", configToken)
.put(Entity.entity(new RemoteConfig("", 88, Set.of(), "FALSE", "TRUE", null), MediaType.APPLICATION_JSON_TYPE)); .put(Entity.entity(new RemoteConfig("", 88, Set.of(), "FALSE", "TRUE", null), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(422); assertThat(response.getStatus()).isEqualTo(422);
@ -313,23 +365,26 @@ class RemoteConfigControllerTest {
verifyNoMoreInteractions(remoteConfigsManager); verifyNoMoreInteractions(remoteConfigsManager);
} }
@Test @ParameterizedTest
void testSetGlobalConfig() { @MethodSource("authorizedTokens")
void testSetGlobalConfig(final String configToken) {
Response response = resources.getJerseyTest() Response response = resources.getJerseyTest()
.target("/v1/config") .target("/v1/config")
.request() .request()
.header("Config-Token", "foo") .header("Config-Token", configToken)
.put(Entity.entity(new RemoteConfig("global.maxGroupSize", 88, Set.of(), "FALSE", "TRUE", null), MediaType.APPLICATION_JSON_TYPE)); .put(Entity.entity(new RemoteConfig("global.maxGroupSize", 88, Set.of(), "FALSE", "TRUE", null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(403); assertThat(response.getStatus()).isEqualTo(403);
verifyNoMoreInteractions(remoteConfigsManager); verifyNoMoreInteractions(remoteConfigsManager);
} }
@Test @ParameterizedTest
void testDelete() { @MethodSource("authorizedTokens")
void testDelete(final String configToken) {
Response response = resources.getJerseyTest() Response response = resources.getJerseyTest()
.target("/v1/config/android.stickers") .target("/v1/config/android.stickers")
.request() .request()
.header("Config-Token", "foo") .header("Config-Token", configToken)
.delete(); .delete();
assertThat(response.getStatus()).isEqualTo(204); assertThat(response.getStatus()).isEqualTo(204);
@ -351,12 +406,13 @@ class RemoteConfigControllerTest {
verifyNoMoreInteractions(remoteConfigsManager); verifyNoMoreInteractions(remoteConfigsManager);
} }
@Test @ParameterizedTest
void testDeleteGlobalConfig() { @MethodSource("authorizedTokens")
void testDeleteGlobalConfig(final String configToken) {
Response response = resources.getJerseyTest() Response response = resources.getJerseyTest()
.target("/v1/config/global.maxGroupSize") .target("/v1/config/global.maxGroupSize")
.request() .request()
.header("Config-Token", "foo") .header("Config-Token", configToken)
.delete(); .delete();
assertThat(response.getStatus()).isEqualTo(403); assertThat(response.getStatus()).isEqualTo(403);
verifyNoMoreInteractions(remoteConfigsManager); verifyNoMoreInteractions(remoteConfigsManager);
@ -386,8 +442,22 @@ class RemoteConfigControllerTest {
double targetNumber = iterations * (config.getPercentage() / 100.0); double targetNumber = iterations * (config.getPercentage() / 100.0);
double variance = targetNumber * 0.01; double variance = targetNumber * 0.01;
assertThat(enabledMap.get(config.getName())).isBetween((int)(targetNumber - variance), (int)(targetNumber + variance)); assertThat(enabledMap.get(config.getName())).isBetween((int) (targetNumber - variance),
(int) (targetNumber + variance));
}
} }
static Stream<Arguments> authorizedTokens() {
return Stream.of(
Arguments.of("foo"),
Arguments.of("user1.valid")
);
}
static Stream<Arguments> unauthorizedTokens() {
return Stream.of(
Arguments.of("baz"),
Arguments.of("user3.valid")
);
} }
} }