Migrate reserved usernames from a relational database to DynamoDB
This commit is contained in:
parent
559205e33f
commit
c910fa406d
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -332,6 +332,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
DynamoDbClient accountsDynamoDbClient = DynamoDbFromConfig.client(config.getAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient reservedUsernamesDynamoDbClient = DynamoDbFromConfig.client(config.getReservedUsernamesDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
|
||||
DynamoDbFromConfig.client(config.getPhoneNumberIdentifiersDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
@ -376,7 +379,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
|
||||
config.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(reservedUsernamesDynamoDbClient,
|
||||
config.getReservedUsernamesDynamoDbConfiguration().getTableName());
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
Keys keys = new Keys(preKeyDynamoDb, config.getKeysDynamoDbConfiguration().getTableName());
|
||||
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb,
|
||||
|
|
|
@ -1,58 +1,91 @@
|
|||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.Timer;
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import java.util.Optional;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import java.util.regex.Pattern;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScanResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.paginators.ScanIterable;
|
||||
|
||||
public class ReservedUsernames {
|
||||
|
||||
public static final String ID = "id";
|
||||
public static final String UID = "uuid";
|
||||
public static final String USERNAME = "username";
|
||||
private final DynamoDbClient dynamoDbClient;
|
||||
private final String tableName;
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Timer queryTimer = metricRegistry.timer(name(ReservedUsernames.class, "query"));
|
||||
|
||||
private final FaultTolerantDatabase database;
|
||||
|
||||
public ReservedUsernames(FaultTolerantDatabase database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public boolean isReserved(String username, UUID uuid) {
|
||||
return database.with(jdbi -> jdbi.withHandle(handle -> {
|
||||
try (Timer.Context ignored = queryTimer.time()) {
|
||||
Optional<Integer> 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<String, Pattern> 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<String, AttributeValue> 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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,6 +140,10 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
|||
DynamoDbClient pendingAccountsDynamoDbClient = DynamoDbFromConfig.client(configuration.getPendingAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient reservedUsernamesDynamoDbClient =
|
||||
DynamoDbFromConfig.client(configuration.getReservedUsernamesDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
AmazonDynamoDB deletedAccountsLockDynamoDbClient = AmazonDynamoDBClientBuilder.standard()
|
||||
.withRegion(configuration.getDeletedAccountsLockDynamoDbConfiguration().getRegion())
|
||||
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(
|
||||
|
@ -166,7 +170,8 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
|||
configuration.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(reservedUsernamesDynamoDbClient,
|
||||
configuration.getReservedUsernamesDynamoDbConfiguration().getTableName());
|
||||
Keys keys = new Keys(preKeysDynamoDb,
|
||||
configuration.getKeysDynamoDbConfiguration().getTableName());
|
||||
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb,
|
||||
|
|
|
@ -118,6 +118,9 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
|||
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
|
||||
DynamoDbFromConfig.client(configuration.getPhoneNumberIdentifiersDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
DynamoDbClient reservedUsernamesDynamoDbClient =
|
||||
DynamoDbFromConfig.client(configuration.getReservedUsernamesDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster",
|
||||
configuration.getCacheClusterConfiguration(), redisClusterClientResources);
|
||||
|
@ -171,7 +174,8 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
|||
configuration.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(reservedUsernamesDynamoDbClient,
|
||||
configuration.getReservedUsernamesDynamoDbConfiguration().getTableName());
|
||||
Keys keys = new Keys(preKeysDynamoDb,
|
||||
configuration.getKeysDynamoDbConfiguration().getTableName());
|
||||
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb,
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
|
||||
|
||||
class ReservedUsernamesTest {
|
||||
|
||||
private static final String RESERVED_USERNAMES_TABLE_NAME = "reserved_usernames_test";
|
||||
|
||||
@RegisterExtension
|
||||
static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder()
|
||||
.tableName(RESERVED_USERNAMES_TABLE_NAME)
|
||||
.hashKey(ReservedUsernames.KEY_PATTERN)
|
||||
.attributeDefinition(AttributeDefinition.builder()
|
||||
.attributeName(ReservedUsernames.KEY_PATTERN)
|
||||
.attributeType(ScalarAttributeType.S)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
private static final UUID RESERVED_FOR_UUID = UUID.randomUUID();
|
||||
|
||||
private ReservedUsernames reservedUsernames;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
reservedUsernames =
|
||||
new ReservedUsernames(dynamoDbExtension.getDynamoDbClient(), RESERVED_USERNAMES_TABLE_NAME);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void isReserved(final String username, final UUID uuid, final boolean expectReserved) {
|
||||
reservedUsernames.reserveUsername(".*myusername.*", RESERVED_FOR_UUID);
|
||||
reservedUsernames.reserveUsername("^foobar$", RESERVED_FOR_UUID);
|
||||
|
||||
assertEquals(expectReserved, reservedUsernames.isReserved(username, uuid));
|
||||
}
|
||||
|
||||
private static Stream<Arguments> 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));
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue