Add filter-provided captcha score thresholds
This commit is contained in:
parent
a8eb27940d
commit
ee53260d72
|
@ -166,6 +166,7 @@ import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
|||
import org.whispersystems.textsecuregcm.spam.FilterSpam;
|
||||
import org.whispersystems.textsecuregcm.spam.RateLimitChallengeListener;
|
||||
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
||||
import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider;
|
||||
import org.whispersystems.textsecuregcm.spam.SpamFilter;
|
||||
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountCleaner;
|
||||
|
@ -780,6 +781,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
webSocketEnvironment.jersey().register(controller);
|
||||
}
|
||||
|
||||
|
||||
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment = new WebSocketEnvironment<>(environment,
|
||||
webSocketEnvironment.getRequestLog(), 60000);
|
||||
provisioningEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||
|
@ -789,6 +791,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
|
||||
registerCorsFilter(environment);
|
||||
registerExceptionMappers(environment, webSocketEnvironment, provisioningEnvironment);
|
||||
registerProviders(environment, webSocketEnvironment, provisioningEnvironment);
|
||||
|
||||
environment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||
webSocketEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||
|
@ -830,6 +833,15 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
GarbageCollectionGauges.registerMetrics();
|
||||
}
|
||||
|
||||
|
||||
private void registerProviders(Environment environment,
|
||||
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment,
|
||||
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment) {
|
||||
environment.jersey().register(ScoreThresholdProvider.ScoreThresholdFeature.class);
|
||||
webSocketEnvironment.jersey().register(ScoreThresholdProvider.ScoreThresholdFeature.class);
|
||||
provisioningEnvironment.jersey().register(ScoreThresholdProvider.ScoreThresholdFeature.class);
|
||||
}
|
||||
|
||||
private void registerExceptionMappers(Environment environment,
|
||||
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment,
|
||||
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment) {
|
||||
|
|
|
@ -5,23 +5,111 @@
|
|||
|
||||
package org.whispersystems.textsecuregcm.captcha;
|
||||
|
||||
/**
|
||||
* A captcha assessment
|
||||
*
|
||||
* @param valid whether the captcha was passed
|
||||
* @param score string representation of the risk level
|
||||
*/
|
||||
public record AssessmentResult(boolean valid, String score) {
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public static AssessmentResult invalid() {
|
||||
return new AssessmentResult(false, "");
|
||||
public class AssessmentResult {
|
||||
|
||||
private final boolean solved;
|
||||
private final float actualScore;
|
||||
private final float defaultScoreThreshold;
|
||||
private final String scoreString;
|
||||
|
||||
/**
|
||||
* A captcha assessment
|
||||
*
|
||||
* @param solved if false, the captcha was not successfully completed
|
||||
* @param actualScore float representation of the risk level from [0, 1.0], with 1.0 being the least risky
|
||||
* @param defaultScoreThreshold the score threshold which the score will be evaluated against by default
|
||||
* @param scoreString a quantized string representation of the risk level, suitable for use in metrics
|
||||
*/
|
||||
private AssessmentResult(boolean solved, float actualScore, float defaultScoreThreshold, final String scoreString) {
|
||||
this.solved = solved;
|
||||
this.actualScore = actualScore;
|
||||
this.defaultScoreThreshold = defaultScoreThreshold;
|
||||
this.scoreString = scoreString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an {@link AssessmentResult} from a captcha evaluation score
|
||||
*
|
||||
* @param actualScore the score
|
||||
* @param defaultScoreThreshold the threshold to compare the score against by default
|
||||
*/
|
||||
public static AssessmentResult fromScore(float actualScore, float defaultScoreThreshold) {
|
||||
if (actualScore < 0 || actualScore > 1.0 || defaultScoreThreshold < 0 || defaultScoreThreshold > 1.0) {
|
||||
throw new IllegalArgumentException("invalid captcha score");
|
||||
}
|
||||
return new AssessmentResult(true, actualScore, defaultScoreThreshold, AssessmentResult.scoreString(actualScore));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a captcha assessment that will always be invalid
|
||||
*/
|
||||
public static AssessmentResult invalid() {
|
||||
return new AssessmentResult(false, 0.0f, 0.0f, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a captcha assessment that will always be valid
|
||||
*/
|
||||
public static AssessmentResult alwaysValid() {
|
||||
return new AssessmentResult(true, 1.0f, 0.0f, "1.0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the captcha assessment should be accepted using the default score threshold
|
||||
*
|
||||
* @return true if this assessment should be accepted under the default score threshold
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return isValid(Optional.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the captcha assessment should be accepted
|
||||
*
|
||||
* @param scoreThreshold the minimum score the assessment requires to pass, uses default if empty
|
||||
* @return true if the assessment scored higher than the provided scoreThreshold
|
||||
*/
|
||||
public boolean isValid(Optional<Float> scoreThreshold) {
|
||||
if (!solved) {
|
||||
return false;
|
||||
}
|
||||
return this.actualScore >= scoreThreshold.orElse(this.defaultScoreThreshold);
|
||||
}
|
||||
|
||||
public String getScoreString() {
|
||||
return scoreString;
|
||||
}
|
||||
|
||||
public float getScore() {
|
||||
return this.actualScore;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Map a captcha score in [0.0, 1.0] to a low cardinality discrete space in [0, 100] suitable for use in metrics
|
||||
*/
|
||||
static String scoreString(final float score) {
|
||||
private static String scoreString(final float score) {
|
||||
final int x = Math.round(score * 10); // [0, 10]
|
||||
return Integer.toString(x * 10); // [0, 100] in increments of 10
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
AssessmentResult that = (AssessmentResult) o;
|
||||
return solved == that.solved && Float.compare(that.actualScore, actualScore) == 0
|
||||
&& Float.compare(that.defaultScoreThreshold, defaultScoreThreshold) == 0 && Objects.equals(scoreString,
|
||||
that.scoreString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(solved, actualScore, defaultScoreThreshold, scoreString);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,8 +91,7 @@ public class CaptchaChecker {
|
|||
final AssessmentResult result = client.verify(siteKey, parsedAction, token, ip);
|
||||
Metrics.counter(ASSESSMENTS_COUNTER_NAME,
|
||||
"action", action,
|
||||
"valid", String.valueOf(result.valid()),
|
||||
"score", result.score(),
|
||||
"score", result.getScoreString(),
|
||||
"provider", prefix)
|
||||
.increment();
|
||||
return result;
|
||||
|
|
|
@ -33,7 +33,6 @@ public class HCaptchaClient implements CaptchaClient {
|
|||
private static final String PREFIX = "signal-hcaptcha";
|
||||
private static final String ASSESSMENT_REASON_COUNTER_NAME = name(HCaptchaClient.class, "assessmentReason");
|
||||
private static final String INVALID_REASON_COUNTER_NAME = name(HCaptchaClient.class, "invalidReason");
|
||||
private static final String INVALID_SITEKEY_COUNTER_NAME = name(HCaptchaClient.class, "invalidSiteKey");
|
||||
private final String apiKey;
|
||||
private final HttpClient client;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
|
@ -115,16 +114,15 @@ public class HCaptchaClient implements CaptchaClient {
|
|||
logger.error("Invalid score {} from hcaptcha response {}", hCaptchaResponse.score, hCaptchaResponse);
|
||||
return AssessmentResult.invalid();
|
||||
}
|
||||
final String scoreString = AssessmentResult.scoreString(score);
|
||||
final BigDecimal threshold = config.getScoreFloorByAction().getOrDefault(action, config.getScoreFloor());
|
||||
final AssessmentResult assessmentResult = AssessmentResult.fromScore(score, threshold.floatValue());
|
||||
|
||||
for (String reason : hCaptchaResponse.scoreReasons) {
|
||||
Metrics.counter(ASSESSMENT_REASON_COUNTER_NAME,
|
||||
"action", action.getActionName(),
|
||||
"reason", reason,
|
||||
"score", scoreString).increment();
|
||||
"score", assessmentResult.getScoreString()).increment();
|
||||
}
|
||||
|
||||
final BigDecimal threshold = config.getScoreFloorByAction().getOrDefault(action, config.getScoreFloor());
|
||||
return new AssessmentResult(score >= threshold.floatValue(), scoreString);
|
||||
return assessmentResult;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,17 +113,16 @@ public class RecaptchaClient implements CaptchaClient {
|
|||
if (assessment.getTokenProperties().getValid()) {
|
||||
final float score = assessment.getRiskAnalysis().getScore();
|
||||
log.debug("assessment for {} was valid, score: {}", action.getActionName(), score);
|
||||
final BigDecimal threshold = config.getScoreFloorByAction().getOrDefault(action, config.getScoreFloor());
|
||||
final AssessmentResult assessmentResult = AssessmentResult.fromScore(score, threshold.floatValue());
|
||||
for (RiskAnalysis.ClassificationReason reason : assessment.getRiskAnalysis().getReasonsList()) {
|
||||
Metrics.counter(ASSESSMENT_REASON_COUNTER_NAME,
|
||||
"action", action.getActionName(),
|
||||
"score", AssessmentResult.scoreString(score),
|
||||
"score", assessmentResult.getScoreString(),
|
||||
"reason", reason.name())
|
||||
.increment();
|
||||
}
|
||||
final BigDecimal threshold = config.getScoreFloorByAction().getOrDefault(action, config.getScoreFloor());
|
||||
return new AssessmentResult(
|
||||
score >= threshold.floatValue(),
|
||||
AssessmentResult.scoreString(score));
|
||||
return assessmentResult;
|
||||
} else {
|
||||
Metrics.counter(INVALID_REASON_COUNTER_NAME,
|
||||
"action", action.getActionName(),
|
||||
|
|
|
@ -93,7 +93,9 @@ import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
|||
import org.whispersystems.textsecuregcm.registration.ClientType;
|
||||
import org.whispersystems.textsecuregcm.registration.MessageTransport;
|
||||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
||||
import org.whispersystems.textsecuregcm.spam.Extract;
|
||||
import org.whispersystems.textsecuregcm.spam.FilterSpam;
|
||||
import org.whispersystems.textsecuregcm.spam.ScoreThreshold;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
|
||||
|
@ -255,14 +257,15 @@ public class AccountController {
|
|||
@Path("/{transport}/code/{number}")
|
||||
@FilterSpam
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response createAccount(@PathParam("transport") String transport,
|
||||
@PathParam("number") String number,
|
||||
@HeaderParam(HttpHeaders.X_FORWARDED_FOR) String forwardedFor,
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent,
|
||||
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) Optional<String> acceptLanguage,
|
||||
@QueryParam("client") Optional<String> client,
|
||||
@QueryParam("captcha") Optional<String> captcha,
|
||||
@QueryParam("challenge") Optional<String> pushChallenge)
|
||||
public Response createAccount(@PathParam("transport") String transport,
|
||||
@PathParam("number") String number,
|
||||
@HeaderParam(HttpHeaders.X_FORWARDED_FOR) String forwardedFor,
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent,
|
||||
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) Optional<String> acceptLanguage,
|
||||
@QueryParam("client") Optional<String> client,
|
||||
@QueryParam("captcha") Optional<String> captcha,
|
||||
@QueryParam("challenge") Optional<String> pushChallenge,
|
||||
@Extract ScoreThreshold captchaScoreThreshold)
|
||||
throws RateLimitExceededException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException, IOException {
|
||||
|
||||
Util.requireNormalizedNumber(number);
|
||||
|
@ -278,12 +281,12 @@ public class AccountController {
|
|||
|
||||
assessmentResult.ifPresent(result ->
|
||||
Metrics.counter(CAPTCHA_ATTEMPT_COUNTER_NAME, Tags.of(
|
||||
Tag.of("success", String.valueOf(result.valid())),
|
||||
Tag.of("success", String.valueOf(result.isValid(captchaScoreThreshold.getScoreThreshold()))),
|
||||
UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of(COUNTRY_CODE_TAG_NAME, countryCode),
|
||||
Tag.of(REGION_TAG_NAME, region),
|
||||
Tag.of(REGION_CODE_TAG_NAME, region),
|
||||
Tag.of(SCORE_TAG_NAME, result.score())))
|
||||
Tag.of(SCORE_TAG_NAME, result.getScoreString())))
|
||||
.increment());
|
||||
|
||||
final boolean pushChallengeMatch = pushChallengeMatches(number, pushChallenge, maybeStoredVerificationCode);
|
||||
|
@ -293,7 +296,7 @@ public class AccountController {
|
|||
}
|
||||
|
||||
final boolean requiresCaptcha = assessmentResult
|
||||
.map(result -> !result.valid())
|
||||
.map(result -> !result.isValid(captchaScoreThreshold.getScoreThreshold()))
|
||||
.orElseGet(
|
||||
() -> registrationCaptchaManager.requiresCaptcha(number, forwardedFor, sourceHost, pushChallengeMatch));
|
||||
|
||||
|
|
|
@ -75,8 +75,10 @@ import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
|||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceException;
|
||||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException;
|
||||
import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
||||
import org.whispersystems.textsecuregcm.spam.Extract;
|
||||
import org.whispersystems.textsecuregcm.spam.FilterSpam;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.spam.ScoreThreshold;
|
||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
|
||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||
|
@ -89,7 +91,6 @@ import org.whispersystems.textsecuregcm.util.Util;
|
|||
public class VerificationController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VerificationController.class);
|
||||
|
||||
private static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15);
|
||||
private static final Duration DYNAMODB_TIMEOUT = Duration.ofSeconds(5);
|
||||
|
||||
|
@ -195,7 +196,8 @@ public class VerificationController {
|
|||
public VerificationSessionResponse updateSession(@PathParam("sessionId") final String encodedSessionId,
|
||||
@HeaderParam(com.google.common.net.HttpHeaders.X_FORWARDED_FOR) String forwardedFor,
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||
@NotNull @Valid final UpdateVerificationSessionRequest updateVerificationSessionRequest) {
|
||||
@NotNull @Valid final UpdateVerificationSessionRequest updateVerificationSessionRequest,
|
||||
@NotNull @Extract final ScoreThreshold captchaScoreThreshold) {
|
||||
|
||||
final String sourceHost = HeaderUtils.getMostRecentProxy(forwardedFor).orElseThrow();
|
||||
|
||||
|
@ -213,7 +215,7 @@ public class VerificationController {
|
|||
verificationSession);
|
||||
|
||||
verificationSession = handleCaptcha(sourceHost, updateVerificationSessionRequest, registrationServiceSession,
|
||||
verificationSession, userAgent);
|
||||
verificationSession, userAgent, captchaScoreThreshold.getScoreThreshold());
|
||||
} catch (final RateLimitExceededException e) {
|
||||
|
||||
final Response response = buildResponseForRateLimitExceeded(verificationSession, registrationServiceSession,
|
||||
|
@ -351,11 +353,13 @@ public class VerificationController {
|
|||
* @throws ForbiddenException if assessment is not valid.
|
||||
* @throws RateLimitExceededException if too many captchas have been submitted
|
||||
*/
|
||||
private VerificationSession handleCaptcha(final String sourceHost,
|
||||
private VerificationSession handleCaptcha(
|
||||
final String sourceHost,
|
||||
final UpdateVerificationSessionRequest updateVerificationSessionRequest,
|
||||
final RegistrationServiceSession registrationServiceSession,
|
||||
VerificationSession verificationSession,
|
||||
final String userAgent) throws RateLimitExceededException {
|
||||
final String userAgent,
|
||||
final Optional<Float> captchaScoreThreshold) throws RateLimitExceededException {
|
||||
|
||||
if (updateVerificationSessionRequest.captcha() == null) {
|
||||
return verificationSession;
|
||||
|
@ -366,23 +370,24 @@ public class VerificationController {
|
|||
|
||||
final AssessmentResult assessmentResult;
|
||||
try {
|
||||
|
||||
assessmentResult = registrationCaptchaManager.assessCaptcha(
|
||||
Optional.of(updateVerificationSessionRequest.captcha()), sourceHost)
|
||||
.orElseThrow(() -> new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR));
|
||||
|
||||
Metrics.counter(CAPTCHA_ATTEMPT_COUNTER_NAME, Tags.of(
|
||||
Tag.of(SUCCESS_TAG_NAME, String.valueOf(assessmentResult.valid())),
|
||||
Tag.of(SUCCESS_TAG_NAME, String.valueOf(assessmentResult.isValid(captchaScoreThreshold))),
|
||||
UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(registrationServiceSession.number())),
|
||||
Tag.of(REGION_CODE_TAG_NAME, Util.getRegion(registrationServiceSession.number())),
|
||||
Tag.of(SCORE_TAG_NAME, assessmentResult.score())))
|
||||
Tag.of(SCORE_TAG_NAME, assessmentResult.getScoreString())))
|
||||
.increment();
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
if (assessmentResult.valid()) {
|
||||
if (assessmentResult.isValid(captchaScoreThreshold)) {
|
||||
final List<VerificationSession.Information> submittedInformation = new ArrayList<>(
|
||||
verificationSession.submittedInformation());
|
||||
submittedInformation.add(VerificationSession.Information.CAPTCHA);
|
||||
|
|
|
@ -68,7 +68,7 @@ public class RateLimitChallengeManager {
|
|||
|
||||
rateLimiters.getRecaptchaChallengeAttemptLimiter().validate(account.getUuid());
|
||||
|
||||
final boolean challengeSuccess = captchaChecker.verify(Action.CHALLENGE, captcha, mostRecentProxyIp).valid();
|
||||
final boolean challengeSuccess = captchaChecker.verify(Action.CHALLENGE, captcha, mostRecentProxyIp).isValid();
|
||||
|
||||
final Tags tags = Tags.of(
|
||||
Tag.of(SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.getNumber())),
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.spam;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Indicates that a parameter should be parsed from a {@link org.glassfish.jersey.server.ContainerRequest}
|
||||
*/
|
||||
@Target({ElementType.PARAMETER, ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Extract {
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.spam;
|
||||
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A ScoreThreshold may be provided by an upstream request filter. If request contains a property for
|
||||
* SCORE_THRESHOLD_PROPERTY_NAME it can be forwarded to a downstream filter to indicate it can use
|
||||
* a more or less strict score threshold when evaluating whether a request should be allowed to continue.
|
||||
*/
|
||||
public class ScoreThreshold {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScoreThreshold.class);
|
||||
|
||||
public static final String PROPERTY_NAME = "scoreThreshold";
|
||||
|
||||
/**
|
||||
* A score threshold in the range [0, 1.0]
|
||||
*/
|
||||
private final Optional<Float> scoreThreshold;
|
||||
|
||||
/**
|
||||
* Extract an optional score threshold parameter provided by an upstream request filter
|
||||
*/
|
||||
public ScoreThreshold(final ContainerRequest containerRequest) {
|
||||
this.scoreThreshold = Optional
|
||||
.ofNullable(containerRequest.getProperty(PROPERTY_NAME))
|
||||
.flatMap(obj -> {
|
||||
if (obj instanceof Float f) {
|
||||
return Optional.of(f);
|
||||
}
|
||||
logger.warn("invalid format for filter provided score threshold {}", obj);
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Float> getScoreThreshold() {
|
||||
return this.scoreThreshold;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package org.whispersystems.textsecuregcm.spam;
|
||||
|
||||
import java.util.function.Function;
|
||||
import javax.inject.Singleton;
|
||||
import javax.ws.rs.core.Feature;
|
||||
import javax.ws.rs.core.FeatureContext;
|
||||
import org.glassfish.jersey.internal.inject.AbstractBinder;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.glassfish.jersey.server.model.Parameter;
|
||||
import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
|
||||
|
||||
/**
|
||||
* Parses a {@link ScoreThreshold} out of a {@link ContainerRequest} to provide to jersey resources.
|
||||
*
|
||||
* A request filter may enrich a ContainerRequest with a scoreThreshold by providing a float property with the name
|
||||
* {@link ScoreThreshold#PROPERTY_NAME}. This indicates the desired scoreThreshold to use when evaluating whether a
|
||||
* request should proceed.
|
||||
*
|
||||
* A resource can consume a ScoreThreshold with by annotating a ScoreThreshold parameter with {@link Extract}
|
||||
*/
|
||||
public class ScoreThresholdProvider implements ValueParamProvider {
|
||||
|
||||
/**
|
||||
* Configures the ScoreThresholdProvider
|
||||
*/
|
||||
public static class ScoreThresholdFeature implements Feature {
|
||||
@Override
|
||||
public boolean configure(FeatureContext context) {
|
||||
context.register(new AbstractBinder() {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(ScoreThresholdProvider.class)
|
||||
.to(ValueParamProvider.class)
|
||||
.in(Singleton.class);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<ContainerRequest, ?> getValueProvider(final Parameter parameter) {
|
||||
if (parameter.getRawType().equals(ScoreThreshold.class)
|
||||
&& parameter.isAnnotationPresent(Extract.class)) {
|
||||
return ScoreThreshold::new;
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public PriorityType getPriority() {
|
||||
return Priority.HIGH;
|
||||
}
|
||||
}
|
|
@ -84,7 +84,7 @@ public class CaptchaCheckerTest {
|
|||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void scoreString(float score, String expected) {
|
||||
assertThat(AssessmentResult.scoreString(score)).isEqualTo(expected);
|
||||
assertThat(AssessmentResult.fromScore(score, 0.0f).getScoreString()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -58,8 +58,7 @@ public class HCaptchaClientTest {
|
|||
if (!success) {
|
||||
assertThat(result).isEqualTo(AssessmentResult.invalid());
|
||||
} else {
|
||||
assertThat(result)
|
||||
.isEqualTo(new AssessmentResult(expectedResult, AssessmentResult.scoreString(score)));
|
||||
assertThat(result.isValid()).isEqualTo(expectedResult);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -109,6 +109,8 @@ import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
|||
import org.whispersystems.textsecuregcm.registration.ClientType;
|
||||
import org.whispersystems.textsecuregcm.registration.MessageTransport;
|
||||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
||||
import org.whispersystems.textsecuregcm.spam.Extract;
|
||||
import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
|
||||
|
@ -215,6 +217,7 @@ class AccountControllerTest {
|
|||
.addProvider(new ImpossiblePhoneNumberExceptionMapper())
|
||||
.addProvider(new NonNormalizedPhoneNumberExceptionMapper())
|
||||
.addProvider(new RateLimitByIpFilter(rateLimiters))
|
||||
.addProvider(ScoreThresholdProvider.ScoreThresholdFeature.class)
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new AccountController(pendingAccountsManager,
|
||||
|
@ -349,7 +352,7 @@ class AccountControllerTest {
|
|||
when(captchaChecker.verify(eq(Action.REGISTRATION), eq(INVALID_CAPTCHA_TOKEN), anyString()))
|
||||
.thenReturn(AssessmentResult.invalid());
|
||||
when(captchaChecker.verify(eq(Action.REGISTRATION), eq(VALID_CAPTCHA_TOKEN), anyString()))
|
||||
.thenReturn(new AssessmentResult(true, ""));
|
||||
.thenReturn(AssessmentResult.alwaysValid());
|
||||
|
||||
doThrow(new RateLimitExceededException(Duration.ZERO, true)).when(pinLimiter).validate(eq(SENDER_OVER_PIN));
|
||||
|
||||
|
@ -690,6 +693,7 @@ class AccountControllerTest {
|
|||
final Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/sms/code/%s", number))
|
||||
.register(Extract.class)
|
||||
.queryParam("challenge", "1234-push")
|
||||
.request()
|
||||
.header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST)
|
||||
|
|
|
@ -69,6 +69,7 @@ import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderEx
|
|||
import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider;
|
||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
@ -100,6 +101,7 @@ class VerificationControllerTest {
|
|||
.addProvider(new ImpossiblePhoneNumberExceptionMapper())
|
||||
.addProvider(new NonNormalizedPhoneNumberExceptionMapper())
|
||||
.addProvider(new RegistrationServiceSenderExceptionMapper())
|
||||
.addProvider(ScoreThresholdProvider.ScoreThresholdFeature.class)
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(
|
||||
|
@ -621,7 +623,7 @@ class VerificationControllerTest {
|
|||
registrationServiceSession.expiration()))));
|
||||
|
||||
when(registrationCaptchaManager.assessCaptcha(any(), any()))
|
||||
.thenReturn(Optional.of(new AssessmentResult(true, "1")));
|
||||
.thenReturn(Optional.of(AssessmentResult.alwaysValid()));
|
||||
|
||||
when(verificationSessionManager.update(any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
@ -669,7 +671,7 @@ class VerificationControllerTest {
|
|||
registrationServiceSession.expiration()))));
|
||||
|
||||
when(registrationCaptchaManager.assessCaptcha(any(), any()))
|
||||
.thenReturn(Optional.of(new AssessmentResult(true, "1")));
|
||||
.thenReturn(Optional.of(AssessmentResult.alwaysValid()));
|
||||
|
||||
when(verificationSessionManager.update(any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
|
|
@ -78,7 +78,7 @@ class RateLimitChallengeManagerTest {
|
|||
|
||||
when(captchaChecker.verify(eq(Action.CHALLENGE), any(), any()))
|
||||
.thenReturn(successfulChallenge
|
||||
? new AssessmentResult(true, "")
|
||||
? AssessmentResult.alwaysValid()
|
||||
: AssessmentResult.invalid());
|
||||
|
||||
when(rateLimiters.getRecaptchaChallengeAttemptLimiter()).thenReturn(mock(RateLimiter.class));
|
||||
|
|
Loading…
Reference in New Issue