From c910fa406d475f31a4de4912cef61945c470c423 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Tue, 16 Nov 2021 17:30:18 -0500 Subject: [PATCH] Migrate reserved usernames from a relational database to DynamoDB --- service/config/sample.yml | 4 + .../WhisperServerConfiguration.java | 9 ++ .../textsecuregcm/WhisperServerService.java | 6 +- .../storage/ReservedUsernames.java | 115 +++++++++++------- .../workers/DeleteUserCommand.java | 7 +- .../SetUserDiscoverabilityCommand.java | 6 +- .../storage/ReservedUsernamesTest.java | 69 +++++++++++ .../tests/storage/ReservedUsernamesTest.java | 76 ------------ 8 files changed, 172 insertions(+), 120 deletions(-) create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/storage/ReservedUsernamesTest.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/ReservedUsernamesTest.java diff --git a/service/config/sample.yml b/service/config/sample.yml index 5ac443238..57fcf7b0c 100644 --- a/service/config/sample.yml +++ b/service/config/sample.yml @@ -153,6 +153,10 @@ pendingDevicesDynamoDb: # DynamoDB table configuration region: us-west-2 tableName: Example_PendingDevices +reservedUsernamesDynamoDb: # DynamoDB table configuration + region: us-west-2 + tableName: Example_ReservedUsernames + phoneNumberIdentifiersDynamoDb: # DynamoDB table configuration region: us-west-2 tableName: Example_PhoneNumberIdentifiers diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java index ac43b4a55..9cf3989fc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java @@ -206,6 +206,11 @@ public class WhisperServerConfiguration extends Configuration { @JsonProperty private DynamoDbConfiguration pendingDevicesDynamoDb; + @Valid + @NotNull + @JsonProperty + private DynamoDbConfiguration reservedUsernamesDynamoDb; + @Valid @NotNull @JsonProperty @@ -551,6 +556,10 @@ public class WhisperServerConfiguration extends Configuration { return pendingDevicesDynamoDb; } + public DynamoDbConfiguration getReservedUsernamesDynamoDbConfiguration() { + return reservedUsernamesDynamoDb; + } + public DonationConfiguration getDonationConfiguration() { return donation; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 8212086e8..d09394375 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -332,6 +332,9 @@ public class WhisperServerService extends Application jdbi.withHandle(handle -> { - try (Timer.Context ignored = queryTimer.time()) { - Optional reservations = handle.createQuery("SELECT COUNT(*) FROM reserved_usernames WHERE " + UID + " != :uuid AND :username ~* " + USERNAME) - .bind("username", username) - .bind("uuid", uuid) - .mapTo(Integer.class) - .findFirst(); - - return reservations.isPresent() && reservations.get() > 0; - } - })); - } + private final LoadingCache patternCache = CacheBuilder.newBuilder() + .maximumSize(1_000) + .build(new CacheLoader<>() { + @Override + public Pattern load(final String s) { + return Pattern.compile(s, Pattern.CASE_INSENSITIVE); + } + }); @VisibleForTesting - public void setReserved(String username, UUID reservedFor) { - database.use(jdbi -> jdbi.useHandle(handle -> { - handle.createUpdate("INSERT INTO reserved_usernames (" + USERNAME + ", " + UID + ") VALUES(:username, :uuid)") - .bind("username", username) - .bind("uuid", reservedFor) - .execute(); - })); + static final String KEY_PATTERN = "P"; + private static final String ATTR_RESERVED_FOR_UUID = "U"; + + private static final Timer IS_RESERVED_TIMER = Metrics.timer(name(ReservedUsernames.class, "isReserved")); + + private static final Logger log = LoggerFactory.getLogger(ReservedUsernames.class); + + public ReservedUsernames(final DynamoDbClient dynamoDbClient, final String tableName) { + this.dynamoDbClient = dynamoDbClient; + this.tableName = tableName; } + public boolean isReserved(final String username, final UUID accountIdentifier) { + return IS_RESERVED_TIMER.record(() -> { + final ScanIterable scanIterable = dynamoDbClient.scanPaginator(ScanRequest.builder() + .tableName(tableName) + .build()); + + for (final ScanResponse scanResponse : scanIterable) { + if (scanResponse.hasItems()) { + for (final Map item : scanResponse.items()) { + try { + final Pattern pattern = patternCache.get(item.get(KEY_PATTERN).s()); + final UUID reservedFor = AttributeValues.getUUID(item, ATTR_RESERVED_FOR_UUID, null); + + if (pattern.matcher(username).matches() && !accountIdentifier.equals(reservedFor)) { + return true; + } + } catch (final Exception e) { + log.error("Failed to load pattern from item: {}", item, e); + } + } + } + } + + return false; + }); + } + + public void reserveUsername(final String pattern, final UUID reservedFor) { + dynamoDbClient.putItem(PutItemRequest.builder() + .tableName(tableName) + .item(Map.of( + KEY_PATTERN, AttributeValues.fromString(pattern), + ATTR_RESERVED_FOR_UUID, AttributeValues.fromUUID(reservedFor))) + .build()); + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/DeleteUserCommand.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/DeleteUserCommand.java index 8ad9a4a80..e401606dd 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/DeleteUserCommand.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/DeleteUserCommand.java @@ -140,6 +140,10 @@ public class DeleteUserCommand extends EnvironmentCommand isReserved() { + return Stream.of( + Arguments.of("myusername", UUID.randomUUID(), true), + Arguments.of("myusername", RESERVED_FOR_UUID, false), + Arguments.of("thyusername", UUID.randomUUID(), false), + Arguments.of("somemyusername", UUID.randomUUID(), true), + Arguments.of("myusernamesome", UUID.randomUUID(), true), + Arguments.of("somemyusernamesome", UUID.randomUUID(), true), + Arguments.of("MYUSERNAME", UUID.randomUUID(), true), + Arguments.of("foobar", UUID.randomUUID(), true), + Arguments.of("foobar", RESERVED_FOR_UUID, false), + Arguments.of("somefoobar", UUID.randomUUID(), false), + Arguments.of("foobarsome", UUID.randomUUID(), false), + Arguments.of("somefoobarsome", UUID.randomUUID(), false), + Arguments.of("FOOBAR", UUID.randomUUID(), true)); + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/ReservedUsernamesTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/ReservedUsernamesTest.java deleted file mode 100644 index 54e194f6d..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/ReservedUsernamesTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2013-2020 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.tests.storage; - -import com.opentable.db.postgres.embedded.LiquibasePreparer; -import com.opentable.db.postgres.junit.EmbeddedPostgresRules; -import com.opentable.db.postgres.junit.PreparedDbRule; -import org.jdbi.v3.core.Jdbi; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; -import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase; -import org.whispersystems.textsecuregcm.storage.ReservedUsernames; -import org.whispersystems.textsecuregcm.storage.Usernames; - -import java.io.IOException; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Optional; -import java.util.UUID; - -import static junit.framework.TestCase.assertTrue; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.Assert.assertFalse; - -public class ReservedUsernamesTest { - - @Rule - public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase(LiquibasePreparer.forClasspathLocation("accountsdb.xml")); - - private ReservedUsernames reserved; - - @Before - public void setupAccountsDao() { - FaultTolerantDatabase faultTolerantDatabase = new FaultTolerantDatabase("reservedUsernamesTest", - Jdbi.create(db.getTestDatabase()), - new CircuitBreakerConfiguration()); - - this.reserved = new ReservedUsernames(faultTolerantDatabase); - } - - @Test - public void testReservedRegexp() { - UUID reservedFor = UUID.randomUUID(); - String username = ".*myusername.*"; - - reserved.setReserved(username, reservedFor); - - - assertTrue(reserved.isReserved("myusername", UUID.randomUUID())); - assertFalse(reserved.isReserved("myusername", reservedFor)); - assertFalse(reserved.isReserved("thyusername", UUID.randomUUID())); - assertTrue(reserved.isReserved("somemyusername", UUID.randomUUID())); - assertTrue(reserved.isReserved("myusernamesome", UUID.randomUUID())); - assertTrue(reserved.isReserved("somemyusernamesome", UUID.randomUUID())); - } - - @Test - public void testReservedLiteral() { - UUID reservedFor = UUID.randomUUID(); - String username = "^foobar$"; - - reserved.setReserved(username, reservedFor); - - assertTrue(reserved.isReserved("foobar", UUID.randomUUID())); - assertFalse(reserved.isReserved("foobar", reservedFor)); - assertFalse(reserved.isReserved("somefoobar", UUID.randomUUID())); - assertFalse(reserved.isReserved("foobarsome", UUID.randomUUID())); - assertFalse(reserved.isReserved("somefoobarsome", UUID.randomUUID())); - } -}