Add an endpoint for testing whether an account with a given ACI or PNI exists
This commit is contained in:
parent
e6237480f8
commit
975f753c2b
|
@ -63,6 +63,9 @@ public class RateLimitsConfiguration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private RateLimitConfiguration usernameSet = new RateLimitConfiguration(100, 100 / (24.0 * 60.0));
|
private RateLimitConfiguration usernameSet = new RateLimitConfiguration(100, 100 / (24.0 * 60.0));
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private RateLimitConfiguration checkAccountExistence = new RateLimitConfiguration(1_000, 1_000 / 60.0);
|
||||||
|
|
||||||
public RateLimitConfiguration getAutoBlock() {
|
public RateLimitConfiguration getAutoBlock() {
|
||||||
return autoBlock;
|
return autoBlock;
|
||||||
}
|
}
|
||||||
|
@ -135,6 +138,10 @@ public class RateLimitsConfiguration {
|
||||||
return usernameSet;
|
return usernameSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RateLimitConfiguration getCheckAccountExistence() {
|
||||||
|
return checkAccountExistence;
|
||||||
|
}
|
||||||
|
|
||||||
public static class RateLimitConfiguration {
|
public static class RateLimitConfiguration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private int bucketSize;
|
private int bucketSize;
|
||||||
|
|
|
@ -15,6 +15,7 @@ import io.dropwizard.auth.Auth;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
import io.micrometer.core.instrument.Tag;
|
import io.micrometer.core.instrument.Tag;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -22,13 +23,17 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
import javax.ws.rs.BadRequestException;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.HEAD;
|
||||||
import javax.ws.rs.HeaderParam;
|
import javax.ws.rs.HeaderParam;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
|
@ -36,8 +41,11 @@ import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
|
@ -627,6 +635,30 @@ public class AccountController {
|
||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HEAD
|
||||||
|
@Path("/account/{uuid}")
|
||||||
|
public Response accountExists(
|
||||||
|
@HeaderParam("X-Forwarded-For") final String forwardedFor,
|
||||||
|
@PathParam("uuid") final UUID uuid,
|
||||||
|
@Context HttpServletRequest request) throws RateLimitExceededException {
|
||||||
|
|
||||||
|
// Disallow clients from making authenticated requests to this endpoint
|
||||||
|
if (StringUtils.isNotBlank(request.getHeader("Authorization"))) {
|
||||||
|
throw new BadRequestException();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String mostRecentProxy = ForwardedIpUtil.getMostRecentProxy(forwardedFor)
|
||||||
|
.orElseThrow(() -> new RateLimitExceededException(Duration.ofHours(1)));
|
||||||
|
|
||||||
|
rateLimiters.getCheckAccountExistenceLimiter().validate(mostRecentProxy);
|
||||||
|
|
||||||
|
final Status status = accounts.getByAccountIdentifier(uuid)
|
||||||
|
.or(() -> accounts.getByPhoneNumberIdentifier(uuid))
|
||||||
|
.isPresent() ? Status.OK : Status.NOT_FOUND;
|
||||||
|
|
||||||
|
return Response.status(status).build();
|
||||||
|
}
|
||||||
|
|
||||||
private void verifyRegistrationLock(final Account existingAccount, @Nullable final String clientRegistrationLock)
|
private void verifyRegistrationLock(final Account existingAccount, @Nullable final String clientRegistrationLock)
|
||||||
throws RateLimitExceededException, WebApplicationException {
|
throws RateLimitExceededException, WebApplicationException {
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,8 @@ public class RateLimiters {
|
||||||
private final RateLimiter usernameLookupLimiter;
|
private final RateLimiter usernameLookupLimiter;
|
||||||
private final RateLimiter usernameSetLimiter;
|
private final RateLimiter usernameSetLimiter;
|
||||||
|
|
||||||
|
private final RateLimiter checkAccountExistenceLimiter;
|
||||||
|
|
||||||
private final AtomicReference<CardinalityRateLimiter> unsealedSenderCardinalityLimiter;
|
private final AtomicReference<CardinalityRateLimiter> unsealedSenderCardinalityLimiter;
|
||||||
private final AtomicReference<RateLimiter> unsealedIpLimiter;
|
private final AtomicReference<RateLimiter> unsealedIpLimiter;
|
||||||
private final AtomicReference<RateLimiter> rateLimitResetLimiter;
|
private final AtomicReference<RateLimiter> rateLimitResetLimiter;
|
||||||
|
@ -127,6 +129,10 @@ public class RateLimiters {
|
||||||
config.getUsernameSet().getBucketSize(),
|
config.getUsernameSet().getBucketSize(),
|
||||||
config.getUsernameSet().getLeakRatePerMinute());
|
config.getUsernameSet().getLeakRatePerMinute());
|
||||||
|
|
||||||
|
this.checkAccountExistenceLimiter = new RateLimiter(cacheCluster, "checkAccountExistence",
|
||||||
|
config.getCheckAccountExistence().getBucketSize(),
|
||||||
|
config.getCheckAccountExistence().getLeakRatePerMinute());
|
||||||
|
|
||||||
this.dailyPreKeysLimiter = new AtomicReference<>(createDailyPreKeysLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getDailyPreKeys()));
|
this.dailyPreKeysLimiter = new AtomicReference<>(createDailyPreKeysLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getDailyPreKeys()));
|
||||||
|
|
||||||
this.unsealedSenderCardinalityLimiter = new AtomicReference<>(createUnsealedSenderCardinalityLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getUnsealedSenderNumber()));
|
this.unsealedSenderCardinalityLimiter = new AtomicReference<>(createUnsealedSenderCardinalityLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getUnsealedSenderNumber()));
|
||||||
|
@ -287,6 +293,10 @@ public class RateLimiters {
|
||||||
return usernameSetLimiter;
|
return usernameSetLimiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RateLimiter getCheckAccountExistenceLimiter() {
|
||||||
|
return checkAccountExistenceLimiter;
|
||||||
|
}
|
||||||
|
|
||||||
private CardinalityRateLimiter createUnsealedSenderCardinalityLimiter(FaultTolerantRedisCluster cacheCluster, CardinalityRateLimitConfiguration configuration) {
|
private CardinalityRateLimiter createUnsealedSenderCardinalityLimiter(FaultTolerantRedisCluster cacheCluster, CardinalityRateLimitConfiguration configuration) {
|
||||||
return new CardinalityRateLimiter(cacheCluster, "unsealedSender", configuration.getTtl(), configuration.getMaxCardinality());
|
return new CardinalityRateLimiter(cacheCluster, "unsealedSender", configuration.getTtl(), configuration.getMaxCardinality());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1749,4 +1749,51 @@ class AccountControllerTest {
|
||||||
Arguments.of("captcha enforced", true, Set.of("1"), 402)
|
Arguments.of("captcha enforced", true, Set.of("1"), 402)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAccountExists() {
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
|
||||||
|
final UUID accountIdentifier = UUID.randomUUID();
|
||||||
|
final UUID phoneNumberIdentifier = UUID.randomUUID();
|
||||||
|
|
||||||
|
when(accountsManager.getByAccountIdentifier(any())).thenReturn(Optional.empty());
|
||||||
|
when(accountsManager.getByAccountIdentifier(accountIdentifier)).thenReturn(Optional.of(account));
|
||||||
|
when(accountsManager.getByPhoneNumberIdentifier(any())).thenReturn(Optional.empty());
|
||||||
|
when(accountsManager.getByPhoneNumberIdentifier(phoneNumberIdentifier)).thenReturn(Optional.of(account));
|
||||||
|
|
||||||
|
when(rateLimiters.getCheckAccountExistenceLimiter()).thenReturn(mock(RateLimiter.class));
|
||||||
|
|
||||||
|
assertThat(resources.getJerseyTest()
|
||||||
|
.target(String.format("/v1/accounts/account/%s", accountIdentifier))
|
||||||
|
.request()
|
||||||
|
.header("X-Forwarded-For", "127.0.0.1")
|
||||||
|
.head()
|
||||||
|
.getStatus()).isEqualTo(200);
|
||||||
|
|
||||||
|
assertThat(resources.getJerseyTest()
|
||||||
|
.target(String.format("/v1/accounts/account/%s", phoneNumberIdentifier))
|
||||||
|
.request()
|
||||||
|
.header("X-Forwarded-For", "127.0.0.1")
|
||||||
|
.head()
|
||||||
|
.getStatus()).isEqualTo(200);
|
||||||
|
|
||||||
|
assertThat(resources.getJerseyTest()
|
||||||
|
.target(String.format("/v1/accounts/account/%s", UUID.randomUUID()))
|
||||||
|
.request()
|
||||||
|
.header("X-Forwarded-For", "127.0.0.1")
|
||||||
|
.head()
|
||||||
|
.getStatus()).isEqualTo(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAccountExistsAuthenticated() {
|
||||||
|
assertThat(resources.getJerseyTest()
|
||||||
|
.target(String.format("/v1/accounts/account/%s", UUID.randomUUID()))
|
||||||
|
.request()
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||||
|
.header("X-Forwarded-For", "127.0.0.1")
|
||||||
|
.head()
|
||||||
|
.getStatus()).isEqualTo(400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue