Remove two-stage check of username availability in reserve/confirm

This commit is contained in:
Jonathan Klabunde Tomer
2024-01-09 14:01:42 -08:00
committed by GitHub
parent ed972a0037
commit 184cdc0331
5 changed files with 91 additions and 106 deletions

View File

@@ -77,6 +77,7 @@ import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
import org.whispersystems.textsecuregcm.storage.AccountsManager.UsernameReservation;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
import org.whispersystems.textsecuregcm.tests.util.DevicesHelper;
@@ -86,6 +87,7 @@ import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper;
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.TestClock;
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
class AccountsManagerTest {
@@ -193,7 +195,6 @@ class AccountsManagerTest {
enrollmentManager = mock(ExperimentEnrollmentManager.class);
when(enrollmentManager.isEnrolled(any(UUID.class), eq(AccountsManager.USERNAME_EXPERIMENT_NAME))).thenReturn(true);
when(accounts.usernameHashAvailable(any())).thenReturn(CompletableFuture.completedFuture(true));
final AccountLockManager accountLockManager = mock(AccountLockManager.class);
@@ -1587,18 +1588,36 @@ class AccountsManagerTest {
@Test
void testReserveUsernameHash() throws UsernameHashNotAvailableException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
final List<byte[]> usernameHashes = List.of(new byte[32], new byte[32]);
when(accounts.usernameHashAvailable(any())).thenReturn(CompletableFuture.completedFuture(true));
when(accounts.getByAccountIdentifierAsync(account.getUuid())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
final List<byte[]> usernameHashes = List.of(TestRandomUtil.nextBytes(32), TestRandomUtil.nextBytes(32));
when(accounts.reserveUsernameHash(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
accountsManager.reserveUsernameHash(account, usernameHashes);
verify(accounts).reserveUsernameHash(eq(account), eq(new byte[32]), eq(Duration.ofMinutes(5)));
UsernameReservation result = accountsManager.reserveUsernameHash(account, usernameHashes).join();
assertArrayEquals(usernameHashes.get(0), result.reservedUsernameHash());
verify(accounts, times(1)).reserveUsernameHash(eq(account), any(), eq(Duration.ofMinutes(5)));
}
@Test
void testReserveUsernameOptimisticLockingFailure() throws UsernameHashNotAvailableException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
when(accounts.getByAccountIdentifierAsync(account.getUuid())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
final List<byte[]> usernameHashes = List.of(TestRandomUtil.nextBytes(32), TestRandomUtil.nextBytes(32));
when(accounts.reserveUsernameHash(any(), any(), any()))
.thenReturn(CompletableFuture.failedFuture(new ContestedOptimisticLockException()))
.thenReturn(CompletableFuture.completedFuture(null));
UsernameReservation result = accountsManager.reserveUsernameHash(account, usernameHashes).join();
assertArrayEquals(usernameHashes.get(0), result.reservedUsernameHash());
verify(accounts, times(2)).reserveUsernameHash(eq(account), any(), eq(Duration.ofMinutes(5)));
}
@Test
void testReserveUsernameHashNotAvailable() {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
when(accounts.usernameHashAvailable(any())).thenReturn(CompletableFuture.completedFuture(false));
when(accounts.getByAccountIdentifierAsync(account.getUuid())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(accounts.reserveUsernameHash(any(), any(), any())).thenReturn(CompletableFuture.failedFuture(new UsernameHashNotAvailableException()));
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1, USERNAME_HASH_2)));
}
@@ -1615,8 +1634,6 @@ class AccountsManagerTest {
void testConfirmReservedUsernameHash() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
setReservationHash(account, USERNAME_HASH_1);
when(accounts.usernameHashAvailable(Optional.of(account.getUuid()), USERNAME_HASH_1))
.thenReturn(CompletableFuture.completedFuture(true));
when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
.thenReturn(CompletableFuture.completedFuture(null));
@@ -1625,12 +1642,26 @@ class AccountsManagerTest {
verify(accounts).confirmUsernameHash(eq(account), eq(USERNAME_HASH_1), eq(ENCRYPTED_USERNAME_1));
}
@Test
void testConfirmReservedUsernameHashOptimisticLockingFailure() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
setReservationHash(account, USERNAME_HASH_1);
when(accounts.getByAccountIdentifierAsync(account.getUuid())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
.thenReturn(CompletableFuture.failedFuture(new ContestedOptimisticLockException()))
.thenReturn(CompletableFuture.completedFuture(null));
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
verify(accounts, times(2)).confirmUsernameHash(eq(account), eq(USERNAME_HASH_1), eq(ENCRYPTED_USERNAME_1));
}
@Test
void testConfirmReservedHashNameMismatch() {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
setReservationHash(account, USERNAME_HASH_1);
when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1)))
.thenReturn(CompletableFuture.completedFuture(true));
when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
.thenReturn(CompletableFuture.completedFuture(null));
CompletableFutureTestUtil.assertFailsWithCause(UsernameReservationNotFoundException.class,
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_2));
}
@@ -1640,11 +1671,11 @@ class AccountsManagerTest {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
// hash was reserved, but the reservation lapsed and another account took it
setReservationHash(account, USERNAME_HASH_1);
when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1)))
.thenReturn(CompletableFuture.completedFuture(false));
when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
.thenReturn(CompletableFuture.failedFuture(new UsernameHashNotAvailableException()));
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
verify(accounts, never()).confirmUsernameHash(any(), any(), any());
assertTrue(account.getUsernameHash().isEmpty());
}
@Test

