Refactor account locks/deleted account manager
This commit is contained in:
parent
e6917d8427
commit
085c7a67c8
|
@ -169,11 +169,11 @@ import org.whispersystems.textsecuregcm.storage.AccountCleaner;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawler;
|
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawler;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerCache;
|
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerCache;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerListener;
|
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerListener;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountLockManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
|
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
|
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||||
|
@ -506,10 +506,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
config.getReportMessageConfiguration().getCounterTtl());
|
config.getReportMessageConfiguration().getCounterTtl());
|
||||||
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager,
|
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager,
|
||||||
messageDeletionAsyncExecutor);
|
messageDeletionAsyncExecutor);
|
||||||
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
|
AccountLockManager accountLockManager = new AccountLockManager(deletedAccountsLockDynamoDbClient, config.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
||||||
deletedAccountsLockDynamoDbClient, config.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
|
||||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||||
deletedAccountsManager, keys, messagesManager, profilesManager,
|
accountLockManager, deletedAccounts, keys, messagesManager, profilesManager,
|
||||||
pendingAccountsManager, secureStorageClient, secureBackupClient, secureValueRecovery2Client,
|
pendingAccountsManager, secureStorageClient, secureBackupClient, secureValueRecovery2Client,
|
||||||
clientPresenceManager,
|
clientPresenceManager,
|
||||||
experimentEnrollmentManager, registrationRecoveryPasswordsManager, clock);
|
experimentEnrollmentManager, registrationRecoveryPasswordsManager, clock);
|
||||||
|
@ -744,7 +743,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
new DirectoryV2Controller(directoryV2CredentialsGenerator),
|
new DirectoryV2Controller(directoryV2CredentialsGenerator),
|
||||||
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
|
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
|
||||||
ReceiptCredentialPresentation::new),
|
ReceiptCredentialPresentation::new),
|
||||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccountsManager,
|
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccounts,
|
||||||
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
|
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
|
||||||
messageDeliveryScheduler, reportSpamTokenProvider),
|
messageDeliveryScheduler, reportSpamTokenProvider),
|
||||||
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
||||||
|
|
|
@ -93,7 +93,7 @@ import org.whispersystems.textsecuregcm.spam.FilterSpam;
|
||||||
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
|
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||||
|
@ -117,7 +117,7 @@ public class MessageController {
|
||||||
private final MessageSender messageSender;
|
private final MessageSender messageSender;
|
||||||
private final ReceiptSender receiptSender;
|
private final ReceiptSender receiptSender;
|
||||||
private final AccountsManager accountsManager;
|
private final AccountsManager accountsManager;
|
||||||
private final DeletedAccountsManager deletedAccountsManager;
|
private final DeletedAccounts deletedAccounts;
|
||||||
private final MessagesManager messagesManager;
|
private final MessagesManager messagesManager;
|
||||||
private final PushNotificationManager pushNotificationManager;
|
private final PushNotificationManager pushNotificationManager;
|
||||||
private final ReportMessageManager reportMessageManager;
|
private final ReportMessageManager reportMessageManager;
|
||||||
|
@ -150,7 +150,7 @@ public class MessageController {
|
||||||
MessageSender messageSender,
|
MessageSender messageSender,
|
||||||
ReceiptSender receiptSender,
|
ReceiptSender receiptSender,
|
||||||
AccountsManager accountsManager,
|
AccountsManager accountsManager,
|
||||||
DeletedAccountsManager deletedAccountsManager,
|
DeletedAccounts deletedAccounts,
|
||||||
MessagesManager messagesManager,
|
MessagesManager messagesManager,
|
||||||
PushNotificationManager pushNotificationManager,
|
PushNotificationManager pushNotificationManager,
|
||||||
ReportMessageManager reportMessageManager,
|
ReportMessageManager reportMessageManager,
|
||||||
|
@ -161,7 +161,7 @@ public class MessageController {
|
||||||
this.messageSender = messageSender;
|
this.messageSender = messageSender;
|
||||||
this.receiptSender = receiptSender;
|
this.receiptSender = receiptSender;
|
||||||
this.accountsManager = accountsManager;
|
this.accountsManager = accountsManager;
|
||||||
this.deletedAccountsManager = deletedAccountsManager;
|
this.deletedAccounts = deletedAccounts;
|
||||||
this.messagesManager = messagesManager;
|
this.messagesManager = messagesManager;
|
||||||
this.pushNotificationManager = pushNotificationManager;
|
this.pushNotificationManager = pushNotificationManager;
|
||||||
this.reportMessageManager = reportMessageManager;
|
this.reportMessageManager = reportMessageManager;
|
||||||
|
@ -621,7 +621,7 @@ public class MessageController {
|
||||||
sourceAci = maybeAccount.map(Account::getUuid);
|
sourceAci = maybeAccount.map(Account::getUuid);
|
||||||
sourcePni = maybeAccount.map(Account::getPhoneNumberIdentifier);
|
sourcePni = maybeAccount.map(Account::getPhoneNumberIdentifier);
|
||||||
} else {
|
} else {
|
||||||
sourceAci = deletedAccountsManager.findDeletedAccountAci(source);
|
sourceAci = deletedAccounts.findUuid(source);
|
||||||
sourcePni = Optional.ofNullable(accountsManager.getPhoneNumberIdentifier(source));
|
sourcePni = Optional.ofNullable(accountsManager.getPhoneNumberIdentifier(source));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -631,7 +631,7 @@ public class MessageController {
|
||||||
|
|
||||||
if (sourceAccount.isEmpty()) {
|
if (sourceAccount.isEmpty()) {
|
||||||
logger.warn("Could not find source: {}", sourceAci.get());
|
logger.warn("Could not find source: {}", sourceAci.get());
|
||||||
sourceNumber = deletedAccountsManager.findDeletedAccountE164(sourceAci.get());
|
sourceNumber = deletedAccounts.findE164(sourceAci.get());
|
||||||
sourcePni = sourceNumber.map(accountsManager::getPhoneNumberIdentifier);
|
sourcePni = sourceNumber.map(accountsManager::getPhoneNumberIdentifier);
|
||||||
} else {
|
} else {
|
||||||
sourceNumber = sourceAccount.map(Account::getNumber);
|
sourceNumber = sourceAccount.map(Account::getNumber);
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
|
import com.amazonaws.services.dynamodbv2.AcquireLockOptions;
|
||||||
|
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
|
||||||
|
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClient;
|
||||||
|
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClientOptions;
|
||||||
|
import com.amazonaws.services.dynamodbv2.LockItem;
|
||||||
|
import com.amazonaws.services.dynamodbv2.ReleaseLockOptions;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class AccountLockManager {
|
||||||
|
|
||||||
|
private final AmazonDynamoDBLockClient lockClient;
|
||||||
|
|
||||||
|
public AccountLockManager(final AmazonDynamoDB lockDynamoDb, final String lockTableName) {
|
||||||
|
this(new AmazonDynamoDBLockClient(
|
||||||
|
AmazonDynamoDBLockClientOptions.builder(lockDynamoDb, lockTableName)
|
||||||
|
.withPartitionKeyName(DeletedAccounts.KEY_ACCOUNT_E164)
|
||||||
|
.withLeaseDuration(15L)
|
||||||
|
.withHeartbeatPeriod(2L)
|
||||||
|
.withTimeUnit(TimeUnit.SECONDS)
|
||||||
|
.withCreateHeartbeatBackgroundThread(true)
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
AccountLockManager(final AmazonDynamoDBLockClient lockClient) {
|
||||||
|
this.lockClient = lockClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquires a distributed, pessimistic lock for the accounts identified by the given phone numbers. By design, the
|
||||||
|
* accounts need not actually exist in order to acquire a lock; this allows lock acquisition for operations that span
|
||||||
|
* account lifecycle changes (like deleting an account or changing a phone number). The given task runs once locks for
|
||||||
|
* all given phone numbers have been acquired, and the locks are released as soon as the task completes by any means.
|
||||||
|
*
|
||||||
|
* @param e164s the phone numbers for which to acquire a distributed, pessimistic lock
|
||||||
|
* @param task the task to execute once locks have been acquired
|
||||||
|
*
|
||||||
|
* @throws InterruptedException if interrupted while acquiring a lock
|
||||||
|
*/
|
||||||
|
public void withLock(final List<String> e164s, final Runnable task) throws InterruptedException {
|
||||||
|
if (e164s.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("List of e164s to lock must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<LockItem> lockItems = new ArrayList<>(e164s.size());
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (final String e164 : e164s) {
|
||||||
|
lockItems.add(lockClient.acquireLock(AcquireLockOptions.builder(e164)
|
||||||
|
.withAcquireReleasedLocksConsistently(true)
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
task.run();
|
||||||
|
} finally {
|
||||||
|
lockItems.forEach(lockItem -> lockClient.releaseLock(ReleaseLockOptions.builder(lockItem)
|
||||||
|
.withBestEffort(true)
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -87,7 +87,8 @@ public class AccountsManager {
|
||||||
private final Accounts accounts;
|
private final Accounts accounts;
|
||||||
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
|
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
|
||||||
private final FaultTolerantRedisCluster cacheCluster;
|
private final FaultTolerantRedisCluster cacheCluster;
|
||||||
private final DeletedAccountsManager deletedAccountsManager;
|
private final AccountLockManager accountLockManager;
|
||||||
|
private final DeletedAccounts deletedAccounts;
|
||||||
private final Keys keys;
|
private final Keys keys;
|
||||||
private final MessagesManager messagesManager;
|
private final MessagesManager messagesManager;
|
||||||
private final ProfilesManager profilesManager;
|
private final ProfilesManager profilesManager;
|
||||||
|
@ -130,7 +131,8 @@ public class AccountsManager {
|
||||||
public AccountsManager(final Accounts accounts,
|
public AccountsManager(final Accounts accounts,
|
||||||
final PhoneNumberIdentifiers phoneNumberIdentifiers,
|
final PhoneNumberIdentifiers phoneNumberIdentifiers,
|
||||||
final FaultTolerantRedisCluster cacheCluster,
|
final FaultTolerantRedisCluster cacheCluster,
|
||||||
final DeletedAccountsManager deletedAccountsManager,
|
final AccountLockManager accountLockManager,
|
||||||
|
final DeletedAccounts deletedAccounts,
|
||||||
final Keys keys,
|
final Keys keys,
|
||||||
final MessagesManager messagesManager,
|
final MessagesManager messagesManager,
|
||||||
final ProfilesManager profilesManager,
|
final ProfilesManager profilesManager,
|
||||||
|
@ -145,7 +147,8 @@ public class AccountsManager {
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
|
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
|
||||||
this.cacheCluster = cacheCluster;
|
this.cacheCluster = cacheCluster;
|
||||||
this.deletedAccountsManager = deletedAccountsManager;
|
this.accountLockManager = accountLockManager;
|
||||||
|
this.deletedAccounts = deletedAccounts;
|
||||||
this.keys = keys;
|
this.keys = keys;
|
||||||
this.messagesManager = messagesManager;
|
this.messagesManager = messagesManager;
|
||||||
this.profilesManager = profilesManager;
|
this.profilesManager = profilesManager;
|
||||||
|
@ -168,7 +171,7 @@ public class AccountsManager {
|
||||||
try (Timer.Context ignored = createTimer.time()) {
|
try (Timer.Context ignored = createTimer.time()) {
|
||||||
final Account account = new Account();
|
final Account account = new Account();
|
||||||
|
|
||||||
deletedAccountsManager.lockAndTake(number, maybeRecentlyDeletedUuid -> {
|
accountLockManager.withLock(List.of(number), () -> {
|
||||||
Device device = new Device();
|
Device device = new Device();
|
||||||
device.setId(Device.MASTER_ID);
|
device.setId(Device.MASTER_ID);
|
||||||
device.setAuthTokenHash(SaltedTokenHash.generateFor(password));
|
device.setAuthTokenHash(SaltedTokenHash.generateFor(password));
|
||||||
|
@ -182,7 +185,10 @@ public class AccountsManager {
|
||||||
device.setUserAgent(signalAgent);
|
device.setUserAgent(signalAgent);
|
||||||
|
|
||||||
account.setNumber(number, phoneNumberIdentifiers.getPhoneNumberIdentifier(number));
|
account.setNumber(number, phoneNumberIdentifiers.getPhoneNumberIdentifier(number));
|
||||||
account.setUuid(maybeRecentlyDeletedUuid.orElseGet(UUID::randomUUID));
|
|
||||||
|
// Reuse the ACI from any recently-deleted account with this number to cover cases where somebody is
|
||||||
|
// re-registering.
|
||||||
|
account.setUuid(deletedAccounts.findUuid(number).orElseGet(UUID::randomUUID));
|
||||||
account.addDevice(device);
|
account.addDevice(device);
|
||||||
account.setRegistrationLockFromAttributes(accountAttributes);
|
account.setRegistrationLockFromAttributes(accountAttributes);
|
||||||
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
|
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
|
||||||
|
@ -236,6 +242,10 @@ public class AccountsManager {
|
||||||
|
|
||||||
accountAttributes.recoveryPassword().ifPresent(registrationRecoveryPassword ->
|
accountAttributes.recoveryPassword().ifPresent(registrationRecoveryPassword ->
|
||||||
registrationRecoveryPasswordsManager.storeForCurrentNumber(account.getNumber(), registrationRecoveryPassword));
|
registrationRecoveryPasswordsManager.storeForCurrentNumber(account.getNumber(), registrationRecoveryPassword));
|
||||||
|
|
||||||
|
// Clear any "recently deleted account" record for this number since, if it existed, we've used its old ACI for
|
||||||
|
// the newly-created account.
|
||||||
|
deletedAccounts.remove(number);
|
||||||
});
|
});
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
|
@ -243,7 +253,7 @@ public class AccountsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Account changeNumber(final Account account,
|
public Account changeNumber(final Account account,
|
||||||
final String number,
|
final String targetNumber,
|
||||||
@Nullable final byte[] pniIdentityKey,
|
@Nullable final byte[] pniIdentityKey,
|
||||||
@Nullable final Map<Long, SignedPreKey> pniSignedPreKeys,
|
@Nullable final Map<Long, SignedPreKey> pniSignedPreKeys,
|
||||||
@Nullable final Map<Long, SignedPreKey> pniPqLastResortPreKeys,
|
@Nullable final Map<Long, SignedPreKey> pniPqLastResortPreKeys,
|
||||||
|
@ -252,7 +262,7 @@ public class AccountsManager {
|
||||||
final String originalNumber = account.getNumber();
|
final String originalNumber = account.getNumber();
|
||||||
final UUID originalPhoneNumberIdentifier = account.getPhoneNumberIdentifier();
|
final UUID originalPhoneNumberIdentifier = account.getPhoneNumberIdentifier();
|
||||||
|
|
||||||
if (originalNumber.equals(number)) {
|
if (originalNumber.equals(targetNumber)) {
|
||||||
if (pniIdentityKey != null) {
|
if (pniIdentityKey != null) {
|
||||||
throw new IllegalArgumentException("change number must supply a changed phone number; otherwise use updatePniKeys");
|
throw new IllegalArgumentException("change number must supply a changed phone number; otherwise use updatePniKeys");
|
||||||
}
|
}
|
||||||
|
@ -263,28 +273,42 @@ public class AccountsManager {
|
||||||
|
|
||||||
final AtomicReference<Account> updatedAccount = new AtomicReference<>();
|
final AtomicReference<Account> updatedAccount = new AtomicReference<>();
|
||||||
|
|
||||||
deletedAccountsManager.lockAndPut(account.getNumber(), number, (originalAci, deletedAci) -> {
|
accountLockManager.withLock(List.of(account.getNumber(), targetNumber), () -> {
|
||||||
redisDelete(account);
|
redisDelete(account);
|
||||||
|
|
||||||
final Optional<Account> maybeExistingAccount = getByE164(number);
|
// There are three possible states for accounts associated with the target phone number:
|
||||||
final Optional<UUID> displacedUuid;
|
//
|
||||||
|
// 1. An account exists with the target number; the caller has proved ownership of the number, so delete the
|
||||||
|
// account with the target number. This will leave a "deleted account" record for the deleted account mapping
|
||||||
|
// the UUID of the deleted account to the target phone number. We'll then overwrite that so it points to the
|
||||||
|
// original number to facilitate switching back and forth between numbers.
|
||||||
|
// 2. No account with the target number exists, but one has recently been deleted. In that case, add a "deleted
|
||||||
|
// account" record that maps the ACI of the recently-deleted account to the now-abandoned original phone number
|
||||||
|
// of the account changing its number (which facilitates ACI consistency in cases that a party is switching
|
||||||
|
// back and forth between numbers).
|
||||||
|
// 3. No account with the target number exists at all, in which case no additional action is needed.
|
||||||
|
final Optional<UUID> recentlyDeletedAci = deletedAccounts.findUuid(targetNumber);
|
||||||
|
final Optional<Account> maybeExistingAccount = getByE164(targetNumber);
|
||||||
|
final Optional<UUID> maybeDisplacedUuid;
|
||||||
|
|
||||||
if (maybeExistingAccount.isPresent()) {
|
if (maybeExistingAccount.isPresent()) {
|
||||||
delete(maybeExistingAccount.get());
|
delete(maybeExistingAccount.get());
|
||||||
displacedUuid = maybeExistingAccount.map(Account::getUuid);
|
maybeDisplacedUuid = maybeExistingAccount.map(Account::getUuid);
|
||||||
} else {
|
} else {
|
||||||
displacedUuid = deletedAci;
|
maybeDisplacedUuid = recentlyDeletedAci;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maybeDisplacedUuid.ifPresent(displacedUuid -> deletedAccounts.put(displacedUuid, originalNumber));
|
||||||
|
|
||||||
final UUID uuid = account.getUuid();
|
final UUID uuid = account.getUuid();
|
||||||
final UUID phoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
|
final UUID phoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(targetNumber);
|
||||||
|
|
||||||
final Account numberChangedAccount;
|
final Account numberChangedAccount;
|
||||||
|
|
||||||
numberChangedAccount = updateWithRetries(
|
numberChangedAccount = updateWithRetries(
|
||||||
account,
|
account,
|
||||||
a -> { setPniKeys(account, pniIdentityKey, pniSignedPreKeys, pniRegistrationIds); return true; },
|
a -> { setPniKeys(account, pniIdentityKey, pniSignedPreKeys, pniRegistrationIds); return true; },
|
||||||
a -> accounts.changeNumber(a, number, phoneNumberIdentifier),
|
a -> accounts.changeNumber(a, targetNumber, phoneNumberIdentifier),
|
||||||
() -> accounts.getByAccountIdentifier(uuid).orElseThrow(),
|
() -> accounts.getByAccountIdentifier(uuid).orElseThrow(),
|
||||||
AccountChangeValidator.NUMBER_CHANGE_VALIDATOR);
|
AccountChangeValidator.NUMBER_CHANGE_VALIDATOR);
|
||||||
|
|
||||||
|
@ -301,8 +325,6 @@ public class AccountsManager {
|
||||||
Function.identity(),
|
Function.identity(),
|
||||||
pniPqLastResortPreKeys::get)));
|
pniPqLastResortPreKeys::get)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return displacedUuid;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedAccount.get();
|
return updatedAccount.get();
|
||||||
|
@ -675,10 +697,13 @@ public class AccountsManager {
|
||||||
|
|
||||||
public void delete(final Account account, final DeletionReason deletionReason) throws InterruptedException {
|
public void delete(final Account account, final DeletionReason deletionReason) throws InterruptedException {
|
||||||
try (final Timer.Context ignored = deleteTimer.time()) {
|
try (final Timer.Context ignored = deleteTimer.time()) {
|
||||||
deletedAccountsManager.lockAndPut(account.getNumber(), () -> {
|
accountLockManager.withLock(List.of(account.getNumber()), () -> {
|
||||||
|
final UUID accountIdentifier = account.getUuid();
|
||||||
|
final String e164 = account.getNumber();
|
||||||
|
|
||||||
delete(account);
|
delete(account);
|
||||||
|
|
||||||
return account.getUuid();
|
deletedAccounts.put(accountIdentifier, e164);
|
||||||
});
|
});
|
||||||
} catch (final RuntimeException | InterruptedException e) {
|
} catch (final RuntimeException | InterruptedException e) {
|
||||||
logger.warn("Failed to delete account", e);
|
logger.warn("Failed to delete account", e);
|
||||||
|
|
|
@ -18,7 +18,7 @@ import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
|
||||||
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
|
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
|
||||||
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;
|
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;
|
||||||
|
|
||||||
public class DeletedAccounts extends AbstractDynamoDbStore {
|
public class DeletedAccounts {
|
||||||
|
|
||||||
// e164, primary key
|
// e164, primary key
|
||||||
static final String KEY_ACCOUNT_E164 = "P";
|
static final String KEY_ACCOUNT_E164 = "P";
|
||||||
|
@ -29,19 +29,16 @@ public class DeletedAccounts extends AbstractDynamoDbStore {
|
||||||
|
|
||||||
static final Duration TIME_TO_LIVE = Duration.ofDays(30);
|
static final Duration TIME_TO_LIVE = Duration.ofDays(30);
|
||||||
|
|
||||||
// Note that this limit is imposed by DynamoDB itself; going above 100 will result in errors
|
private final DynamoDbClient dynamoDbClient;
|
||||||
static final int GET_BATCH_SIZE = 100;
|
|
||||||
|
|
||||||
private final String tableName;
|
private final String tableName;
|
||||||
|
|
||||||
public DeletedAccounts(final DynamoDbClient dynamoDb, final String tableName) {
|
public DeletedAccounts(final DynamoDbClient dynamoDbClient, final String tableName) {
|
||||||
|
this.dynamoDbClient = dynamoDbClient;
|
||||||
super(dynamoDb);
|
|
||||||
this.tableName = tableName;
|
this.tableName = tableName;
|
||||||
}
|
}
|
||||||
|
|
||||||
void put(UUID uuid, String e164) {
|
public void put(UUID uuid, String e164) {
|
||||||
db().putItem(PutItemRequest.builder()
|
dynamoDbClient.putItem(PutItemRequest.builder()
|
||||||
.tableName(tableName)
|
.tableName(tableName)
|
||||||
.item(Map.of(
|
.item(Map.of(
|
||||||
KEY_ACCOUNT_E164, AttributeValues.fromString(e164),
|
KEY_ACCOUNT_E164, AttributeValues.fromString(e164),
|
||||||
|
@ -50,8 +47,8 @@ public class DeletedAccounts extends AbstractDynamoDbStore {
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<UUID> findUuid(final String e164) {
|
public Optional<UUID> findUuid(final String e164) {
|
||||||
final GetItemResponse response = db().getItem(GetItemRequest.builder()
|
final GetItemResponse response = dynamoDbClient.getItem(GetItemRequest.builder()
|
||||||
.tableName(tableName)
|
.tableName(tableName)
|
||||||
.consistentRead(true)
|
.consistentRead(true)
|
||||||
.key(Map.of(KEY_ACCOUNT_E164, AttributeValues.fromString(e164)))
|
.key(Map.of(KEY_ACCOUNT_E164, AttributeValues.fromString(e164)))
|
||||||
|
@ -60,8 +57,8 @@ public class DeletedAccounts extends AbstractDynamoDbStore {
|
||||||
return Optional.ofNullable(AttributeValues.getUUID(response.item(), ATTR_ACCOUNT_UUID, null));
|
return Optional.ofNullable(AttributeValues.getUUID(response.item(), ATTR_ACCOUNT_UUID, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<String> findE164(final UUID uuid) {
|
public Optional<String> findE164(final UUID uuid) {
|
||||||
final QueryResponse response = db().query(QueryRequest.builder()
|
final QueryResponse response = dynamoDbClient.query(QueryRequest.builder()
|
||||||
.tableName(tableName)
|
.tableName(tableName)
|
||||||
.indexName(UUID_TO_E164_INDEX_NAME)
|
.indexName(UUID_TO_E164_INDEX_NAME)
|
||||||
.keyConditionExpression("#uuid = :uuid")
|
.keyConditionExpression("#uuid = :uuid")
|
||||||
|
@ -75,15 +72,14 @@ public class DeletedAccounts extends AbstractDynamoDbStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.count() > 1) {
|
if (response.count() > 1) {
|
||||||
throw new RuntimeException(
|
throw new RuntimeException("Impossible result: more than one phone number returned for UUID: " + uuid);
|
||||||
"Impossible result: more than one phone number returned for UUID: " + uuid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Optional.ofNullable(response.items().get(0).get(KEY_ACCOUNT_E164).s());
|
return Optional.ofNullable(response.items().get(0).get(KEY_ACCOUNT_E164).s());
|
||||||
}
|
}
|
||||||
|
|
||||||
void remove(final String e164) {
|
public void remove(final String e164) {
|
||||||
db().deleteItem(DeleteItemRequest.builder()
|
dynamoDbClient.deleteItem(DeleteItemRequest.builder()
|
||||||
.tableName(tableName)
|
.tableName(tableName)
|
||||||
.key(Map.of(KEY_ACCOUNT_E164, AttributeValues.fromString(e164)))
|
.key(Map.of(KEY_ACCOUNT_E164, AttributeValues.fromString(e164)))
|
||||||
.build());
|
.build());
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013-2021 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.storage;
|
|
||||||
|
|
||||||
import com.amazonaws.services.dynamodbv2.AcquireLockOptions;
|
|
||||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
|
|
||||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClient;
|
|
||||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClientOptions;
|
|
||||||
import com.amazonaws.services.dynamodbv2.LockItem;
|
|
||||||
import com.amazonaws.services.dynamodbv2.ReleaseLockOptions;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.function.BiFunction;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class DeletedAccountsManager {
|
|
||||||
|
|
||||||
private final DeletedAccounts deletedAccounts;
|
|
||||||
|
|
||||||
private final AmazonDynamoDBLockClient lockClient;
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(DeletedAccountsManager.class);
|
|
||||||
|
|
||||||
public DeletedAccountsManager(final DeletedAccounts deletedAccounts, final AmazonDynamoDB lockDynamoDb, final String lockTableName) {
|
|
||||||
this.deletedAccounts = deletedAccounts;
|
|
||||||
|
|
||||||
lockClient = new AmazonDynamoDBLockClient(
|
|
||||||
AmazonDynamoDBLockClientOptions.builder(lockDynamoDb, lockTableName)
|
|
||||||
.withPartitionKeyName(DeletedAccounts.KEY_ACCOUNT_E164)
|
|
||||||
.withLeaseDuration(15L)
|
|
||||||
.withHeartbeatPeriod(2L)
|
|
||||||
.withTimeUnit(TimeUnit.SECONDS)
|
|
||||||
.withCreateHeartbeatBackgroundThread(true)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Acquires a pessimistic lock for the given phone number and performs the given action, passing the UUID of the
|
|
||||||
* recently-deleted account (if any) that previously held the given number.
|
|
||||||
*
|
|
||||||
* @param e164 the phone number to lock and with which to perform an action
|
|
||||||
* @param consumer the action to take; accepts the UUID of the account that previously held the given e164, if any,
|
|
||||||
* as an argument
|
|
||||||
*
|
|
||||||
* @throws InterruptedException if interrupted while waiting to acquire a lock on the given phone number
|
|
||||||
*/
|
|
||||||
public void lockAndTake(final String e164, final Consumer<Optional<UUID>> consumer) throws InterruptedException {
|
|
||||||
withLock(List.of(e164), acis -> {
|
|
||||||
try {
|
|
||||||
consumer.accept(acis.get(0));
|
|
||||||
deletedAccounts.remove(e164);
|
|
||||||
} catch (final Exception e) {
|
|
||||||
log.warn("Consumer threw an exception while holding lock on a deleted account record", e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Acquires a pessimistic lock for the given phone number and performs an action that deletes an account, returning
|
|
||||||
* the UUID of the deleted account. The UUID of the deleted account will be stored in the list of recently-deleted
|
|
||||||
* e164-to-UUID mappings.
|
|
||||||
*
|
|
||||||
* @param e164 the phone number to lock and with which to perform an action
|
|
||||||
* @param supplier the deletion action to take on the account associated with the given number; must return the UUID
|
|
||||||
* of the deleted account
|
|
||||||
*
|
|
||||||
* @throws InterruptedException if interrupted while waiting to acquire a lock on the given phone number
|
|
||||||
*/
|
|
||||||
public void lockAndPut(final String e164, final Supplier<UUID> supplier) throws InterruptedException {
|
|
||||||
withLock(List.of(e164), ignored -> {
|
|
||||||
try {
|
|
||||||
deletedAccounts.put(supplier.get(), e164);
|
|
||||||
} catch (final Exception e) {
|
|
||||||
log.warn("Supplier threw an exception while holding lock on a deleted account record", e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Acquires a pessimistic lock for the given phone numbers and performs an action that may or may not delete an
|
|
||||||
* account associated with the target number. The UUID of the deleted account (if any) will be stored in the list of
|
|
||||||
* recently-deleted e164-to-UUID mappings.
|
|
||||||
*
|
|
||||||
* @param original the phone number of an existing account to lock and with which to perform an action
|
|
||||||
* @param target the phone number of an account that may or may not exist with which to perform an action
|
|
||||||
* @param function the action to take on the given phone numbers and ACIs, if known; the action may delete the account
|
|
||||||
* identified by the target number, in which case it must return the ACI of that account
|
|
||||||
* @throws InterruptedException if interrupted while waiting to acquire a lock on the given phone numbers
|
|
||||||
*/
|
|
||||||
public void lockAndPut(final String original, final String target,
|
|
||||||
final BiFunction<Optional<UUID>, Optional<UUID>, Optional<UUID>> function)
|
|
||||||
throws InterruptedException {
|
|
||||||
|
|
||||||
withLock(List.of(original, target), acis -> {
|
|
||||||
try {
|
|
||||||
function.apply(acis.get(0), acis.get(1)).ifPresent(aci -> deletedAccounts.put(aci, original));
|
|
||||||
} catch (final Exception e) {
|
|
||||||
log.warn("Supplier threw an exception while holding lock on a deleted account record", e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void withLock(final List<String> e164s, final Consumer<List<Optional<UUID>>> task)
|
|
||||||
throws InterruptedException {
|
|
||||||
final List<LockItem> lockItems = new ArrayList<>(e164s.size());
|
|
||||||
|
|
||||||
try {
|
|
||||||
final List<Optional<UUID>> previouslyDeletedUuids = new ArrayList<>(e164s.size());
|
|
||||||
for (final String e164 : e164s) {
|
|
||||||
lockItems.add(lockClient.acquireLock(AcquireLockOptions.builder(e164)
|
|
||||||
.withAcquireReleasedLocksConsistently(true)
|
|
||||||
.build()));
|
|
||||||
previouslyDeletedUuids.add(deletedAccounts.findUuid(e164));
|
|
||||||
}
|
|
||||||
|
|
||||||
task.accept(previouslyDeletedUuids);
|
|
||||||
} finally {
|
|
||||||
for (final LockItem lockItem : lockItems) {
|
|
||||||
lockClient.releaseLock(ReleaseLockOptions.builder(lockItem)
|
|
||||||
.withBestEffort(true)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<UUID> findDeletedAccountAci(final String e164) {
|
|
||||||
return deletedAccounts.findUuid(e164);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<String> findDeletedAccountE164(final UUID uuid) {
|
|
||||||
return deletedAccounts.findE164(uuid);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -37,10 +37,10 @@ import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
|
||||||
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||||
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountLockManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
|
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||||
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
||||||
|
@ -205,12 +205,10 @@ public class AssignUsernameCommand extends EnvironmentCommand<WhisperServerConfi
|
||||||
configuration.getReportMessageConfiguration().getCounterTtl());
|
configuration.getReportMessageConfiguration().getCounterTtl());
|
||||||
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache,
|
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache,
|
||||||
reportMessageManager, messageDeletionExecutor);
|
reportMessageManager, messageDeletionExecutor);
|
||||||
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
|
AccountLockManager accountLockManager = new AccountLockManager(deletedAccountsLockDynamoDbClient, configuration.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
||||||
deletedAccountsLockDynamoDbClient,
|
|
||||||
configuration.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
|
||||||
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
||||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||||
deletedAccountsManager, keys, messagesManager, profilesManager,
|
accountLockManager, deletedAccounts, keys, messagesManager, profilesManager,
|
||||||
pendingAccountsManager, secureStorageClient, secureBackupClient, secureValueRecovery2Client, clientPresenceManager,
|
pendingAccountsManager, secureStorageClient, secureBackupClient, secureValueRecovery2Client, clientPresenceManager,
|
||||||
experimentEnrollmentManager, registrationRecoveryPasswordsManager, Clock.systemUTC());
|
experimentEnrollmentManager, registrationRecoveryPasswordsManager, Clock.systemUTC());
|
||||||
|
|
||||||
|
|
|
@ -31,10 +31,10 @@ import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||||
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
|
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
|
||||||
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||||
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountLockManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
|
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||||
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
||||||
|
@ -64,7 +64,6 @@ record CommandDependencies(
|
||||||
ReportMessageManager reportMessageManager,
|
ReportMessageManager reportMessageManager,
|
||||||
MessagesCache messagesCache,
|
MessagesCache messagesCache,
|
||||||
MessagesManager messagesManager,
|
MessagesManager messagesManager,
|
||||||
DeletedAccountsManager deletedAccountsManager,
|
|
||||||
StoredVerificationCodeManager pendingAccountsManager,
|
StoredVerificationCodeManager pendingAccountsManager,
|
||||||
ClientPresenceManager clientPresenceManager,
|
ClientPresenceManager clientPresenceManager,
|
||||||
Keys keys,
|
Keys keys,
|
||||||
|
@ -190,12 +189,10 @@ record CommandDependencies(
|
||||||
configuration.getReportMessageConfiguration().getCounterTtl());
|
configuration.getReportMessageConfiguration().getCounterTtl());
|
||||||
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache,
|
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache,
|
||||||
reportMessageManager, messageDeletionExecutor);
|
reportMessageManager, messageDeletionExecutor);
|
||||||
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
|
AccountLockManager accountLockManager = new AccountLockManager(deletedAccountsLockDynamoDbClient, configuration.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
||||||
deletedAccountsLockDynamoDbClient,
|
|
||||||
configuration.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
|
||||||
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
||||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||||
deletedAccountsManager, keys, messagesManager, profilesManager,
|
accountLockManager, deletedAccounts, keys, messagesManager, profilesManager,
|
||||||
pendingAccountsManager, secureStorageClient, secureBackupClient, secureValueRecovery2Client, clientPresenceManager,
|
pendingAccountsManager, secureStorageClient, secureBackupClient, secureValueRecovery2Client, clientPresenceManager,
|
||||||
experimentEnrollmentManager, registrationRecoveryPasswordsManager, clock);
|
experimentEnrollmentManager, registrationRecoveryPasswordsManager, clock);
|
||||||
|
|
||||||
|
@ -205,7 +202,6 @@ record CommandDependencies(
|
||||||
reportMessageManager,
|
reportMessageManager,
|
||||||
messagesCache,
|
messagesCache,
|
||||||
messagesManager,
|
messagesManager,
|
||||||
deletedAccountsManager,
|
|
||||||
pendingAccountsManager,
|
pendingAccountsManager,
|
||||||
clientPresenceManager,
|
clientPresenceManager,
|
||||||
keys,
|
keys,
|
||||||
|
|
|
@ -97,7 +97,7 @@ import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
|
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||||
|
@ -141,7 +141,7 @@ class MessageControllerTest {
|
||||||
private static final MessageSender messageSender = mock(MessageSender.class);
|
private static final MessageSender messageSender = mock(MessageSender.class);
|
||||||
private static final ReceiptSender receiptSender = mock(ReceiptSender.class);
|
private static final ReceiptSender receiptSender = mock(ReceiptSender.class);
|
||||||
private static final AccountsManager accountsManager = mock(AccountsManager.class);
|
private static final AccountsManager accountsManager = mock(AccountsManager.class);
|
||||||
private static final DeletedAccountsManager deletedAccountsManager = mock(DeletedAccountsManager.class);
|
private static final DeletedAccounts deletedAccounts = mock(DeletedAccounts.class);
|
||||||
private static final MessagesManager messagesManager = mock(MessagesManager.class);
|
private static final MessagesManager messagesManager = mock(MessagesManager.class);
|
||||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||||
private static final RateLimiter rateLimiter = mock(RateLimiter.class);
|
private static final RateLimiter rateLimiter = mock(RateLimiter.class);
|
||||||
|
@ -159,7 +159,7 @@ class MessageControllerTest {
|
||||||
.addProvider(MultiRecipientMessageProvider.class)
|
.addProvider(MultiRecipientMessageProvider.class)
|
||||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||||
.addResource(
|
.addResource(
|
||||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccountsManager,
|
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccounts,
|
||||||
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
|
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
|
||||||
messageDeliveryScheduler, ReportSpamTokenProvider.noop()))
|
messageDeliveryScheduler, ReportSpamTokenProvider.noop()))
|
||||||
.build();
|
.build();
|
||||||
|
@ -214,7 +214,7 @@ class MessageControllerTest {
|
||||||
messageSender,
|
messageSender,
|
||||||
receiptSender,
|
receiptSender,
|
||||||
accountsManager,
|
accountsManager,
|
||||||
deletedAccountsManager,
|
deletedAccounts,
|
||||||
messagesManager,
|
messagesManager,
|
||||||
rateLimiters,
|
rateLimiters,
|
||||||
rateLimiter,
|
rateLimiter,
|
||||||
|
@ -634,7 +634,7 @@ class MessageControllerTest {
|
||||||
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
|
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
|
||||||
|
|
||||||
when(accountsManager.getByE164(senderNumber)).thenReturn(Optional.of(account));
|
when(accountsManager.getByE164(senderNumber)).thenReturn(Optional.of(account));
|
||||||
when(deletedAccountsManager.findDeletedAccountAci(senderNumber)).thenReturn(Optional.of(senderAci));
|
when(deletedAccounts.findUuid(senderNumber)).thenReturn(Optional.of(senderAci));
|
||||||
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
|
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
|
||||||
|
|
||||||
Response response =
|
Response response =
|
||||||
|
@ -649,7 +649,7 @@ class MessageControllerTest {
|
||||||
|
|
||||||
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
|
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
|
||||||
messageGuid, AuthHelper.VALID_UUID, Optional.empty(), userAgent);
|
messageGuid, AuthHelper.VALID_UUID, Optional.empty(), userAgent);
|
||||||
verify(deletedAccountsManager, never()).findDeletedAccountE164(any(UUID.class));
|
verify(deletedAccounts, never()).findE164(any(UUID.class));
|
||||||
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
|
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
|
||||||
|
|
||||||
when(accountsManager.getByE164(senderNumber)).thenReturn(Optional.empty());
|
when(accountsManager.getByE164(senderNumber)).thenReturn(Optional.empty());
|
||||||
|
@ -684,7 +684,7 @@ class MessageControllerTest {
|
||||||
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
|
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
|
||||||
|
|
||||||
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account));
|
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account));
|
||||||
when(deletedAccountsManager.findDeletedAccountE164(senderAci)).thenReturn(Optional.of(senderNumber));
|
when(deletedAccounts.findE164(senderAci)).thenReturn(Optional.of(senderNumber));
|
||||||
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
|
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
|
||||||
|
|
||||||
Response response =
|
Response response =
|
||||||
|
@ -699,7 +699,7 @@ class MessageControllerTest {
|
||||||
|
|
||||||
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
|
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
|
||||||
messageGuid, AuthHelper.VALID_UUID, Optional.empty(), userAgent);
|
messageGuid, AuthHelper.VALID_UUID, Optional.empty(), userAgent);
|
||||||
verify(deletedAccountsManager, never()).findDeletedAccountE164(any(UUID.class));
|
verify(deletedAccounts, never()).findE164(any(UUID.class));
|
||||||
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
|
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
|
||||||
|
|
||||||
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.empty());
|
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.empty());
|
||||||
|
@ -734,7 +734,7 @@ class MessageControllerTest {
|
||||||
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
|
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
|
||||||
|
|
||||||
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account));
|
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account));
|
||||||
when(deletedAccountsManager.findDeletedAccountE164(senderAci)).thenReturn(Optional.of(senderNumber));
|
when(deletedAccounts.findE164(senderAci)).thenReturn(Optional.of(senderNumber));
|
||||||
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
|
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
|
||||||
|
|
||||||
Entity<SpamReport> entity = Entity.entity(new SpamReport(new byte[3]), "application/json");
|
Entity<SpamReport> entity = Entity.entity(new SpamReport(new byte[3]), "application/json");
|
||||||
|
@ -753,7 +753,7 @@ class MessageControllerTest {
|
||||||
eq(AuthHelper.VALID_UUID),
|
eq(AuthHelper.VALID_UUID),
|
||||||
argThat(maybeBytes -> maybeBytes.map(bytes -> Arrays.equals(bytes, new byte[3])).orElse(false)),
|
argThat(maybeBytes -> maybeBytes.map(bytes -> Arrays.equals(bytes, new byte[3])).orElse(false)),
|
||||||
any());
|
any());
|
||||||
verify(deletedAccountsManager, never()).findDeletedAccountE164(any(UUID.class));
|
verify(deletedAccounts, never()).findE164(any(UUID.class));
|
||||||
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
|
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
|
||||||
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.empty());
|
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
@ -792,7 +792,7 @@ class MessageControllerTest {
|
||||||
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
|
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
|
||||||
|
|
||||||
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account));
|
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account));
|
||||||
when(deletedAccountsManager.findDeletedAccountE164(senderAci)).thenReturn(Optional.of(senderNumber));
|
when(deletedAccounts.findE164(senderAci)).thenReturn(Optional.of(senderNumber));
|
||||||
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
|
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
|
||||||
|
|
||||||
Response response =
|
Response response =
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClient;
|
||||||
|
import com.amazonaws.services.dynamodbv2.ReleaseLockOptions;
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class AccountLockManagerTest {
|
||||||
|
|
||||||
|
private AmazonDynamoDBLockClient lockClient;
|
||||||
|
|
||||||
|
private AccountLockManager accountLockManager;
|
||||||
|
|
||||||
|
private static final String FIRST_NUMBER = PhoneNumberUtil.getInstance().format(
|
||||||
|
PhoneNumberUtil.getInstance().getExampleNumber("US"), PhoneNumberUtil.PhoneNumberFormat.E164);
|
||||||
|
|
||||||
|
private static final String SECOND_NUMBER = PhoneNumberUtil.getInstance().format(
|
||||||
|
PhoneNumberUtil.getInstance().getExampleNumber("JP"), PhoneNumberUtil.PhoneNumberFormat.E164);
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
lockClient = mock(AmazonDynamoDBLockClient.class);
|
||||||
|
|
||||||
|
accountLockManager = new AccountLockManager(lockClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withLock() throws InterruptedException {
|
||||||
|
accountLockManager.withLock(List.of(FIRST_NUMBER, SECOND_NUMBER), () -> {});
|
||||||
|
|
||||||
|
verify(lockClient, times(2)).acquireLock(any());
|
||||||
|
verify(lockClient, times(2)).releaseLock(any(ReleaseLockOptions.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withLockTaskThrowsException() throws InterruptedException {
|
||||||
|
assertThrows(RuntimeException.class, () -> accountLockManager.withLock(List.of(FIRST_NUMBER, SECOND_NUMBER), () -> {
|
||||||
|
throw new RuntimeException();
|
||||||
|
}));
|
||||||
|
|
||||||
|
verify(lockClient, times(2)).acquireLock(any());
|
||||||
|
verify(lockClient, times(2)).releaseLock(any(ReleaseLockOptions.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withLockEmptyList() {
|
||||||
|
final Runnable task = mock(Runnable.class);
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> accountLockManager.withLock(Collections.emptyList(), () -> {}));
|
||||||
|
verify(task, never()).run();
|
||||||
|
}
|
||||||
|
}
|
|
@ -80,8 +80,7 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||||
deletedAccounts = new DeletedAccounts(DYNAMO_DB_EXTENSION.getDynamoDbClient(),
|
deletedAccounts = new DeletedAccounts(DYNAMO_DB_EXTENSION.getDynamoDbClient(),
|
||||||
Tables.DELETED_ACCOUNTS.tableName());
|
Tables.DELETED_ACCOUNTS.tableName());
|
||||||
|
|
||||||
final DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
|
final AccountLockManager accountLockManager = new AccountLockManager(DYNAMO_DB_EXTENSION.getLegacyDynamoClient(),
|
||||||
DYNAMO_DB_EXTENSION.getLegacyDynamoClient(),
|
|
||||||
Tables.DELETED_ACCOUNTS_LOCK.tableName());
|
Tables.DELETED_ACCOUNTS_LOCK.tableName());
|
||||||
|
|
||||||
final SecureStorageClient secureStorageClient = mock(SecureStorageClient.class);
|
final SecureStorageClient secureStorageClient = mock(SecureStorageClient.class);
|
||||||
|
@ -102,8 +101,7 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||||
accounts,
|
accounts,
|
||||||
phoneNumberIdentifiers,
|
phoneNumberIdentifiers,
|
||||||
CACHE_CLUSTER_EXTENSION.getRedisCluster(),
|
CACHE_CLUSTER_EXTENSION.getRedisCluster(),
|
||||||
deletedAccountsManager,
|
accountLockManager, deletedAccounts, mock(Keys.class),
|
||||||
mock(Keys.class),
|
|
||||||
mock(MessagesManager.class),
|
mock(MessagesManager.class),
|
||||||
mock(ProfilesManager.class),
|
mock(ProfilesManager.class),
|
||||||
mock(StoredVerificationCodeManager.class),
|
mock(StoredVerificationCodeManager.class),
|
||||||
|
|
|
@ -92,13 +92,17 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
commands = mock(RedisAdvancedClusterCommands.class);
|
commands = mock(RedisAdvancedClusterCommands.class);
|
||||||
|
|
||||||
final DeletedAccountsManager deletedAccountsManager = mock(DeletedAccountsManager.class);
|
final AccountLockManager accountLockManager = mock(AccountLockManager.class);
|
||||||
|
|
||||||
doAnswer(invocation -> {
|
doAnswer(invocation -> {
|
||||||
//noinspection unchecked
|
final Runnable task = invocation.getArgument(1);
|
||||||
invocation.getArgument(1, Consumer.class).accept(Optional.empty());
|
task.run();
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}).when(deletedAccountsManager).lockAndTake(anyString(), any());
|
}).when(accountLockManager).withLock(any(), any());
|
||||||
|
|
||||||
|
final DeletedAccounts deletedAccounts = mock(DeletedAccounts.class);
|
||||||
|
when(deletedAccounts.findUuid(any())).thenReturn(Optional.empty());
|
||||||
|
|
||||||
final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
|
final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
|
||||||
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(anyString()))
|
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(anyString()))
|
||||||
|
@ -108,8 +112,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||||
accounts,
|
accounts,
|
||||||
phoneNumberIdentifiers,
|
phoneNumberIdentifiers,
|
||||||
RedisClusterHelper.builder().stringCommands(commands).build(),
|
RedisClusterHelper.builder().stringCommands(commands).build(),
|
||||||
deletedAccountsManager,
|
accountLockManager, deletedAccounts, mock(Keys.class),
|
||||||
mock(Keys.class),
|
|
||||||
mock(MessagesManager.class),
|
mock(MessagesManager.class),
|
||||||
mock(ProfilesManager.class),
|
mock(ProfilesManager.class),
|
||||||
mock(StoredVerificationCodeManager.class),
|
mock(StoredVerificationCodeManager.class),
|
||||||
|
|
|
@ -36,7 +36,6 @@ import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.function.BiFunction;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
@ -71,7 +70,7 @@ class AccountsManagerTest {
|
||||||
private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2);
|
private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2);
|
||||||
|
|
||||||
private Accounts accounts;
|
private Accounts accounts;
|
||||||
private DeletedAccountsManager deletedAccountsManager;
|
private DeletedAccounts deletedAccounts;
|
||||||
private Keys keys;
|
private Keys keys;
|
||||||
private MessagesManager messagesManager;
|
private MessagesManager messagesManager;
|
||||||
private ProfilesManager profilesManager;
|
private ProfilesManager profilesManager;
|
||||||
|
@ -94,7 +93,7 @@ class AccountsManagerTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setup() throws InterruptedException {
|
void setup() throws InterruptedException {
|
||||||
accounts = mock(Accounts.class);
|
accounts = mock(Accounts.class);
|
||||||
deletedAccountsManager = mock(DeletedAccountsManager.class);
|
deletedAccounts = mock(DeletedAccounts.class);
|
||||||
keys = mock(Keys.class);
|
keys = mock(Keys.class);
|
||||||
messagesManager = mock(MessagesManager.class);
|
messagesManager = mock(MessagesManager.class);
|
||||||
profilesManager = mock(ProfilesManager.class);
|
profilesManager = mock(ProfilesManager.class);
|
||||||
|
@ -113,11 +112,7 @@ class AccountsManagerTest {
|
||||||
return null;
|
return null;
|
||||||
}).when(accounts).changeNumber(any(), anyString(), any());
|
}).when(accounts).changeNumber(any(), anyString(), any());
|
||||||
|
|
||||||
doAnswer(invocation -> {
|
when(deletedAccounts.findUuid(anyString())).thenReturn(Optional.empty());
|
||||||
//noinspection unchecked
|
|
||||||
invocation.getArgument(1, Consumer.class).accept(Optional.empty());
|
|
||||||
return null;
|
|
||||||
}).when(deletedAccountsManager).lockAndTake(anyString(), any());
|
|
||||||
|
|
||||||
final SecureStorageClient storageClient = mock(SecureStorageClient.class);
|
final SecureStorageClient storageClient = mock(SecureStorageClient.class);
|
||||||
when(storageClient.deleteStoredData(any())).thenReturn(CompletableFuture.completedFuture(null));
|
when(storageClient.deleteStoredData(any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||||
|
@ -147,11 +142,21 @@ class AccountsManagerTest {
|
||||||
when(enrollmentManager.isEnrolled(any(UUID.class), eq(AccountsManager.USERNAME_EXPERIMENT_NAME))).thenReturn(true);
|
when(enrollmentManager.isEnrolled(any(UUID.class), eq(AccountsManager.USERNAME_EXPERIMENT_NAME))).thenReturn(true);
|
||||||
when(accounts.usernameHashAvailable(any())).thenReturn(true);
|
when(accounts.usernameHashAvailable(any())).thenReturn(true);
|
||||||
|
|
||||||
|
final AccountLockManager accountLockManager = mock(AccountLockManager.class);
|
||||||
|
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
final Runnable task = invocation.getArgument(1);
|
||||||
|
task.run();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}).when(accountLockManager).withLock(any(), any());
|
||||||
|
|
||||||
accountsManager = new AccountsManager(
|
accountsManager = new AccountsManager(
|
||||||
accounts,
|
accounts,
|
||||||
phoneNumberIdentifiers,
|
phoneNumberIdentifiers,
|
||||||
RedisClusterHelper.builder().stringCommands(commands).build(),
|
RedisClusterHelper.builder().stringCommands(commands).build(),
|
||||||
deletedAccountsManager,
|
accountLockManager,
|
||||||
|
deletedAccounts,
|
||||||
keys,
|
keys,
|
||||||
messagesManager,
|
messagesManager,
|
||||||
profilesManager,
|
profilesManager,
|
||||||
|
@ -571,12 +576,7 @@ class AccountsManagerTest {
|
||||||
void testCreateAccountRecentlyDeleted() throws InterruptedException {
|
void testCreateAccountRecentlyDeleted() throws InterruptedException {
|
||||||
final UUID recentlyDeletedUuid = UUID.randomUUID();
|
final UUID recentlyDeletedUuid = UUID.randomUUID();
|
||||||
|
|
||||||
doAnswer(invocation -> {
|
when(deletedAccounts.findUuid(anyString())).thenReturn(Optional.of(recentlyDeletedUuid));
|
||||||
//noinspection unchecked
|
|
||||||
invocation.getArgument(1, Consumer.class).accept(Optional.of(recentlyDeletedUuid));
|
|
||||||
return null;
|
|
||||||
}).when(deletedAccountsManager).lockAndTake(anyString(), any());
|
|
||||||
|
|
||||||
when(accounts.create(any())).thenReturn(true);
|
when(accounts.create(any())).thenReturn(true);
|
||||||
|
|
||||||
final String e164 = "+18005550123";
|
final String e164 = "+18005550123";
|
||||||
|
@ -634,9 +634,6 @@ class AccountsManagerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testChangePhoneNumber() throws InterruptedException, MismatchedDevicesException {
|
void testChangePhoneNumber() throws InterruptedException, MismatchedDevicesException {
|
||||||
doAnswer(invocation -> invocation.getArgument(2, BiFunction.class).apply(Optional.empty(), Optional.empty()))
|
|
||||||
.when(deletedAccountsManager).lockAndPut(anyString(), anyString(), any());
|
|
||||||
|
|
||||||
final String originalNumber = "+14152222222";
|
final String originalNumber = "+14152222222";
|
||||||
final String targetNumber = "+14153333333";
|
final String targetNumber = "+14153333333";
|
||||||
final UUID uuid = UUID.randomUUID();
|
final UUID uuid = UUID.randomUUID();
|
||||||
|
@ -661,7 +658,7 @@ class AccountsManagerTest {
|
||||||
account = accountsManager.changeNumber(account, number, null, null, null, null);
|
account = accountsManager.changeNumber(account, number, null, null, null, null);
|
||||||
|
|
||||||
assertEquals(number, account.getNumber());
|
assertEquals(number, account.getNumber());
|
||||||
verify(deletedAccountsManager, never()).lockAndPut(anyString(), anyString(), any());
|
verify(deletedAccounts, never()).put(any(), any());
|
||||||
verify(keys, never()).delete(any());
|
verify(keys, never()).delete(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -676,15 +673,12 @@ class AccountsManagerTest {
|
||||||
"AccountsManager should not allow use of changeNumber with new PNI keys but without changing number");
|
"AccountsManager should not allow use of changeNumber with new PNI keys but without changing number");
|
||||||
|
|
||||||
verify(accounts, never()).update(any());
|
verify(accounts, never()).update(any());
|
||||||
verifyNoInteractions(deletedAccountsManager);
|
verifyNoInteractions(deletedAccounts);
|
||||||
verifyNoInteractions(keys);
|
verifyNoInteractions(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testChangePhoneNumberExistingAccount() throws InterruptedException, MismatchedDevicesException {
|
void testChangePhoneNumberExistingAccount() throws InterruptedException, MismatchedDevicesException {
|
||||||
doAnswer(invocation -> invocation.getArgument(2, BiFunction.class).apply(Optional.empty(), Optional.empty()))
|
|
||||||
.when(deletedAccountsManager).lockAndPut(anyString(), anyString(), any());
|
|
||||||
|
|
||||||
final String originalNumber = "+14152222222";
|
final String originalNumber = "+14152222222";
|
||||||
final String targetNumber = "+14153333333";
|
final String targetNumber = "+14153333333";
|
||||||
final UUID existingAccountUuid = UUID.randomUUID();
|
final UUID existingAccountUuid = UUID.randomUUID();
|
||||||
|
@ -712,9 +706,6 @@ class AccountsManagerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testChangePhoneNumberWithPqKeysExistingAccount() throws InterruptedException, MismatchedDevicesException {
|
void testChangePhoneNumberWithPqKeysExistingAccount() throws InterruptedException, MismatchedDevicesException {
|
||||||
doAnswer(invocation -> invocation.getArgument(2, BiFunction.class).apply(Optional.empty(), Optional.empty()))
|
|
||||||
.when(deletedAccountsManager).lockAndPut(anyString(), anyString(), any());
|
|
||||||
|
|
||||||
final String originalNumber = "+14152222222";
|
final String originalNumber = "+14152222222";
|
||||||
final String targetNumber = "+14153333333";
|
final String targetNumber = "+14153333333";
|
||||||
final UUID existingAccountUuid = UUID.randomUUID();
|
final UUID existingAccountUuid = UUID.randomUUID();
|
||||||
|
@ -799,7 +790,7 @@ class AccountsManagerTest {
|
||||||
updatedAccount.getDevices().stream().collect(Collectors.toMap(Device::getId, d -> d.getPhoneNumberIdentityRegistrationId().getAsInt())));
|
updatedAccount.getDevices().stream().collect(Collectors.toMap(Device::getId, d -> d.getPhoneNumberIdentityRegistrationId().getAsInt())));
|
||||||
|
|
||||||
verify(accounts).update(any());
|
verify(accounts).update(any());
|
||||||
verifyNoInteractions(deletedAccountsManager);
|
verifyNoInteractions(deletedAccounts);
|
||||||
|
|
||||||
verify(keys).delete(oldPni);
|
verify(keys).delete(oldPni);
|
||||||
}
|
}
|
||||||
|
@ -846,7 +837,7 @@ class AccountsManagerTest {
|
||||||
updatedAccount.getDevices().stream().collect(Collectors.toMap(Device::getId, d -> d.getPhoneNumberIdentityRegistrationId().getAsInt())));
|
updatedAccount.getDevices().stream().collect(Collectors.toMap(Device::getId, d -> d.getPhoneNumberIdentityRegistrationId().getAsInt())));
|
||||||
|
|
||||||
verify(accounts).update(any());
|
verify(accounts).update(any());
|
||||||
verifyNoInteractions(deletedAccountsManager);
|
verifyNoInteractions(deletedAccounts);
|
||||||
|
|
||||||
verify(keys).delete(oldPni);
|
verify(keys).delete(oldPni);
|
||||||
|
|
||||||
|
|
|
@ -30,12 +30,10 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||||
|
@ -94,13 +92,17 @@ class AccountsManagerUsernameIntegrationTest {
|
||||||
Tables.USERNAMES.tableName(),
|
Tables.USERNAMES.tableName(),
|
||||||
SCAN_PAGE_SIZE));
|
SCAN_PAGE_SIZE));
|
||||||
|
|
||||||
final DeletedAccountsManager deletedAccountsManager = mock(DeletedAccountsManager.class);
|
final AccountLockManager accountLockManager = mock(AccountLockManager.class);
|
||||||
doAnswer((final InvocationOnMock invocationOnMock) -> {
|
|
||||||
@SuppressWarnings("unchecked")
|
doAnswer(invocation -> {
|
||||||
Consumer<Optional<UUID>> consumer = invocationOnMock.getArgument(1, Consumer.class);
|
final Runnable task = invocation.getArgument(1);
|
||||||
consumer.accept(Optional.empty());
|
task.run();
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}).when(deletedAccountsManager).lockAndTake(any(), any());
|
}).when(accountLockManager).withLock(any(), any());
|
||||||
|
|
||||||
|
final DeletedAccounts deletedAccounts = mock(DeletedAccounts.class);
|
||||||
|
when(deletedAccounts.findUuid(any())).thenReturn(Optional.empty());
|
||||||
|
|
||||||
final PhoneNumberIdentifiers phoneNumberIdentifiers =
|
final PhoneNumberIdentifiers phoneNumberIdentifiers =
|
||||||
new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbClient(), Tables.PNI.tableName());
|
new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbClient(), Tables.PNI.tableName());
|
||||||
|
@ -112,7 +114,8 @@ class AccountsManagerUsernameIntegrationTest {
|
||||||
accounts,
|
accounts,
|
||||||
phoneNumberIdentifiers,
|
phoneNumberIdentifiers,
|
||||||
CACHE_CLUSTER_EXTENSION.getRedisCluster(),
|
CACHE_CLUSTER_EXTENSION.getRedisCluster(),
|
||||||
deletedAccountsManager,
|
accountLockManager,
|
||||||
|
deletedAccounts,
|
||||||
mock(Keys.class),
|
mock(Keys.class),
|
||||||
mock(MessagesManager.class),
|
mock(MessagesManager.class),
|
||||||
mock(ProfilesManager.class),
|
mock(ProfilesManager.class),
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables;
|
|
||||||
|
|
||||||
class DeletedAccountsManagerTest {
|
|
||||||
|
|
||||||
@RegisterExtension
|
|
||||||
static final DynamoDbExtension DYNAMO_DB_EXTENSION =
|
|
||||||
new DynamoDbExtension(Tables.DELETED_ACCOUNTS, Tables.DELETED_ACCOUNTS_LOCK);
|
|
||||||
|
|
||||||
private DeletedAccounts deletedAccounts;
|
|
||||||
private DeletedAccountsManager deletedAccountsManager;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
deletedAccounts = new DeletedAccounts(DYNAMO_DB_EXTENSION.getDynamoDbClient(),
|
|
||||||
Tables.DELETED_ACCOUNTS.tableName());
|
|
||||||
|
|
||||||
deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
|
|
||||||
DYNAMO_DB_EXTENSION.getLegacyDynamoClient(),
|
|
||||||
Tables.DELETED_ACCOUNTS_LOCK.tableName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testLockAndTake() throws InterruptedException {
|
|
||||||
final UUID uuid = UUID.randomUUID();
|
|
||||||
final String e164 = "+18005551234";
|
|
||||||
|
|
||||||
deletedAccounts.put(uuid, e164);
|
|
||||||
deletedAccountsManager.lockAndTake(e164, maybeUuid -> assertEquals(Optional.of(uuid), maybeUuid));
|
|
||||||
assertEquals(Optional.empty(), deletedAccounts.findUuid(e164));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testLockAndTakeWithException() {
|
|
||||||
final UUID uuid = UUID.randomUUID();
|
|
||||||
final String e164 = "+18005551234";
|
|
||||||
|
|
||||||
deletedAccounts.put(uuid, e164);
|
|
||||||
|
|
||||||
assertThrows(RuntimeException.class, () -> deletedAccountsManager.lockAndTake(e164, maybeUuid -> {
|
|
||||||
assertEquals(Optional.of(uuid), maybeUuid);
|
|
||||||
throw new RuntimeException("OH NO");
|
|
||||||
}));
|
|
||||||
|
|
||||||
assertEquals(Optional.of(uuid), deletedAccounts.findUuid(e164));
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue