Explicitly call spam-filter for verification session updates
Pass in the same information to the spam-filter, but just use explicit method calls rather than jersey request filters.
This commit is contained in:
parent
4f40c128bf
commit
69330f47fd
|
@ -182,10 +182,8 @@ import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||||
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||||
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
||||||
import org.whispersystems.textsecuregcm.spam.ChallengeConstraintChecker;
|
import org.whispersystems.textsecuregcm.spam.ChallengeConstraintChecker;
|
||||||
import org.whispersystems.textsecuregcm.spam.FilterSpam;
|
import org.whispersystems.textsecuregcm.spam.RegistrationFraudChecker;
|
||||||
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
||||||
import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider;
|
|
||||||
import org.whispersystems.textsecuregcm.spam.SenderOverrideProvider;
|
|
||||||
import org.whispersystems.textsecuregcm.spam.SpamChecker;
|
import org.whispersystems.textsecuregcm.spam.SpamChecker;
|
||||||
import org.whispersystems.textsecuregcm.spam.SpamFilter;
|
import org.whispersystems.textsecuregcm.spam.SpamFilter;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountLockManager;
|
import org.whispersystems.textsecuregcm.storage.AccountLockManager;
|
||||||
|
@ -862,7 +860,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
final List<SpamFilter> spamFilters = ServiceLoader.load(SpamFilter.class)
|
final List<SpamFilter> spamFilters = ServiceLoader.load(SpamFilter.class)
|
||||||
.stream()
|
.stream()
|
||||||
.map(ServiceLoader.Provider::get)
|
.map(ServiceLoader.Provider::get)
|
||||||
.filter(s -> s.getClass().isAnnotationPresent(FilterSpam.class))
|
|
||||||
.flatMap(filter -> {
|
.flatMap(filter -> {
|
||||||
try {
|
try {
|
||||||
filter.configure(config.getSpamFilterConfiguration().getEnvironment());
|
filter.configure(config.getSpamFilterConfiguration().getEnvironment());
|
||||||
|
@ -898,6 +895,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
log.warn("No challenge-constraint-checkers found; using default (no-op) provider as a default");
|
log.warn("No challenge-constraint-checkers found; using default (no-op) provider as a default");
|
||||||
return ChallengeConstraintChecker.noop();
|
return ChallengeConstraintChecker.noop();
|
||||||
});
|
});
|
||||||
|
final RegistrationFraudChecker registrationFraudChecker = spamFilter
|
||||||
|
.map(SpamFilter::getRegistrationFraudChecker)
|
||||||
|
.orElseGet(() -> {
|
||||||
|
log.warn("No registration-fraud-checkers found; using default (no-op) provider as a default");
|
||||||
|
return RegistrationFraudChecker.noop();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
spamFilter.map(SpamFilter::getReportedMessageListener).ifPresent(reportMessageManager::addListener);
|
spamFilter.map(SpamFilter::getReportedMessageListener).ifPresent(reportMessageManager::addListener);
|
||||||
|
|
||||||
final RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager,
|
final RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager,
|
||||||
|
@ -956,7 +961,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
config.getCdnConfiguration().bucket()),
|
config.getCdnConfiguration().bucket()),
|
||||||
new VerificationController(registrationServiceClient, new VerificationSessionManager(verificationSessions),
|
new VerificationController(registrationServiceClient, new VerificationSessionManager(verificationSessions),
|
||||||
pushNotificationManager, registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters,
|
pushNotificationManager, registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters,
|
||||||
accountsManager, dynamicConfigurationManager, clock)
|
accountsManager, registrationFraudChecker, dynamicConfigurationManager, clock)
|
||||||
);
|
);
|
||||||
if (config.getSubscription() != null && config.getOneTimeDonations() != null) {
|
if (config.getSubscription() != null && config.getOneTimeDonations() != null) {
|
||||||
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getOneTimeDonations(),
|
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getOneTimeDonations(),
|
||||||
|
@ -978,7 +983,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
|
|
||||||
registerCorsFilter(environment);
|
registerCorsFilter(environment);
|
||||||
registerExceptionMappers(environment, webSocketEnvironment, provisioningEnvironment);
|
registerExceptionMappers(environment, webSocketEnvironment, provisioningEnvironment);
|
||||||
registerProviders(environment, webSocketEnvironment, provisioningEnvironment);
|
|
||||||
|
|
||||||
environment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
environment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||||
webSocketEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
webSocketEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||||
|
@ -1004,25 +1008,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
|
|
||||||
environment.admin().addTask(new SetRequestLoggingEnabledTask());
|
environment.admin().addTask(new SetRequestLoggingEnabledTask());
|
||||||
|
|
||||||
|
// healthcheck, admin port
|
||||||
environment.healthChecks().register("cacheCluster", new RedisClusterHealthCheck(cacheCluster));
|
environment.healthChecks().register("cacheCluster", new RedisClusterHealthCheck(cacheCluster));
|
||||||
|
|
||||||
MetricsUtil.registerSystemResourceMetrics(environment);
|
MetricsUtil.registerSystemResourceMetrics(environment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void registerProviders(Environment environment,
|
|
||||||
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment,
|
|
||||||
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment) {
|
|
||||||
List.of(
|
|
||||||
ScoreThresholdProvider.ScoreThresholdFeature.class,
|
|
||||||
SenderOverrideProvider.SenderOverrideFeature.class)
|
|
||||||
.forEach(feature -> {
|
|
||||||
environment.jersey().register(feature);
|
|
||||||
webSocketEnvironment.jersey().register(feature);
|
|
||||||
provisioningEnvironment.jersey().register(feature);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerExceptionMappers(Environment environment,
|
private void registerExceptionMappers(Environment environment,
|
||||||
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment,
|
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment,
|
||||||
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment) {
|
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment) {
|
||||||
|
|
|
@ -113,7 +113,6 @@ import org.whispersystems.textsecuregcm.push.MessageSender;
|
||||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.spam.FilterSpam;
|
|
||||||
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
||||||
import org.whispersystems.textsecuregcm.spam.SpamChecker;
|
import org.whispersystems.textsecuregcm.spam.SpamChecker;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
|
|
@ -80,10 +80,8 @@ import org.whispersystems.textsecuregcm.registration.RegistrationServiceExceptio
|
||||||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException;
|
import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException;
|
||||||
import org.whispersystems.textsecuregcm.registration.TransportNotAllowedException;
|
import org.whispersystems.textsecuregcm.registration.TransportNotAllowedException;
|
||||||
import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
||||||
import org.whispersystems.textsecuregcm.spam.Extract;
|
import org.whispersystems.textsecuregcm.spam.RegistrationFraudChecker;
|
||||||
import org.whispersystems.textsecuregcm.spam.FilterSpam;
|
import org.whispersystems.textsecuregcm.spam.RegistrationFraudChecker.VerificationCheck;
|
||||||
import org.whispersystems.textsecuregcm.spam.ScoreThreshold;
|
|
||||||
import org.whispersystems.textsecuregcm.spam.SenderOverride;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||||
|
@ -121,7 +119,7 @@ public class VerificationController {
|
||||||
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
|
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
|
||||||
private final RateLimiters rateLimiters;
|
private final RateLimiters rateLimiters;
|
||||||
private final AccountsManager accountsManager;
|
private final AccountsManager accountsManager;
|
||||||
|
private final RegistrationFraudChecker registrationFraudChecker;
|
||||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
|
||||||
|
@ -132,6 +130,7 @@ public class VerificationController {
|
||||||
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
|
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
|
||||||
final RateLimiters rateLimiters,
|
final RateLimiters rateLimiters,
|
||||||
final AccountsManager accountsManager,
|
final AccountsManager accountsManager,
|
||||||
|
final RegistrationFraudChecker registrationFraudChecker,
|
||||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||||
final Clock clock) {
|
final Clock clock) {
|
||||||
this.registrationServiceClient = registrationServiceClient;
|
this.registrationServiceClient = registrationServiceClient;
|
||||||
|
@ -141,6 +140,7 @@ public class VerificationController {
|
||||||
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
|
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
|
||||||
this.rateLimiters = rateLimiters;
|
this.rateLimiters = rateLimiters;
|
||||||
this.accountsManager = accountsManager;
|
this.accountsManager = accountsManager;
|
||||||
|
this.registrationFraudChecker = registrationFraudChecker;
|
||||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
@ -195,17 +195,16 @@ public class VerificationController {
|
||||||
return buildResponse(registrationServiceSession, verificationSession);
|
return buildResponse(registrationServiceSession, verificationSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
@FilterSpam
|
|
||||||
@PATCH
|
@PATCH
|
||||||
@Path("/session/{sessionId}")
|
@Path("/session/{sessionId}")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public VerificationSessionResponse updateSession(@PathParam("sessionId") final String encodedSessionId,
|
public VerificationSessionResponse updateSession(
|
||||||
|
@PathParam("sessionId") final String encodedSessionId,
|
||||||
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||||
@Context ContainerRequestContext requestContext,
|
@Context ContainerRequestContext requestContext,
|
||||||
@NotNull @Valid final UpdateVerificationSessionRequest updateVerificationSessionRequest,
|
@NotNull @Valid final UpdateVerificationSessionRequest updateVerificationSessionRequest,
|
||||||
@NotNull @Extract final ScoreThreshold scoreThreshold,
|
@Context ContainerRequestContext context) {
|
||||||
@NotNull @Extract final SenderOverride senderOverride) {
|
|
||||||
|
|
||||||
final String sourceHost = (String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
final String sourceHost = (String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||||
|
|
||||||
|
@ -215,10 +214,16 @@ public class VerificationController {
|
||||||
final RegistrationServiceSession registrationServiceSession = retrieveRegistrationServiceSession(encodedSessionId);
|
final RegistrationServiceSession registrationServiceSession = retrieveRegistrationServiceSession(encodedSessionId);
|
||||||
VerificationSession verificationSession = retrieveVerificationSession(registrationServiceSession);
|
VerificationSession verificationSession = retrieveVerificationSession(registrationServiceSession);
|
||||||
|
|
||||||
|
final VerificationCheck verificationCheck = registrationFraudChecker.checkVerificationAttempt(
|
||||||
|
context,
|
||||||
|
verificationSession,
|
||||||
|
registrationServiceSession.number(),
|
||||||
|
updateVerificationSessionRequest);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// these handle* methods ordered from least likely to fail to most, so take care when considering a change
|
// these handle* methods ordered from least likely to fail to most, so take care when considering a change
|
||||||
|
|
||||||
verificationSession = handleSenderOverrides(verificationSession, senderOverride);
|
verificationSession = verificationCheck.updatedSession().orElse(verificationSession);
|
||||||
|
|
||||||
verificationSession = handlePushToken(pushTokenAndType, verificationSession);
|
verificationSession = handlePushToken(pushTokenAndType, verificationSession);
|
||||||
|
|
||||||
|
@ -226,7 +231,7 @@ public class VerificationController {
|
||||||
verificationSession);
|
verificationSession);
|
||||||
|
|
||||||
verificationSession = handleCaptcha(sourceHost, updateVerificationSessionRequest, registrationServiceSession,
|
verificationSession = handleCaptcha(sourceHost, updateVerificationSessionRequest, registrationServiceSession,
|
||||||
verificationSession, userAgent, scoreThreshold.getScoreThreshold());
|
verificationSession, userAgent, verificationCheck.scoreThreshold());
|
||||||
} catch (final RateLimitExceededException e) {
|
} catch (final RateLimitExceededException e) {
|
||||||
|
|
||||||
final Response response = buildResponseForRateLimitExceeded(verificationSession, registrationServiceSession,
|
final Response response = buildResponseForRateLimitExceeded(verificationSession, registrationServiceSession,
|
||||||
|
@ -424,31 +429,6 @@ public class VerificationController {
|
||||||
return verificationSession;
|
return verificationSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the verification session with explicit sender overrides if present. When the session is used to send
|
|
||||||
* verification codes, these overrides will be used.
|
|
||||||
*
|
|
||||||
* @param verificationSession the session to update
|
|
||||||
* @param senderOverride configured sender overrides
|
|
||||||
* @return An updated {@link VerificationSession}
|
|
||||||
*/
|
|
||||||
private VerificationSession handleSenderOverrides(
|
|
||||||
VerificationSession verificationSession,
|
|
||||||
SenderOverride senderOverride) {
|
|
||||||
return new VerificationSession(
|
|
||||||
verificationSession.pushChallenge(),
|
|
||||||
verificationSession.requestedInformation(),
|
|
||||||
verificationSession.submittedInformation(),
|
|
||||||
Optional.ofNullable(verificationSession.smsSenderOverride())
|
|
||||||
.or(senderOverride::getSmsSenderOverride).orElse(null),
|
|
||||||
Optional.ofNullable(verificationSession.voiceSenderOverride())
|
|
||||||
.or(senderOverride::getVoiceSenderOverride)
|
|
||||||
.orElse(null), verificationSession.allowedToRequestCode(),
|
|
||||||
verificationSession.createdTimestamp(),
|
|
||||||
clock.millis(),
|
|
||||||
verificationSession.remoteExpirationSeconds());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/session/{sessionId}")
|
@Path("/session/{sessionId}")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 {
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013-2021 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.spam;
|
|
||||||
|
|
||||||
import javax.ws.rs.NameBinding;
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A name-binding annotation that associates {@link SpamFilter}s with resource methods.
|
|
||||||
*/
|
|
||||||
@NameBinding
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
|
||||||
public @interface FilterSpam {
|
|
||||||
}
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.spam;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import javax.ws.rs.container.ContainerRequestContext;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.UpdateVerificationSessionRequest;
|
||||||
|
import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
||||||
|
|
||||||
|
public interface RegistrationFraudChecker {
|
||||||
|
|
||||||
|
record VerificationCheck(Optional<VerificationSession> updatedSession, Optional<Float> scoreThreshold) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a registration attempt is suspicious
|
||||||
|
*
|
||||||
|
* @param requestContext The request context for an update verification session attempt
|
||||||
|
* @param verificationSession The target verification session
|
||||||
|
* @param e164 The target phone number
|
||||||
|
* @param request The information to add to the verification session
|
||||||
|
* @return A SessionUpdate including updates to the verification session that should be persisted to the caller along
|
||||||
|
* with other constraints to enforce when evaluating the UpdateVerificationSessionRequest.
|
||||||
|
*/
|
||||||
|
VerificationCheck checkVerificationAttempt(
|
||||||
|
final ContainerRequestContext requestContext,
|
||||||
|
final VerificationSession verificationSession,
|
||||||
|
final String e164,
|
||||||
|
final UpdateVerificationSessionRequest request);
|
||||||
|
|
||||||
|
static RegistrationFraudChecker noop() {
|
||||||
|
return (ignoredContext, ignoredSession, ignoredE164, ignoredRequest) -> new VerificationCheck(
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
* {@link #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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
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.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* <p>
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 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 SenderOverride may be provided by an upstream request filter. If request contains a property for
|
|
||||||
* {@link #SMS_SENDER_OVERRIDE_PROPERTY_NAME} or {@link #VOICE_SENDER_OVERRIDE_PROPERTY_NAME} it can be
|
|
||||||
* forwarded to a downstream filter to indicate a specific sender should be used when sending verification codes.
|
|
||||||
*/
|
|
||||||
public class SenderOverride {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SenderOverride.class);
|
|
||||||
public static final String SMS_SENDER_OVERRIDE_PROPERTY_NAME = "smsSenderOverride";
|
|
||||||
public static final String VOICE_SENDER_OVERRIDE_PROPERTY_NAME = "voiceSenderOverride";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the sender to use to deliver a verification code via SMS
|
|
||||||
*/
|
|
||||||
private final Optional<String> smsSenderOverride;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the sender to use to deliver a verification code via voice
|
|
||||||
*/
|
|
||||||
private final Optional<String> voiceSenderOverride;
|
|
||||||
|
|
||||||
public SenderOverride(final ContainerRequest containerRequest) {
|
|
||||||
this.smsSenderOverride = parse(String.class, SMS_SENDER_OVERRIDE_PROPERTY_NAME, containerRequest);
|
|
||||||
this.voiceSenderOverride = parse(String.class, VOICE_SENDER_OVERRIDE_PROPERTY_NAME, containerRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <T> Optional<T> parse(Class<T> type, final String propertyName,
|
|
||||||
final ContainerRequest containerRequest) {
|
|
||||||
return Optional
|
|
||||||
.ofNullable(containerRequest.getProperty(propertyName))
|
|
||||||
.flatMap(obj -> {
|
|
||||||
if (type.isInstance(obj)) {
|
|
||||||
return Optional.of(type.cast(obj));
|
|
||||||
}
|
|
||||||
logger.warn("invalid format for filter provided property {}: {}", propertyName, obj);
|
|
||||||
return Optional.empty();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Optional<String> getSmsSenderOverride() {
|
|
||||||
return smsSenderOverride;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<String> getVoiceSenderOverride() {
|
|
||||||
return voiceSenderOverride;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 SenderOverride} out of a {@link ContainerRequest} to provide to jersey resources.
|
|
||||||
* <p>
|
|
||||||
* A request filter may enrich a ContainerRequest with senderOverrides by providing a string property names defined in
|
|
||||||
* {@link SenderOverride}. This indicates the desired senderOverride to use when sending verification codes.
|
|
||||||
* <p>
|
|
||||||
* A resource can consume a SenderOverride with by annotating a SenderOverride parameter with {@link Extract}
|
|
||||||
*/
|
|
||||||
public class SenderOverrideProvider implements ValueParamProvider {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the SenderOverrideProvider
|
|
||||||
*/
|
|
||||||
public static class SenderOverrideFeature implements Feature {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean configure(FeatureContext context) {
|
|
||||||
context.register(new AbstractBinder() {
|
|
||||||
@Override
|
|
||||||
protected void configure() {
|
|
||||||
bind(SenderOverrideProvider.class)
|
|
||||||
.to(ValueParamProvider.class)
|
|
||||||
.in(Singleton.class);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Function<ContainerRequest, ?> getValueProvider(final Parameter parameter) {
|
|
||||||
if (parameter.getRawType().equals(SenderOverride.class)
|
|
||||||
&& parameter.isAnnotationPresent(Extract.class)) {
|
|
||||||
return SenderOverride::new;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PriorityType getPriority() {
|
|
||||||
return Priority.HIGH;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,17 +11,14 @@ import javax.ws.rs.container.ContainerRequestFilter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A spam filter is a {@link ContainerRequestFilter} that filters requests to endpoints to detect and respond to
|
* A spam filter provides various checkers and listeners to detect and respond to patterns of spam and fraud.
|
||||||
* patterns of spam and fraud.
|
|
||||||
* <p/>
|
* <p/>
|
||||||
* Spam filters are managed components that are generally loaded dynamically via a {@link java.util.ServiceLoader}.
|
* Spam filters are managed components that are generally loaded dynamically via a {@link java.util.ServiceLoader}.
|
||||||
* Their {@link #configure(String)} method will be called prior to be adding to the server's pool of {@link Managed}
|
* Their {@link #configure(String)} method will be called prior to be adding to the server's pool of {@link Managed}
|
||||||
* objects.
|
* objects.
|
||||||
* <p/>
|
* <p/>
|
||||||
* Spam filters must be annotated with {@link FilterSpam}, a name binding annotation that restricts the endpoints to
|
|
||||||
* which the filter may apply.
|
|
||||||
*/
|
*/
|
||||||
public interface SpamFilter extends ContainerRequestFilter, Managed {
|
public interface SpamFilter extends Managed {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures this spam filter. This method will be called before the filter is added to the server's pool of managed
|
* Configures this spam filter. This method will be called before the filter is added to the server's pool of managed
|
||||||
|
@ -65,6 +62,13 @@ public interface SpamFilter extends ContainerRequestFilter, Managed {
|
||||||
*/
|
*/
|
||||||
SpamChecker getSpamChecker();
|
SpamChecker getSpamChecker();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a checker that will be called to check registration attempts
|
||||||
|
*
|
||||||
|
* @return a {@link RegistrationFraudChecker} controlled by the spam filter
|
||||||
|
*/
|
||||||
|
RegistrationFraudChecker getRegistrationFraudChecker();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a checker that will be called to determine what constraints should be applied
|
* Return a checker that will be called to determine what constraints should be applied
|
||||||
* when a user requests or solves a challenge (captchas, push challenges, etc).
|
* when a user requests or solves a challenge (captchas, push challenges, etc).
|
||||||
|
|
|
@ -78,8 +78,6 @@ import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMa
|
||||||
import org.whispersystems.textsecuregcm.mappers.JsonMappingExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.JsonMappingExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider;
|
|
||||||
import org.whispersystems.textsecuregcm.spam.SenderOverrideProvider;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||||
|
@ -151,8 +149,6 @@ class AccountControllerTest {
|
||||||
.addProvider(new NonNormalizedPhoneNumberExceptionMapper())
|
.addProvider(new NonNormalizedPhoneNumberExceptionMapper())
|
||||||
.addProvider(TEST_REMOTE_ADDRESS_FILTER_PROVIDER)
|
.addProvider(TEST_REMOTE_ADDRESS_FILTER_PROVIDER)
|
||||||
.addProvider(new RateLimitByIpFilter(rateLimiters))
|
.addProvider(new RateLimitByIpFilter(rateLimiters))
|
||||||
.addProvider(ScoreThresholdProvider.ScoreThresholdFeature.class)
|
|
||||||
.addProvider(SenderOverrideProvider.SenderOverrideFeature.class)
|
|
||||||
.setMapper(SystemMapper.jsonMapper())
|
.setMapper(SystemMapper.jsonMapper())
|
||||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||||
.addResource(new AccountController(
|
.addResource(new AccountController(
|
||||||
|
|
|
@ -73,8 +73,7 @@ import org.whispersystems.textsecuregcm.registration.RegistrationServiceExceptio
|
||||||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException;
|
import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException;
|
||||||
import org.whispersystems.textsecuregcm.registration.TransportNotAllowedException;
|
import org.whispersystems.textsecuregcm.registration.TransportNotAllowedException;
|
||||||
import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
||||||
import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider;
|
import org.whispersystems.textsecuregcm.spam.RegistrationFraudChecker;
|
||||||
import org.whispersystems.textsecuregcm.spam.SenderOverrideProvider;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
|
@ -112,14 +111,12 @@ class VerificationControllerTest {
|
||||||
.addProvider(new ImpossiblePhoneNumberExceptionMapper())
|
.addProvider(new ImpossiblePhoneNumberExceptionMapper())
|
||||||
.addProvider(new NonNormalizedPhoneNumberExceptionMapper())
|
.addProvider(new NonNormalizedPhoneNumberExceptionMapper())
|
||||||
.addProvider(new RegistrationServiceSenderExceptionMapper())
|
.addProvider(new RegistrationServiceSenderExceptionMapper())
|
||||||
.addProvider(ScoreThresholdProvider.ScoreThresholdFeature.class)
|
|
||||||
.addProvider(SenderOverrideProvider.SenderOverrideFeature.class)
|
|
||||||
.setMapper(SystemMapper.jsonMapper())
|
.setMapper(SystemMapper.jsonMapper())
|
||||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||||
.addResource(
|
.addResource(
|
||||||
new VerificationController(registrationServiceClient, verificationSessionManager, pushNotificationManager,
|
new VerificationController(registrationServiceClient, verificationSessionManager, pushNotificationManager,
|
||||||
registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters, accountsManager,
|
registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters, accountsManager,
|
||||||
dynamicConfigurationManager, clock))
|
RegistrationFraudChecker.noop(), dynamicConfigurationManager, clock))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
|
Loading…
Reference in New Issue