Add dynamic configuration for signup captcha by country code

This commit is contained in:
Chris Eager 2021-03-29 14:46:05 -05:00 committed by Chris Eager
parent c8414a63fb
commit ad6c271f9d
6 changed files with 119 additions and 3 deletions

View File

@ -427,7 +427,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(new TimestampResponseFilter());
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, usernamesManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender, backupCredentialsGenerator));
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, usernamesManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender, backupCredentialsGenerator));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices()));
environment.jersey().register(new DirectoryController(directoryCredentialsGenerator));
environment.jersey().register(new ProvisioningController(rateLimiters, provisioningManager));

View File

@ -41,6 +41,9 @@ public class DynamicConfiguration {
@Valid
private DynamicTwilioConfiguration twilio = new DynamicTwilioConfiguration();
@JsonProperty
private DynamicSignupCaptchaConfiguration signupCaptcha = new DynamicSignupCaptchaConfiguration();
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
final String experimentName) {
return Optional.ofNullable(experiments.get(experimentName));
@ -80,4 +83,7 @@ public class DynamicConfiguration {
this.twilio = twilioConfiguration;
}
public DynamicSignupCaptchaConfiguration getSignupCaptchaConfiguration() {
return signupCaptcha;
}
}

View File

@ -0,0 +1,23 @@
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import java.util.Collections;
import java.util.Set;
import javax.validation.constraints.NotNull;
public class DynamicSignupCaptchaConfiguration {
@JsonProperty
@NotNull
private Set<String> countryCodes = Collections.emptySet();
public Set<String> getCountryCodes() {
return countryCodes;
}
@VisibleForTesting
public void setCountryCodes(Set<String> numbers) {
this.countryCodes = numbers;
}
}

View File

@ -16,7 +16,6 @@ import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@ -48,6 +47,7 @@ import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.auth.TurnToken;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicSignupCaptchaConfiguration;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.AccountCreationResult;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
@ -70,6 +70,7 @@ import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
@ -111,6 +112,7 @@ public class AccountController {
private final SmsSender smsSender;
private final DirectoryQueue directoryQueue;
private final MessagesManager messagesManager;
private final DynamicConfigurationManager dynamicConfigurationManager;
private final TurnTokenGenerator turnTokenGenerator;
private final Map<String, Integer> testDevices;
private final RecaptchaClient recaptchaClient;
@ -126,6 +128,7 @@ public class AccountController {
SmsSender smsSenderFactory,
DirectoryQueue directoryQueue,
MessagesManager messagesManager,
DynamicConfigurationManager dynamicConfigurationManager,
TurnTokenGenerator turnTokenGenerator,
Map<String, Integer> testDevices,
RecaptchaClient recaptchaClient,
@ -141,6 +144,7 @@ public class AccountController {
this.smsSender = smsSenderFactory;
this.directoryQueue = directoryQueue;
this.messagesManager = messagesManager;
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.testDevices = testDevices;
this.turnTokenGenerator = turnTokenGenerator;
this.recaptchaClient = recaptchaClient;
@ -590,9 +594,10 @@ public class AccountController {
}
}
final String countryCode = Util.getCountryCode(number);
{
final List<Tag> tags = new ArrayList<>();
tags.add(Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)));
tags.add(Tag.of(COUNTRY_CODE_TAG_NAME, countryCode));
try {
if (pushChallenge.isPresent()) {
@ -650,6 +655,11 @@ public class AccountController {
return new CaptchaRequirement(true, true);
}
DynamicSignupCaptchaConfiguration signupCaptchaConfig = dynamicConfigurationManager.getConfiguration().getSignupCaptchaConfiguration();
if (signupCaptchaConfig.getCountryCodes().contains(countryCode)) {
return new CaptchaRequirement(true, false);
}
return new CaptchaRequirement(false, false);
}

View File

@ -276,4 +276,28 @@ class DynamicConfigurationTest {
assertEquals(Set.of("44"), config.getAllowedCountryCodes());
}
}
@Test
public void testParseSignupCaptchaConfiguration() throws JsonProcessingException {
{
final String emptyConfigYaml = "test: true";
final DynamicConfiguration emptyConfig = DynamicConfigurationManager.OBJECT_MAPPER
.readValue(emptyConfigYaml, DynamicConfiguration.class);
assertTrue(emptyConfig.getSignupCaptchaConfiguration().getCountryCodes().isEmpty());
}
{
final String signupCaptchaConfig =
"signupCaptcha:\n"
+ " countryCodes:\n"
+ " - 1";
final DynamicSignupCaptchaConfiguration config = DynamicConfigurationManager.OBJECT_MAPPER
.readValue(signupCaptchaConfig, DynamicConfiguration.class)
.getSignupCaptchaConfiguration();
assertEquals(Set.of("1"), config.getCountryCodes());
}
}
}

