diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 7bbdc71ee..7a1bdbf57 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -157,13 +157,14 @@ 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; + } + })); + } + + @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(); + })); + } + +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/UsernamesManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/UsernamesManager.java index 5816affba..09bb3ee63 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/UsernamesManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/UsernamesManager.java @@ -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); diff --git a/service/src/main/resources/accountsdb.xml b/service/src/main/resources/accountsdb.xml index 17758cb82..4ff07f2c2 100644 --- a/service/src/main/resources/accountsdb.xml +++ b/service/src/main/resources/accountsdb.xml @@ -223,4 +223,20 @@ + + + + + + + + + + + + + + + + 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 new file mode 100644 index 000000000..975a4284d --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/ReservedUsernamesTest.java @@ -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())); + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/UsernamesManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/UsernamesManagerTest.java index 8f8791890..3afda4ec3 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/UsernamesManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/UsernamesManagerTest.java @@ -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 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 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 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 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 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 retrieved = usernamesManager.get(uuid); assertTrue(retrieved.isPresent());