View File

@@ -182,7 +182,7 @@ class AccountsManagerUsernameIntegrationTest {
}
@Test
void testReserveUsernameSnatched() throws InterruptedException, UsernameHashNotAvailableException {
void testReserveUsernameGetFirstAvailableChoice() throws InterruptedException, UsernameHashNotAvailableException {
final Account account = AccountsHelper.createAccount(accountsManager, "+18005551111");
ArrayList<byte[]> usernameHashes = new ArrayList<>(Arrays.asList(USERNAME_HASH_1, USERNAME_HASH_2));
@@ -198,23 +198,14 @@ class AccountsManagerUsernameIntegrationTest {
byte[] availableHash = TestRandomUtil.nextBytes(32);
usernameHashes.add(availableHash);
usernameHashes.add(TestRandomUtil.nextBytes(32));
// first time this is called lie and say the username is available
// this simulates seeing an available username and then it being taken
// by someone before the write
doReturn(CompletableFuture.completedFuture(true))
.doCallRealMethod()
.when(accounts).usernameHashAvailable(any());
final byte[] username = accountsManager
.reserveUsernameHash(account, usernameHashes)
.join()
.reservedUsernameHash();
assertArrayEquals(username, availableHash);
// 1 attempt on first try (returns true),
// 5 more attempts until "availableHash" returns true
verify(accounts, times(4)).usernameHashAvailable(any());
}
@Test

View File

@@ -945,15 +945,15 @@ class AccountsTest {
verifyStoredState(firstAccount.getNumber(), firstAccount.getUuid(), firstAccount.getPhoneNumberIdentifier(), USERNAME_HASH_1, maybeAccount.get(), firstAccount);
// throw an error if second account tries to reserve or confirm the same username hash
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(secondAccount, USERNAME_HASH_1, Duration.ofDays(1)));
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.confirmUsernameHash(secondAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
// throw an error if first account tries to reserve or confirm the username hash that it has already confirmed
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1)));
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThat(secondAccount.getReservedUsernameHash()).isEmpty();
@@ -1029,9 +1029,9 @@ class AccountsTest {
assertThat(account1.getUsernameHash()).isEmpty();
// account 2 shouldn't be able to reserve or confirm the same username hash
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)));
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
@@ -1095,7 +1095,7 @@ class AccountsTest {
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).doesNotContainKey(Accounts.ATTR_TTL);
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)));
assertThat(account.getReservedUsernameHash()).isEmpty();
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
@@ -1103,24 +1103,6 @@ class AccountsTest {
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).doesNotContainKey(Accounts.ATTR_TTL);
}
@Test
void testUsernameHashAvailable() {
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
createAccount(account1);
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1)).join();
assertThat(accounts.usernameHashAvailable(USERNAME_HASH_1).join()).isFalse();
assertThat(accounts.usernameHashAvailable(Optional.empty(), USERNAME_HASH_1).join()).isFalse();
assertThat(accounts.usernameHashAvailable(Optional.of(UUID.randomUUID()), USERNAME_HASH_1).join()).isFalse();
assertThat(accounts.usernameHashAvailable(Optional.of(account1.getUuid()), USERNAME_HASH_1).join()).isTrue();
accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
assertThat(accounts.usernameHashAvailable(USERNAME_HASH_1).join()).isFalse();
assertThat(accounts.usernameHashAvailable(Optional.empty(), USERNAME_HASH_1).join()).isFalse();
assertThat(accounts.usernameHashAvailable(Optional.of(UUID.randomUUID()), USERNAME_HASH_1).join()).isFalse();
assertThat(accounts.usernameHashAvailable(Optional.of(account1.getUuid()), USERNAME_HASH_1).join()).isFalse();
}
@Test
void testConfirmReservedUsernameHashWrongAccountUuid() {
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
@@ -1133,7 +1115,7 @@ class AccountsTest {
assertThat(account1.getUsernameHash()).isEmpty();
// only account1 should be able to confirm the reserved hash
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
}
@@ -1148,7 +1130,7 @@ class AccountsTest {
for (int i = 0; i <= 2; i++) {
clock.pin(Instant.EPOCH.plus(Duration.ofDays(i)));
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)));
}
@@ -1159,9 +1141,9 @@ class AccountsTest {
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2)));
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join().get().getUuid()).isEqualTo(account2.getUuid());
}