mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-22 22:48:02 +01:00
Accept a captcha score threshold for challenges from the spam filter
This commit is contained in:
committed by
GitHub
parent
9c053e20da
commit
fd19299ae0
@@ -22,18 +22,29 @@ import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.container.ContainerRequestFilter;
|
||||
import javax.ws.rs.core.Feature;
|
||||
import javax.ws.rs.core.FeatureContext;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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.ValueSource;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.spam.PushChallengeConfigProvider;
|
||||
import org.whispersystems.textsecuregcm.spam.ScoreThreshold;
|
||||
import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
@@ -44,10 +55,24 @@ class ChallengeControllerTest {
|
||||
|
||||
private static final ChallengeController challengeController = new ChallengeController(rateLimitChallengeManager);
|
||||
|
||||
private static final AtomicReference<Float> scoreThreshold = new AtomicReference<>();
|
||||
|
||||
private static final ResourceExtension EXTENSION = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
Set.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
||||
.addProvider(ScoreThresholdProvider.ScoreThresholdFeature.class)
|
||||
.addProvider(PushChallengeConfigProvider.PushChallengeConfigFeature.class)
|
||||
.addProvider(new Feature() {
|
||||
public boolean configure(FeatureContext featureContext) {
|
||||
featureContext.register(new ContainerRequestFilter() {
|
||||
public void filter(ContainerRequestContext requestContext) {
|
||||
requestContext.setProperty(ScoreThreshold.PROPERTY_NAME, scoreThreshold.get());
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new RateLimitExceededExceptionMapper())
|
||||
@@ -57,6 +82,7 @@ class ChallengeControllerTest {
|
||||
@AfterEach
|
||||
void teardown() {
|
||||
reset(rateLimitChallengeManager);
|
||||
scoreThreshold.set(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -99,8 +125,9 @@ class ChallengeControllerTest {
|
||||
assertEquals(String.valueOf(retryAfter.toSeconds()), response.getHeaderString("Retry-After"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleRecaptcha() throws RateLimitExceededException, IOException {
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = { true, false } )
|
||||
void testHandleRecaptcha(boolean hasThreshold) throws RateLimitExceededException, IOException {
|
||||
final String recaptchaChallengeJson = """
|
||||
{
|
||||
"type": "recaptcha",
|
||||
@@ -109,9 +136,13 @@ class ChallengeControllerTest {
|
||||
}
|
||||
""";
|
||||
|
||||
when(rateLimitChallengeManager.answerRecaptchaChallenge(any(), any(), any(), any()))
|
||||
when(rateLimitChallengeManager.answerRecaptchaChallenge(any(), any(), any(), any(), any()))
|
||||
.thenReturn(true);
|
||||
|
||||
|
||||
if (hasThreshold) {
|
||||
scoreThreshold.set(Float.valueOf(0.5f));
|
||||
}
|
||||
final Response response = EXTENSION.target("/v1/challenge")
|
||||
.request()
|
||||
.header(HttpHeaders.X_FORWARDED_FOR, "10.0.0.1")
|
||||
@@ -120,7 +151,7 @@ class ChallengeControllerTest {
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
verify(rateLimitChallengeManager).answerRecaptchaChallenge(eq(AuthHelper.VALID_ACCOUNT), eq("The value of the solved captcha token"), eq("10.0.0.1"), anyString());
|
||||
verify(rateLimitChallengeManager).answerRecaptchaChallenge(eq(AuthHelper.VALID_ACCOUNT), eq("The value of the solved captcha token"), eq("10.0.0.1"), anyString(), eq(hasThreshold ? Optional.of(0.5f) : Optional.empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -132,8 +163,7 @@ class ChallengeControllerTest {
|
||||
"captcha": "The value of the solved captcha token"
|
||||
}
|
||||
""";
|
||||
|
||||
when(rateLimitChallengeManager.answerRecaptchaChallenge(eq(AuthHelper.VALID_ACCOUNT), eq("The value of the solved captcha token"), eq("10.0.0.1"), anyString()))
|
||||
when(rateLimitChallengeManager.answerRecaptchaChallenge(eq(AuthHelper.VALID_ACCOUNT), eq("The value of the solved captcha token"), eq("10.0.0.1"), anyString(), any()))
|
||||
.thenReturn(false);
|
||||
|
||||
final Response response = EXTENSION.target("/v1/challenge")
|
||||
@@ -157,7 +187,7 @@ class ChallengeControllerTest {
|
||||
|
||||
final Duration retryAfter = Duration.ofMinutes(17);
|
||||
doThrow(new RateLimitExceededException(retryAfter, true)).when(rateLimitChallengeManager)
|
||||
.answerRecaptchaChallenge(any(), any(), any(), any());
|
||||
.answerRecaptchaChallenge(any(), any(), any(), any(), any());
|
||||
|
||||
final Response response = EXTENSION.target("/v1/challenge")
|
||||
.request()
|
||||
|
||||
@@ -13,9 +13,14 @@ import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.whispersystems.textsecuregcm.captcha.Action;
|
||||
import org.whispersystems.textsecuregcm.captcha.AssessmentResult;
|
||||
@@ -27,6 +32,8 @@ import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
||||
class RateLimitChallengeManagerTest {
|
||||
|
||||
private static final float DEFAULT_SCORE_THRESHOLD = 0.1f;
|
||||
|
||||
private PushChallengeManager pushChallengeManager;
|
||||
private CaptchaChecker captchaChecker;
|
||||
private RateLimiters rateLimiters;
|
||||
@@ -71,27 +78,39 @@ class RateLimitChallengeManagerTest {
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
void answerRecaptchaChallenge(final boolean successfulChallenge) throws RateLimitExceededException, IOException {
|
||||
@MethodSource
|
||||
void answerRecaptchaChallenge(Optional<Float> scoreThreshold, float actualScore, boolean expectSuccess)
|
||||
throws RateLimitExceededException, IOException {
|
||||
final Account account = mock(Account.class);
|
||||
when(account.getNumber()).thenReturn("+18005551234");
|
||||
when(account.getUuid()).thenReturn(UUID.randomUUID());
|
||||
|
||||
when(captchaChecker.verify(eq(Action.CHALLENGE), any(), any()))
|
||||
.thenReturn(successfulChallenge
|
||||
? AssessmentResult.alwaysValid()
|
||||
: AssessmentResult.invalid());
|
||||
.thenReturn(AssessmentResult.fromScore(actualScore, DEFAULT_SCORE_THRESHOLD));
|
||||
|
||||
when(rateLimiters.getRecaptchaChallengeAttemptLimiter()).thenReturn(mock(RateLimiter.class));
|
||||
when(rateLimiters.getRecaptchaChallengeSuccessLimiter()).thenReturn(mock(RateLimiter.class));
|
||||
when(rateLimiters.getRateLimitResetLimiter()).thenReturn(mock(RateLimiter.class));
|
||||
|
||||
rateLimitChallengeManager.answerRecaptchaChallenge(account, "captcha", "10.0.0.1", "Test User-Agent");
|
||||
rateLimitChallengeManager.answerRecaptchaChallenge(account, "captcha", "10.0.0.1", "Test User-Agent", scoreThreshold);
|
||||
|
||||
if (successfulChallenge) {
|
||||
if (expectSuccess) {
|
||||
verify(rateLimitChallengeListener).handleRateLimitChallengeAnswered(account, ChallengeType.CAPTCHA);
|
||||
} else {
|
||||
verifyNoInteractions(rateLimitChallengeListener);
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> answerRecaptchaChallenge() {
|
||||
return Stream.of(
|
||||
Arguments.of(Optional.empty(), 0.5f, true),
|
||||
Arguments.of(Optional.empty(), 0.1f, true),
|
||||
Arguments.of(Optional.empty(), 0.0f, false),
|
||||
Arguments.of(Optional.of(0.1f), 0.5f, true),
|
||||
Arguments.of(Optional.of(0.1f), 0.1f, true),
|
||||
Arguments.of(Optional.of(0.1f), 0.0f, false),
|
||||
Arguments.of(Optional.of(0.3f), 0.5f, true),
|
||||
Arguments.of(Optional.of(0.3f), 0.1f, false),
|
||||
Arguments.of(Optional.of(0.3f), 0.0f, false));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user