View File

@ -32,8 +32,10 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@ -43,7 +45,9 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
@ -52,6 +56,8 @@ import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicSignupCaptchaConfiguration;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
@ -75,6 +81,7 @@ import org.whispersystems.textsecuregcm.storage.AbusiveHostRule;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
@ -130,6 +137,8 @@ class AccountControllerTest {
private static APNSender apnSender = mock(APNSender.class);
private static UsernamesManager usernamesManager = mock(UsernamesManager.class);
private static DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
private byte[] registration_lock_key = new byte[32];
private static ExternalServiceCredentialGenerator storageCredentialGenerator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], false);
@ -148,6 +157,7 @@ class AccountControllerTest {
smsSender,
directoryQueue,
storedMessages,
dynamicConfigurationManager,
turnTokenGenerator,
new HashMap<>(),
recaptchaClient,
@ -207,6 +217,15 @@ class AccountControllerTest {
when(usernamesManager.put(eq(AuthHelper.VALID_UUID), eq("n00bkiller"))).thenReturn(true);
when(usernamesManager.put(eq(AuthHelper.VALID_UUID), eq("takenusername"))).thenReturn(false);
{
DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
when(dynamicConfigurationManager.getConfiguration())
.thenReturn(dynamicConfiguration);
DynamicSignupCaptchaConfiguration signupCaptchaConfig = new DynamicSignupCaptchaConfiguration();
when(dynamicConfiguration.getSignupCaptchaConfiguration()).thenReturn(signupCaptchaConfig);
}
when(abusiveHostRules.getAbusiveHostRulesFor(eq(ABUSIVE_HOST))).thenReturn(Collections.singletonList(new AbusiveHostRule(ABUSIVE_HOST, true, Collections.emptyList())));
when(abusiveHostRules.getAbusiveHostRulesFor(eq(RESTRICTED_HOST))).thenReturn(Collections.singletonList(new AbusiveHostRule(RESTRICTED_HOST, false, Collections.singletonList("+123"))));
when(abusiveHostRules.getAbusiveHostRulesFor(eq(NICE_HOST))).thenReturn(Collections.emptyList());
@ -1275,4 +1294,38 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(204);
verify(accountsManager).delete(AuthHelper.VALID_ACCOUNT, AccountsManager.DeletionReason.USER_REQUEST);
}
@ParameterizedTest
@MethodSource
void testSignupCaptcha(final String message, final boolean enforced, final Set<String> countryCodes, final int expectedResponseStatusCode) {
DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
when(dynamicConfigurationManager.getConfiguration())
.thenReturn(dynamicConfiguration);
DynamicSignupCaptchaConfiguration signupCaptchaConfig = new DynamicSignupCaptchaConfiguration();
signupCaptchaConfig.setCountryCodes(countryCodes);
when(dynamicConfiguration.getSignupCaptchaConfiguration())
.thenReturn(signupCaptchaConfig);
Response response =
resources.getJerseyTest()
.target(String.format("/v1/accounts/sms/code/%s", SENDER))
.queryParam("challenge", "1234-push")
.request()
.header("X-Forwarded-For", NICE_HOST)
.get();
assertThat(response.getStatus()).isEqualTo(expectedResponseStatusCode);
verify(smsSender, 200 == expectedResponseStatusCode ? times(1) : never())
.deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString());
}
static Stream<Arguments> testSignupCaptcha() {
return Stream.of(
Arguments.of("captcha not enforced", false, Collections.emptySet(), 200),
Arguments.of("no enforced country codes", true, Collections.emptySet(), 200),
Arguments.of("captcha enforced", true, Set.of("1"), 402)
);
}
}