Move score floor to dynamic configuration, add distribution summary

This commit is contained in:
Chris Eager 2022-03-02 15:18:33 -08:00 committed by GitHub
parent 9fc5002619
commit eee6307789
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 167 additions and 117 deletions

View File

@ -246,7 +246,6 @@ recaptcha:
secret: unset secret: unset
recaptchaV2: recaptchaV2:
scoreFloor: 1.0
projectPath: projects/example projectPath: projects/example
credentialConfigurationJson: "{ }" # service account configuration for backend authentication credentialConfigurationJson: "{ }" # service account configuration for backend authentication

View File

@ -476,10 +476,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration()); TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
LegacyRecaptchaClient legacyRecaptchaClient = new LegacyRecaptchaClient(config.getRecaptchaConfiguration().getSecret()); LegacyRecaptchaClient legacyRecaptchaClient = new LegacyRecaptchaClient(config.getRecaptchaConfiguration().getSecret());
EnterpriseRecaptchaClient enterpriseRecaptchaClient = new EnterpriseRecaptchaClient( EnterpriseRecaptchaClient enterpriseRecaptchaClient = new EnterpriseRecaptchaClient(
config.getRecaptchaV2Configuration().getScoreFloor().doubleValue(),
config.getRecaptchaV2Configuration().getProjectPath(), config.getRecaptchaV2Configuration().getProjectPath(),
config.getRecaptchaV2Configuration().getCredentialConfigurationJson()); config.getRecaptchaV2Configuration().getCredentialConfigurationJson(),
TransitionalRecaptchaClient transitionalRecaptchaClient = new TransitionalRecaptchaClient(legacyRecaptchaClient, enterpriseRecaptchaClient); dynamicConfigurationManager);
TransitionalRecaptchaClient transitionalRecaptchaClient = new TransitionalRecaptchaClient(legacyRecaptchaClient,
enterpriseRecaptchaClient);
PushChallengeManager pushChallengeManager = new PushChallengeManager(apnSender, gcmSender, pushChallengeDynamoDb); PushChallengeManager pushChallengeManager = new PushChallengeManager(apnSender, gcmSender, pushChallengeDynamoDb);
RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager, RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager,
transitionalRecaptchaClient, dynamicRateLimiters); transitionalRecaptchaClient, dynamicRateLimiters);

View File

@ -5,25 +5,13 @@
package org.whispersystems.textsecuregcm.configuration; package org.whispersystems.textsecuregcm.configuration;
import java.math.BigDecimal;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class RecaptchaV2Configuration { public class RecaptchaV2Configuration {
private BigDecimal scoreFloor;
private String projectPath; private String projectPath;
private String credentialConfigurationJson; private String credentialConfigurationJson;
@DecimalMin("0")
@DecimalMax("1")
@NotNull
public BigDecimal getScoreFloor() {
return scoreFloor;
}
@NotEmpty @NotEmpty
public String getProjectPath() { public String getProjectPath() {
return projectPath; return projectPath;

View File

@ -0,0 +1,36 @@
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.Set;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
public class DynamicCaptchaConfiguration {
@JsonProperty
@DecimalMin("0")
@DecimalMax("1")
@NotNull
private BigDecimal scoreFloor;
@JsonProperty
@NotNull
private Set<String> signupCountryCodes = Collections.emptySet();
public Set<String> getSignupCountryCodes() {
return signupCountryCodes;
}
@VisibleForTesting
public void setSignupCountryCodes(Set<String> numbers) {
this.signupCountryCodes = numbers;
}
public BigDecimal getScoreFloor() {
return scoreFloor;
}
}

View File

@ -38,7 +38,8 @@ public class DynamicConfiguration {
private DynamicTwilioConfiguration twilio = new DynamicTwilioConfiguration(); private DynamicTwilioConfiguration twilio = new DynamicTwilioConfiguration();
@JsonProperty @JsonProperty
private DynamicSignupCaptchaConfiguration signupCaptcha = new DynamicSignupCaptchaConfiguration(); @Valid
private DynamicCaptchaConfiguration captcha = new DynamicCaptchaConfiguration();
@JsonProperty @JsonProperty
@Valid @Valid
@ -86,8 +87,8 @@ public class DynamicConfiguration {
this.twilio = twilioConfiguration; this.twilio = twilioConfiguration;
} }
public DynamicSignupCaptchaConfiguration getSignupCaptchaConfiguration() { public DynamicCaptchaConfiguration getCaptchaConfiguration() {
return signupCaptcha; return captcha;
} }
public DynamicRateLimitChallengeConfiguration getRateLimitChallengeConfiguration() { public DynamicRateLimitChallengeConfiguration getRateLimitChallengeConfiguration() {

View File

@ -1,23 +0,0 @@
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

@ -61,8 +61,8 @@ import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.auth.TurnToken; import org.whispersystems.textsecuregcm.auth.TurnToken;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicSignupCaptchaConfiguration;
import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse; import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
@ -770,8 +770,9 @@ public class AccountController {
return new CaptchaRequirement(true, true); return new CaptchaRequirement(true, true);
} }
DynamicSignupCaptchaConfiguration signupCaptchaConfig = dynamicConfigurationManager.getConfiguration().getSignupCaptchaConfiguration(); DynamicCaptchaConfiguration captchaConfig = dynamicConfigurationManager.getConfiguration()
if (signupCaptchaConfig.getCountryCodes().contains(countryCode)) { .getCaptchaConfiguration();
if (captchaConfig.getSignupCountryCodes().contains(countryCode)) {
return new CaptchaRequirement(true, false); return new CaptchaRequirement(true, false);
} }

View File

@ -5,6 +5,8 @@
package org.whispersystems.textsecuregcm.recaptcha; package org.whispersystems.textsecuregcm.recaptcha;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import com.google.api.gax.core.FixedCredentialsProvider; import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceClient; import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceClient;
@ -12,36 +14,38 @@ import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceSetting
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.recaptchaenterprise.v1.Assessment; import com.google.recaptchaenterprise.v1.Assessment;
import com.google.recaptchaenterprise.v1.Event; import com.google.recaptchaenterprise.v1.Event;
import io.micrometer.core.instrument.Metrics;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Objects; import java.util.Objects;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.ws.rs.BadRequestException; import javax.ws.rs.BadRequestException;
import org.slf4j.Logger; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
public class EnterpriseRecaptchaClient implements RecaptchaClient { public class EnterpriseRecaptchaClient implements RecaptchaClient {
@VisibleForTesting @VisibleForTesting
static final String SEPARATOR = "."; static final String SEPARATOR = ".";
private static final Logger logger = LoggerFactory.getLogger(EnterpriseRecaptchaClient.class); private static final String SCORE_DISTRIBUTION_NAME = name(EnterpriseRecaptchaClient.class, "scoreDistribution");
private final double scoreFloor;
private final String projectPath; private final String projectPath;
private final RecaptchaEnterpriseServiceClient client; private final RecaptchaEnterpriseServiceClient client;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
public EnterpriseRecaptchaClient( public EnterpriseRecaptchaClient(
final double scoreFloor,
@Nonnull final String projectPath, @Nonnull final String projectPath,
@Nonnull final String recaptchaCredentialConfigurationJson) { @Nonnull final String recaptchaCredentialConfigurationJson,
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
try { try {
this.scoreFloor = scoreFloor;
this.projectPath = Objects.requireNonNull(projectPath); this.projectPath = Objects.requireNonNull(projectPath);
this.client = RecaptchaEnterpriseServiceClient.create(RecaptchaEnterpriseServiceSettings.newBuilder() this.client = RecaptchaEnterpriseServiceClient.create(RecaptchaEnterpriseServiceSettings.newBuilder()
.setCredentialsProvider(FixedCredentialsProvider.create(GoogleCredentials.fromStream( .setCredentialsProvider(FixedCredentialsProvider.create(GoogleCredentials.fromStream(
new ByteArrayInputStream(recaptchaCredentialConfigurationJson.getBytes(StandardCharsets.UTF_8))))) new ByteArrayInputStream(recaptchaCredentialConfigurationJson.getBytes(StandardCharsets.UTF_8)))))
.build()); .build());
this.dynamicConfigurationManager = dynamicConfigurationManager;
} catch (IOException e) { } catch (IOException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@ -89,6 +93,15 @@ public class EnterpriseRecaptchaClient implements RecaptchaClient {
final Event event = eventBuilder.build(); final Event event = eventBuilder.build();
final Assessment assessment = client.createAssessment(projectPath, Assessment.newBuilder().setEvent(event).build()); final Assessment assessment = client.createAssessment(projectPath, Assessment.newBuilder().setEvent(event).build());
return assessment.getTokenProperties().getValid() && assessment.getRiskAnalysis().getScore() >= scoreFloor; if (assessment.getTokenProperties().getValid()) {
final float score = assessment.getRiskAnalysis().getScore();
Metrics.summary(SCORE_DISTRIBUTION_NAME).record(score);
return score >= dynamicConfigurationManager.getConfiguration().getCaptchaConfiguration().getScoreFloor()
.floatValue();
} else {
return false;
}
} }
} }

View File

@ -26,10 +26,15 @@ import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
class DynamicConfigurationTest { class DynamicConfigurationTest {
private static final String REQUIRED_CONFIG = """
captcha:
scoreFloor: 1.0
""";
@Test @Test
void testParseExperimentConfig() throws JsonProcessingException { void testParseExperimentConfig() throws JsonProcessingException {
{ {
final String emptyConfigYaml = "test: true"; final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true");
final DynamicConfiguration emptyConfig = final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
@ -37,7 +42,7 @@ class DynamicConfigurationTest {
} }
{ {
final String experimentConfigYaml = """ final String experimentConfigYaml = REQUIRED_CONFIG.concat("""
experiments: experiments:
percentageOnly: percentageOnly:
enrollmentPercentage: 12 enrollmentPercentage: 12
@ -49,7 +54,7 @@ class DynamicConfigurationTest {
uuidsOnly: uuidsOnly:
enrolledUuids: enrolledUuids:
- 71618739-114c-4b1f-bb0d-6478a44eb600 - 71618739-114c-4b1f-bb0d-6478a44eb600
"""; """);
final DynamicConfiguration config = final DynamicConfiguration config =
DynamicConfigurationManager.parseConfiguration(experimentConfigYaml, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(experimentConfigYaml, DynamicConfiguration.class).orElseThrow();
@ -78,7 +83,7 @@ class DynamicConfigurationTest {
@Test @Test
void testParsePreRegistrationExperiments() throws JsonProcessingException { void testParsePreRegistrationExperiments() throws JsonProcessingException {
{ {
final String emptyConfigYaml = "test: true"; final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true");
final DynamicConfiguration emptyConfig = final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
@ -86,7 +91,7 @@ class DynamicConfigurationTest {
} }
{ {
final String experimentConfigYaml = """ final String experimentConfigYaml = REQUIRED_CONFIG.concat("""
preRegistrationExperiments: preRegistrationExperiments:
percentageOnly: percentageOnly:
enrollmentPercentage: 17 enrollmentPercentage: 17
@ -107,7 +112,7 @@ class DynamicConfigurationTest {
- +120255551212 - +120255551212
excludedCountryCodes: excludedCountryCodes:
- 47 - 47
"""; """);
final DynamicConfiguration config = final DynamicConfiguration config =
DynamicConfigurationManager.parseConfiguration(experimentConfigYaml, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(experimentConfigYaml, DynamicConfiguration.class).orElseThrow();
@ -160,7 +165,7 @@ class DynamicConfigurationTest {
@Test @Test
void testParseRemoteDeprecationConfig() throws JsonProcessingException { void testParseRemoteDeprecationConfig() throws JsonProcessingException {
{ {
final String emptyConfigYaml = "test: true"; final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true");
final DynamicConfiguration emptyConfig = final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
@ -168,7 +173,7 @@ class DynamicConfigurationTest {
} }
{ {
final String remoteDeprecationConfig = """ final String remoteDeprecationConfig = REQUIRED_CONFIG.concat("""
remoteDeprecation: remoteDeprecation:
minimumVersions: minimumVersions:
IOS: 1.2.3 IOS: 1.2.3
@ -178,7 +183,7 @@ class DynamicConfigurationTest {
blockedVersions: blockedVersions:
DESKTOP: DESKTOP:
- 1.4.0-beta.2 - 1.4.0-beta.2
"""; """);
final DynamicConfiguration config = final DynamicConfiguration config =
DynamicConfigurationManager.parseConfiguration(remoteDeprecationConfig, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(remoteDeprecationConfig, DynamicConfiguration.class).orElseThrow();
@ -199,7 +204,7 @@ class DynamicConfigurationTest {
@Test @Test
void testParseFeatureFlags() throws JsonProcessingException { void testParseFeatureFlags() throws JsonProcessingException {
{ {
final String emptyConfigYaml = "test: true"; final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true");
final DynamicConfiguration emptyConfig = final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
@ -207,10 +212,10 @@ class DynamicConfigurationTest {
} }
{ {
final String featureFlagYaml = """ final String featureFlagYaml = REQUIRED_CONFIG.concat("""
featureFlags: featureFlags:
- testFlag - testFlag
"""; """);
final DynamicConfiguration emptyConfig = final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(featureFlagYaml, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(featureFlagYaml, DynamicConfiguration.class).orElseThrow();
@ -222,7 +227,7 @@ class DynamicConfigurationTest {
@Test @Test
void testParseTwilioConfiguration() throws JsonProcessingException { void testParseTwilioConfiguration() throws JsonProcessingException {
{ {
final String emptyConfigYaml = "test: true"; final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true");
final DynamicConfiguration emptyConfig = final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
@ -230,12 +235,12 @@ class DynamicConfigurationTest {
} }
{ {
final String twilioConfigYaml = """ final String twilioConfigYaml = REQUIRED_CONFIG.concat("""
twilio: twilio:
numbers: numbers:
- 2135551212 - 2135551212
- 2135551313 - 2135551313
"""; """);
final DynamicTwilioConfiguration config = final DynamicTwilioConfiguration config =
DynamicConfigurationManager.parseConfiguration(twilioConfigYaml, DynamicConfiguration.class).orElseThrow() DynamicConfigurationManager.parseConfiguration(twilioConfigYaml, DynamicConfiguration.class).orElseThrow()
@ -248,7 +253,7 @@ class DynamicConfigurationTest {
@Test @Test
void testParsePaymentsConfiguration() throws JsonProcessingException { void testParsePaymentsConfiguration() throws JsonProcessingException {
{ {
final String emptyConfigYaml = "test: true"; final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true");
final DynamicConfiguration emptyConfig = final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
@ -256,11 +261,11 @@ class DynamicConfigurationTest {
} }
{ {
final String paymentsConfigYaml = """ final String paymentsConfigYaml = REQUIRED_CONFIG.concat("""
payments: payments:
disallowedPrefixes: disallowedPrefixes:
- +44 - +44
"""; """);
final DynamicPaymentsConfiguration config = final DynamicPaymentsConfiguration config =
DynamicConfigurationManager.parseConfiguration(paymentsConfigYaml, DynamicConfiguration.class).orElseThrow() DynamicConfigurationManager.parseConfiguration(paymentsConfigYaml, DynamicConfiguration.class).orElseThrow()
@ -271,34 +276,47 @@ class DynamicConfigurationTest {
} }
@Test @Test
void testParseSignupCaptchaConfiguration() throws JsonProcessingException { void testParseCaptchaConfiguration() throws JsonProcessingException {
{ {
final String emptyConfigYaml = "test: true"; final String emptyConfigYaml = "test: true";
final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
assertTrue(emptyConfig.getSignupCaptchaConfiguration().getCountryCodes().isEmpty()); assertTrue(DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).isEmpty(),
"empty config should not validate");
} }
{ {
final String signupCaptchaConfig = """ final String captchaConfig = """
signupCaptcha: captcha:
countryCodes: signupCountryCodes:
- 1 - 1
scoreFloor: null
"""; """;
final DynamicSignupCaptchaConfiguration config = assertTrue(DynamicConfigurationManager.parseConfiguration(captchaConfig, DynamicConfiguration.class).isEmpty(),
DynamicConfigurationManager.parseConfiguration(signupCaptchaConfig, DynamicConfiguration.class).orElseThrow() "score floor must not be null");
.getSignupCaptchaConfiguration(); }
assertEquals(Set.of("1"), config.getCountryCodes()); {
final String captchaConfig = """
captcha:
signupCountryCodes:
- 1
scoreFloor: 0.9
""";
final DynamicCaptchaConfiguration config =
DynamicConfigurationManager.parseConfiguration(captchaConfig, DynamicConfiguration.class).orElseThrow()
.getCaptchaConfiguration();
assertEquals(Set.of("1"), config.getSignupCountryCodes());
assertEquals(0.9f, config.getScoreFloor().floatValue());
} }
} }
@Test @Test
void testParseLimits() throws JsonProcessingException { void testParseLimits() throws JsonProcessingException {
{ {
final String emptyConfigYaml = "test: true"; final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true");
final DynamicConfiguration emptyConfig = final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
@ -307,12 +325,12 @@ class DynamicConfigurationTest {
} }
{ {
final String limitsConfig = """ final String limitsConfig = REQUIRED_CONFIG.concat("""
limits: limits:
rateLimitReset: rateLimitReset:
bucketSize: 17 bucketSize: 17
leakRatePerMinute: 44 leakRatePerMinute: 44
"""; """);
final RateLimitConfiguration resetRateLimitConfiguration = final RateLimitConfiguration resetRateLimitConfiguration =
DynamicConfigurationManager.parseConfiguration(limitsConfig, DynamicConfiguration.class).orElseThrow() DynamicConfigurationManager.parseConfiguration(limitsConfig, DynamicConfiguration.class).orElseThrow()
@ -326,7 +344,7 @@ class DynamicConfigurationTest {
@Test @Test
void testParseRateLimitReset() throws JsonProcessingException { void testParseRateLimitReset() throws JsonProcessingException {
{ {
final String emptyConfigYaml = "test: true"; final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true");
final DynamicConfiguration emptyConfig = final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
@ -334,13 +352,13 @@ class DynamicConfigurationTest {
} }
{ {
final String rateLimitChallengeConfig = """ final String rateLimitChallengeConfig = REQUIRED_CONFIG.concat("""
rateLimitChallenge: rateLimitChallenge:
clientSupportedVersions: clientSupportedVersions:
IOS: 5.1.0 IOS: 5.1.0
ANDROID: 5.2.0 ANDROID: 5.2.0
DESKTOP: 5.0.0 DESKTOP: 5.0.0
"""; """);
DynamicRateLimitChallengeConfiguration rateLimitChallengeConfiguration = DynamicRateLimitChallengeConfiguration rateLimitChallengeConfiguration =
DynamicConfigurationManager.parseConfiguration(rateLimitChallengeConfig, DynamicConfiguration.class).orElseThrow() DynamicConfigurationManager.parseConfiguration(rateLimitChallengeConfig, DynamicConfiguration.class).orElseThrow()
@ -357,7 +375,7 @@ class DynamicConfigurationTest {
@Test @Test
void testParseDirectoryReconciler() throws JsonProcessingException { void testParseDirectoryReconciler() throws JsonProcessingException {
{ {
final String emptyConfigYaml = "test: true"; final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true");
final DynamicConfiguration emptyConfig = final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
@ -365,10 +383,10 @@ class DynamicConfigurationTest {
} }
{ {
final String directoryReconcilerConfig = """ final String directoryReconcilerConfig = REQUIRED_CONFIG.concat("""
directoryReconciler: directoryReconciler:
enabled: false enabled: false
"""; """);
DynamicDirectoryReconcilerConfiguration directoryReconcilerConfiguration = DynamicDirectoryReconcilerConfiguration directoryReconcilerConfiguration =
DynamicConfigurationManager.parseConfiguration(directoryReconcilerConfig, DynamicConfiguration.class).orElseThrow() DynamicConfigurationManager.parseConfiguration(directoryReconcilerConfig, DynamicConfiguration.class).orElseThrow()

View File

@ -1,12 +1,13 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.time.Duration;
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.Timeout;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient;
@ -14,10 +15,15 @@ import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfiguratio
import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse; import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse;
import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest; import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest;
import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionResponse; import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionResponse;
import java.util.concurrent.TimeUnit;
class DynamicConfigurationManagerTest { class DynamicConfigurationManagerTest {
private static final SdkBytes VALID_CONFIG = SdkBytes.fromUtf8String("""
test: true
captcha:
scoreFloor: 1.0
""");
private DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager; private DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private AppConfigDataClient appConfig; private AppConfigDataClient appConfig;
private StartConfigurationSessionRequest startConfigurationSession; private StartConfigurationSessionRequest startConfigurationSession;
@ -35,7 +41,7 @@ class DynamicConfigurationManagerTest {
} }
@Test @Test
void testGetInitalConfig() { void testGetInitialConfig() {
when(appConfig.startConfigurationSession(startConfigurationSession)) when(appConfig.startConfigurationSession(startConfigurationSession))
.thenReturn(StartConfigurationSessionResponse.builder() .thenReturn(StartConfigurationSessionResponse.builder()
.initialConfigurationToken("initial") .initialConfigurationToken("initial")
@ -45,7 +51,7 @@ class DynamicConfigurationManagerTest {
when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder() when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder()
.configurationToken("initial").build())) .configurationToken("initial").build()))
.thenReturn(GetLatestConfigurationResponse.builder() .thenReturn(GetLatestConfigurationResponse.builder()
.configuration(SdkBytes.fromUtf8String("test: true")) .configuration(VALID_CONFIG)
.nextPollConfigurationToken("next").build()); .nextPollConfigurationToken("next").build());
// subsequent config calls will return empty (no update) // subsequent config calls will return empty (no update)
@ -55,8 +61,10 @@ class DynamicConfigurationManagerTest {
.configuration(SdkBytes.fromUtf8String("")) .configuration(SdkBytes.fromUtf8String(""))
.nextPollConfigurationToken("next").build()); .nextPollConfigurationToken("next").build());
assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
dynamicConfigurationManager.start(); dynamicConfigurationManager.start();
assertThat(dynamicConfigurationManager.getConfiguration()).isNotNull(); assertThat(dynamicConfigurationManager.getConfiguration()).isNotNull();
});
} }
@Test @Test
@ -77,7 +85,7 @@ class DynamicConfigurationManagerTest {
when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder(). when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder().
configurationToken("goodconfig").build())) configurationToken("goodconfig").build()))
.thenReturn(GetLatestConfigurationResponse.builder() .thenReturn(GetLatestConfigurationResponse.builder()
.configuration(SdkBytes.fromUtf8String("test: true")) .configuration(VALID_CONFIG)
.nextPollConfigurationToken("next").build()); .nextPollConfigurationToken("next").build());
// all subsequent config calls will return an empty config (no update) // all subsequent config calls will return an empty config (no update)
@ -86,13 +94,15 @@ class DynamicConfigurationManagerTest {
.thenReturn(GetLatestConfigurationResponse.builder() .thenReturn(GetLatestConfigurationResponse.builder()
.configuration(SdkBytes.fromUtf8String("")) .configuration(SdkBytes.fromUtf8String(""))
.nextPollConfigurationToken("next").build()); .nextPollConfigurationToken("next").build());
assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
dynamicConfigurationManager.start(); dynamicConfigurationManager.start();
assertThat(dynamicConfigurationManager.getConfiguration()).isNotNull(); assertThat(dynamicConfigurationManager.getConfiguration()).isNotNull();
});
} }
@Test @Test
@Timeout(value=5, unit= TimeUnit.SECONDS) void testGetConfigMultiple() {
void testGetConfigMultiple() throws InterruptedException {
when(appConfig.startConfigurationSession(startConfigurationSession)) when(appConfig.startConfigurationSession(startConfigurationSession))
.thenReturn(StartConfigurationSessionResponse.builder() .thenReturn(StartConfigurationSessionResponse.builder()
.initialConfigurationToken("0") .initialConfigurationToken("0")
@ -102,7 +112,7 @@ class DynamicConfigurationManagerTest {
when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder(). when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder().
configurationToken("0").build())) configurationToken("0").build()))
.thenReturn(GetLatestConfigurationResponse.builder() .thenReturn(GetLatestConfigurationResponse.builder()
.configuration(SdkBytes.fromUtf8String("test: true")) .configuration(VALID_CONFIG)
.nextPollConfigurationToken("1").build()); .nextPollConfigurationToken("1").build());
// config update with a real config // config update with a real config
@ -112,6 +122,8 @@ class DynamicConfigurationManagerTest {
.configuration(SdkBytes.fromUtf8String(""" .configuration(SdkBytes.fromUtf8String("""
featureFlags: featureFlags:
- testFlag - testFlag
captcha:
scoreFloor: 1.0
""")) """))
.nextPollConfigurationToken("2").build()); .nextPollConfigurationToken("2").build());
@ -122,11 +134,16 @@ class DynamicConfigurationManagerTest {
.configuration(SdkBytes.fromUtf8String("")) .configuration(SdkBytes.fromUtf8String(""))
.nextPollConfigurationToken("2").build()); .nextPollConfigurationToken("2").build());
// the internal waiting done by dynamic configuration manager catches the InterruptedException used
// by JUnits @Timeout, so we use assertTimeoutPreemptively
assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
// we should eventually get the updated config (or the test will timeout) // we should eventually get the updated config (or the test will timeout)
dynamicConfigurationManager.start(); dynamicConfigurationManager.start();
while (dynamicConfigurationManager.getConfiguration().getActiveFeatureFlags().isEmpty()) { while (dynamicConfigurationManager.getConfiguration().getActiveFeatureFlags().isEmpty()) {
Thread.sleep(100); Thread.sleep(100);
} }
assertThat(dynamicConfigurationManager.getConfiguration().getActiveFeatureFlags()).containsExactly("testFlag"); assertThat(dynamicConfigurationManager.getConfiguration().getActiveFeatureFlags()).containsExactly("testFlag");
});
} }
} }

View File

@ -52,7 +52,6 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
@ -61,8 +60,8 @@ import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock; import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; 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.AccountController;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.AccountAttributes;
@ -263,9 +262,9 @@ class AccountControllerTest {
when(dynamicConfigurationManager.getConfiguration()) when(dynamicConfigurationManager.getConfiguration())
.thenReturn(dynamicConfiguration); .thenReturn(dynamicConfiguration);
DynamicSignupCaptchaConfiguration signupCaptchaConfig = new DynamicSignupCaptchaConfiguration(); DynamicCaptchaConfiguration signupCaptchaConfig = new DynamicCaptchaConfiguration();
when(dynamicConfiguration.getSignupCaptchaConfiguration()).thenReturn(signupCaptchaConfig); when(dynamicConfiguration.getCaptchaConfiguration()).thenReturn(signupCaptchaConfig);
} }
when(abusiveHostRules.getAbusiveHostRulesFor(eq(ABUSIVE_HOST))).thenReturn(Collections.singletonList(new AbusiveHostRule(ABUSIVE_HOST, true, Collections.emptyList()))); 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(RESTRICTED_HOST))).thenReturn(Collections.singletonList(new AbusiveHostRule(RESTRICTED_HOST, false, Collections.singletonList("+123"))));
@ -1741,9 +1740,9 @@ class AccountControllerTest {
when(dynamicConfigurationManager.getConfiguration()) when(dynamicConfigurationManager.getConfiguration())
.thenReturn(dynamicConfiguration); .thenReturn(dynamicConfiguration);
DynamicSignupCaptchaConfiguration signupCaptchaConfig = new DynamicSignupCaptchaConfiguration(); DynamicCaptchaConfiguration signupCaptchaConfig = new DynamicCaptchaConfiguration();
signupCaptchaConfig.setCountryCodes(countryCodes); signupCaptchaConfig.setSignupCountryCodes(countryCodes);
when(dynamicConfiguration.getSignupCaptchaConfiguration()) when(dynamicConfiguration.getCaptchaConfiguration())
.thenReturn(signupCaptchaConfig); .thenReturn(signupCaptchaConfig);
Response response = Response response =