Username reservation table

This commit is contained in:
Moxie Marlinspike 2019-09-24 18:35:02 -07:00
parent 99c228dd6d
commit 523134f24b
6 changed files with 171 additions and 23 deletions

View File

@ -157,13 +157,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
FaultTolerantDatabase messageDatabase = new FaultTolerantDatabase("message_database", messageJdbi, config.getMessageStoreConfiguration().getCircuitBreakerConfiguration());
FaultTolerantDatabase abuseDatabase = new FaultTolerantDatabase("abuse_database", abuseJdbi, config.getAbuseDatabaseConfiguration().getCircuitBreakerConfiguration());
Accounts accounts = new Accounts(accountDatabase);
PendingAccounts pendingAccounts = new PendingAccounts(accountDatabase);
PendingDevices pendingDevices = new PendingDevices(accountDatabase);
Usernames usernames = new Usernames(accountDatabase);
Keys keys = new Keys(keysDatabase);
Messages messages = new Messages(messageDatabase);
AbusiveHostRules abusiveHostRules = new AbusiveHostRules(abuseDatabase);
Accounts accounts = new Accounts(accountDatabase);
PendingAccounts pendingAccounts = new PendingAccounts(accountDatabase);
PendingDevices pendingDevices = new PendingDevices (accountDatabase);
Usernames usernames = new Usernames(accountDatabase);
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
Keys keys = new Keys(keysDatabase);
Messages messages = new Messages(messageDatabase);
AbusiveHostRules abusiveHostRules = new AbusiveHostRules(abuseDatabase);
RedisClientFactory cacheClientFactory = new RedisClientFactory("main_cache", config.getCacheConfiguration().getUrl(), config.getCacheConfiguration().getReplicaUrls(), config.getCacheConfiguration().getCircuitBreakerConfiguration());
RedisClientFactory directoryClientFactory = new RedisClientFactory("directory_cache", config.getDirectoryConfiguration().getRedisConfiguration().getUrl(), config.getDirectoryConfiguration().getRedisConfiguration().getReplicaUrls(), config.getDirectoryConfiguration().getRedisConfiguration().getCircuitBreakerConfiguration());
@ -180,7 +181,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheClient);
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, cacheClient );
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
UsernamesManager usernamesManager = new UsernamesManager(usernames, cacheClient);
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheClient);
MessagesCache messagesCache = new MessagesCache(messagesClient, messages, accountsManager, config.getMessageCacheConfiguration().getPersistDelayMinutes());
MessagesManager messagesManager = new MessagesManager(messages, messagesCache);
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager);

View File

@ -0,0 +1,53 @@
package org.whispersystems.textsecuregcm.storage;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import com.google.common.annotations.VisibleForTesting;
import org.whispersystems.textsecuregcm.util.Constants;
import java.util.Optional;
import java.util.UUID;
import static com.codahale.metrics.MetricRegistry.name;
public class ReservedUsernames {
public static final String ID = "id";
public static final String UID = "uuid";
public static final String USERNAME = "username";
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;
}
}));
}
@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();
}));
}
}

View File

@ -30,15 +30,21 @@ public class UsernamesManager {
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
private final Usernames usernames;
private final ReservedUsernames reservedUsernames;
private final ReplicatedJedisPool cacheClient;
public UsernamesManager(Usernames usernames, ReplicatedJedisPool cacheClient) {
this.usernames = usernames;
this.cacheClient = cacheClient;
public UsernamesManager(Usernames usernames, ReservedUsernames reservedUsernames, ReplicatedJedisPool cacheClient) {
this.usernames = usernames;
this.reservedUsernames = reservedUsernames;
this.cacheClient = cacheClient;
}
public boolean put(UUID uuid, String username) {
try (Timer.Context ignored = createTimer.time()) {
if (reservedUsernames.isReserved(username, uuid)) {
return false;
}
if (databasePut(uuid, username)) {
redisSet(uuid, username);

View File

@ -223,4 +223,20 @@
</createTable>
</changeSet>
<changeSet id="10" author="moxie">
<createTable tableName="reserved_usernames">
<column name="id" type="bigint" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="username" type="text">
<constraints nullable="false" unique="true"/>
</column>
<column name="uuid" type="uuid">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,71 @@
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()));
}
}

View File

@ -2,21 +2,16 @@ package org.whispersystems.textsecuregcm.tests.storage;
import org.junit.Test;
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.ReservedUsernames;
import org.whispersystems.textsecuregcm.storage.Usernames;
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import java.util.HashSet;
import java.util.Optional;
import java.util.UUID;
import static junit.framework.TestCase.assertSame;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import redis.clients.jedis.Jedis;
@ -29,13 +24,14 @@ public class UsernamesManagerTest {
ReplicatedJedisPool cacheClient = mock(ReplicatedJedisPool.class);
Jedis jedis = mock(Jedis.class );
Usernames usernames = mock(Usernames.class );
ReservedUsernames reserved = mock(ReservedUsernames.class);
UUID uuid = UUID.randomUUID();
when(cacheClient.getReadResource()).thenReturn(jedis);
when(jedis.get(eq("UsernameByUsername::n00bkiller"))).thenReturn(uuid.toString());
UsernamesManager usernamesManager = new UsernamesManager(usernames, cacheClient);
UsernamesManager usernamesManager = new UsernamesManager(usernames, reserved, cacheClient);
Optional<UUID> retrieved = usernamesManager.get("n00bkiller");
assertTrue(retrieved.isPresent());
@ -52,13 +48,14 @@ public class UsernamesManagerTest {
ReplicatedJedisPool cacheClient = mock(ReplicatedJedisPool.class);
Jedis jedis = mock(Jedis.class );
Usernames usernames = mock(Usernames.class );
ReservedUsernames reserved = mock(ReservedUsernames.class);
UUID uuid = UUID.randomUUID();
when(cacheClient.getReadResource()).thenReturn(jedis);
when(jedis.get(eq("UsernameByUuid::" + uuid.toString()))).thenReturn("n00bkiller");
UsernamesManager usernamesManager = new UsernamesManager(usernames, cacheClient);
UsernamesManager usernamesManager = new UsernamesManager(usernames, reserved, cacheClient);
Optional<String> retrieved = usernamesManager.get(uuid);
assertTrue(retrieved.isPresent());
@ -76,6 +73,7 @@ public class UsernamesManagerTest {
ReplicatedJedisPool cacheClient = mock(ReplicatedJedisPool.class);
Jedis jedis = mock(Jedis.class );
Usernames usernames = mock(Usernames.class );
ReservedUsernames reserved = mock(ReservedUsernames.class);
UUID uuid = UUID.randomUUID();
@ -85,7 +83,7 @@ public class UsernamesManagerTest {
when(jedis.get(eq("UsernameByUsername::n00bkiller"))).thenReturn(null);
when(usernames.get(eq("n00bkiller"))).thenReturn(Optional.of(uuid));
UsernamesManager usernamesManager = new UsernamesManager(usernames, cacheClient);
UsernamesManager usernamesManager = new UsernamesManager(usernames, reserved, cacheClient);
Optional<UUID> retrieved = usernamesManager.get("n00bkiller");
assertTrue(retrieved.isPresent());
@ -106,6 +104,7 @@ public class UsernamesManagerTest {
ReplicatedJedisPool cacheClient = mock(ReplicatedJedisPool.class);
Jedis jedis = mock(Jedis.class );
Usernames usernames = mock(Usernames.class );
ReservedUsernames reserved = mock(ReservedUsernames.class);
UUID uuid = UUID.randomUUID();
@ -114,7 +113,7 @@ public class UsernamesManagerTest {
when(jedis.get(eq("UsernameByUuid::" + uuid.toString()))).thenReturn(null);
when(usernames.get(eq(uuid))).thenReturn(Optional.of("n00bkiller"));
UsernamesManager usernamesManager = new UsernamesManager(usernames, cacheClient);
UsernamesManager usernamesManager = new UsernamesManager(usernames, reserved, cacheClient);
Optional<String> retrieved = usernamesManager.get(uuid);
assertTrue(retrieved.isPresent());
@ -135,6 +134,7 @@ public class UsernamesManagerTest {
ReplicatedJedisPool cacheClient = mock(ReplicatedJedisPool.class);
Jedis jedis = mock(Jedis.class );
Usernames usernames = mock(Usernames.class );
ReservedUsernames reserved = mock(ReservedUsernames.class);
UUID uuid = UUID.randomUUID();
@ -143,7 +143,7 @@ public class UsernamesManagerTest {
when(jedis.get(eq("UsernameByUsername::n00bkiller"))).thenThrow(new JedisException("Connection lost!"));
when(usernames.get(eq("n00bkiller"))).thenReturn(Optional.of(uuid));
UsernamesManager usernamesManager = new UsernamesManager(usernames, cacheClient);
UsernamesManager usernamesManager = new UsernamesManager(usernames, reserved, cacheClient);
Optional<UUID> retrieved = usernamesManager.get("n00bkiller");
assertTrue(retrieved.isPresent());
@ -164,6 +164,7 @@ public class UsernamesManagerTest {
ReplicatedJedisPool cacheClient = mock(ReplicatedJedisPool.class);
Jedis jedis = mock(Jedis.class );
Usernames usernames = mock(Usernames.class );
ReservedUsernames reserved = mock(ReservedUsernames.class);
UUID uuid = UUID.randomUUID();
@ -172,7 +173,7 @@ public class UsernamesManagerTest {
when(jedis.get(eq("UsernameByUuid::" + uuid))).thenThrow(new JedisException("Connection lost!"));
when(usernames.get(eq(uuid))).thenReturn(Optional.of("n00bkiller"));
UsernamesManager usernamesManager = new UsernamesManager(usernames, cacheClient);
UsernamesManager usernamesManager = new UsernamesManager(usernames, reserved, cacheClient);
Optional<String> retrieved = usernamesManager.get(uuid);
assertTrue(retrieved.isPresent());