mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 05:38:04 +01:00
Add basic support for phone number identifiers
This commit is contained in:
@@ -171,6 +171,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private AccountsDynamoDbConfiguration accountsDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration phoneNumberIdentifiersDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
@@ -436,6 +441,10 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return accountsDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getPhoneNumberIdentifiersDynamoDbConfiguration() {
|
||||
return phoneNumberIdentifiersDynamoDb;
|
||||
}
|
||||
|
||||
public DeletedAccountsDynamoDbConfiguration getDeletedAccountsDynamoDbConfiguration() {
|
||||
return deletedAccountsDynamoDb;
|
||||
}
|
||||
|
||||
@@ -186,6 +186,7 @@ import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.NonNormalizedAccountCrawlerListener;
|
||||
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||
import org.whispersystems.textsecuregcm.storage.Profiles;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
@@ -330,6 +331,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
DynamoDbClient accountsDynamoDbClient = DynamoDbFromConfig.client(config.getAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
|
||||
DynamoDbFromConfig.client(config.getPhoneNumberIdentifiersDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(config.getDeletedAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
@@ -365,7 +370,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
Accounts accounts = new Accounts(accountsDynamoDbClient,
|
||||
config.getAccountsDynamoDbConfiguration().getTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getScanPageSize());
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
|
||||
config.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
@@ -465,9 +473,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, pushLatencyManager, reportMessageManager);
|
||||
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
|
||||
deletedAccountsLockDynamoDbClient, config.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, deletedAccountsManager,
|
||||
directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager, pendingAccountsManager,
|
||||
secureStorageClient, secureBackupClient, clientPresenceManager, clock);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
|
||||
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(accountsManager, messagesManager);
|
||||
DispatchManager dispatchManager = new DispatchManager(pubSubClientFactory, Optional.of(deadLetterHandler));
|
||||
|
||||
@@ -76,7 +76,7 @@ public class AuthEnablementRefreshRequirementProvider implements WebsocketRefres
|
||||
@SuppressWarnings("unchecked") final Map<Long, Boolean> initialDevicesEnabled =
|
||||
(Map<Long, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
|
||||
|
||||
return accountsManager.get((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
|
||||
return accountsManager.getByAccountIdentifier((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
|
||||
final Set<Long> deviceIdsToDisplace;
|
||||
final Map<Long, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account);
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ public class BaseAccountAuthenticator {
|
||||
deviceId = identifierAndDeviceId.second();
|
||||
}
|
||||
|
||||
Optional<Account> account = accountsManager.get(accountUuid);
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(accountUuid);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
failureReason = "noSuchAccount";
|
||||
|
||||
@@ -8,6 +8,9 @@ public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
@NotNull
|
||||
private String phoneNumberTableName;
|
||||
|
||||
@NotNull
|
||||
private String phoneNumberIdentifierTableName;
|
||||
|
||||
private int scanPageSize = 100;
|
||||
|
||||
@JsonProperty
|
||||
@@ -15,6 +18,11 @@ public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
return phoneNumberTableName;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public String getPhoneNumberIdentifierTableName() {
|
||||
return phoneNumberIdentifierTableName;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public int getScanPageSize() {
|
||||
return scanPageSize;
|
||||
|
||||
@@ -353,7 +353,7 @@ public class AccountController {
|
||||
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
|
||||
.ifPresent(smsSender::reportVerificationSucceeded);
|
||||
|
||||
Optional<Account> existingAccount = accounts.get(number);
|
||||
Optional<Account> existingAccount = accounts.getByE164(number);
|
||||
|
||||
if (existingAccount.isPresent()) {
|
||||
verifyRegistrationLock(existingAccount.get(), accountAttributes.getRegistrationLock());
|
||||
@@ -412,7 +412,7 @@ public class AccountController {
|
||||
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
|
||||
.ifPresent(smsSender::reportVerificationSucceeded);
|
||||
|
||||
final Optional<Account> existingAccount = accounts.get(request.getNumber());
|
||||
final Optional<Account> existingAccount = accounts.getByE164(request.getNumber());
|
||||
|
||||
if (existingAccount.isPresent()) {
|
||||
verifyRegistrationLock(existingAccount.get(), request.getRegistrationLock());
|
||||
|
||||
@@ -168,7 +168,7 @@ public class DeviceController {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
Optional<Account> account = accounts.get(number);
|
||||
Optional<Account> account = accounts.getByE164(number);
|
||||
|
||||
if (!account.isPresent()) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
|
||||
@@ -159,7 +159,7 @@ public class DonationController {
|
||||
|
||||
@Override
|
||||
public boolean block() {
|
||||
final Optional<Account> optionalAccount = accountsManager.get(auth.getAccount().getUuid());
|
||||
final Optional<Account> optionalAccount = accountsManager.getByAccountIdentifier(auth.getAccount().getUuid());
|
||||
optionalAccount.ifPresent(account -> {
|
||||
accountsManager.update(account, a -> {
|
||||
a.addBadge(clock, new AccountBadge(badgeId, receiptExpiration, request.isVisible()));
|
||||
|
||||
@@ -129,7 +129,7 @@ public class KeysController {
|
||||
|
||||
final Optional<Account> account = auth.map(AuthenticatedAccount::getAccount);
|
||||
|
||||
Optional<Account> target = accounts.get(targetUuid);
|
||||
Optional<Account> target = accounts.getByAccountIdentifier(targetUuid);
|
||||
OptionalAccess.verify(account, accessKey, target, deviceId);
|
||||
|
||||
assert (target.isPresent());
|
||||
|
||||
@@ -215,7 +215,7 @@ public class MessageController {
|
||||
Optional<Account> destination;
|
||||
|
||||
if (!isSyncMessage) {
|
||||
destination = accountsManager.get(destinationUuid);
|
||||
destination = accountsManager.getByAccountIdentifier(destinationUuid);
|
||||
} else {
|
||||
destination = source.map(AuthenticatedAccount::getAccount);
|
||||
}
|
||||
@@ -311,7 +311,7 @@ public class MessageController {
|
||||
.map(Recipient::getUuid)
|
||||
.distinct()
|
||||
.collect(Collectors.toUnmodifiableMap(Function.identity(), uuid -> {
|
||||
Optional<Account> account = accountsManager.get(uuid);
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
|
||||
if (account.isEmpty()) {
|
||||
throw new WebApplicationException(Status.NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ public class ProfileController {
|
||||
isSelf = uuid.equals(authedUuid);
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(uuid);
|
||||
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid);
|
||||
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
|
||||
|
||||
assert(accountProfile.isPresent());
|
||||
@@ -316,7 +316,7 @@ public class ProfileController {
|
||||
|
||||
final boolean isSelf = auth.getAccount().getUuid().equals(uuid.get());
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(uuid.get());
|
||||
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid.get());
|
||||
|
||||
if (accountProfile.isEmpty()) {
|
||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||
@@ -398,7 +398,7 @@ public class ProfileController {
|
||||
isSelf = authedUuid.equals(identifier);
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(identifier);
|
||||
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(identifier);
|
||||
OptionalAccess.verify(auth.map(AuthenticatedAccount::getAccount), accessKey, accountProfile);
|
||||
|
||||
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());
|
||||
|
||||
@@ -125,7 +125,7 @@ public class APNSender implements Managed {
|
||||
private void handleUnregisteredUser(String registrationId, UUID uuid, long deviceId) {
|
||||
// logger.info("Got APN Unregistered: " + number + "," + deviceId);
|
||||
|
||||
Optional<Account> account = accountsManager.get(uuid);
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
logger.info("No account found: {}", uuid);
|
||||
|
||||
@@ -99,7 +99,7 @@ public class ApnFallbackManager implements Managed {
|
||||
|
||||
final Optional<Account> maybeAccount = separated.map(Pair::first)
|
||||
.map(UUID::fromString)
|
||||
.flatMap(accountsManager::get);
|
||||
.flatMap(accountsManager::getByAccountIdentifier);
|
||||
|
||||
final Optional<Device> maybeDevice = separated.map(Pair::second)
|
||||
.flatMap(deviceId -> maybeAccount.flatMap(account -> account.getDevice(deviceId)));
|
||||
|
||||
@@ -139,7 +139,7 @@ public class GCMSender {
|
||||
}
|
||||
|
||||
private Optional<Account> getAccountForEvent(GcmMessage message) {
|
||||
Optional<Account> account = message.getUuid().flatMap(accountsManager::get);
|
||||
Optional<Account> account = message.getUuid().flatMap(accountsManager::getByAccountIdentifier);
|
||||
|
||||
if (account.isPresent()) {
|
||||
Optional<Device> device = account.get().getDevice(message.getDeviceId());
|
||||
|
||||
@@ -33,7 +33,7 @@ public class ReceiptSender {
|
||||
return;
|
||||
}
|
||||
|
||||
final Account destinationAccount = accountManager.get(destinationUuid)
|
||||
final Account destinationAccount = accountManager.getByAccountIdentifier(destinationUuid)
|
||||
.orElseThrow(() -> new NoSuchUserException(destinationUuid));
|
||||
|
||||
final Envelope.Builder message = Envelope.newBuilder()
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class Account {
|
||||
|
||||
@@ -32,6 +33,11 @@ public class Account {
|
||||
@JsonIgnore
|
||||
private UUID uuid;
|
||||
|
||||
// Nullable only until initial migration is complete
|
||||
@JsonProperty("pni")
|
||||
@Nullable
|
||||
private UUID phoneNumberIdentifier;
|
||||
|
||||
@JsonProperty
|
||||
private String number;
|
||||
|
||||
@@ -80,9 +86,10 @@ public class Account {
|
||||
public Account() {}
|
||||
|
||||
@VisibleForTesting
|
||||
public Account(String number, UUID uuid, Set<Device> devices, byte[] unidentifiedAccessKey) {
|
||||
public Account(String number, UUID uuid, final UUID phoneNumberIdentifier, Set<Device> devices, byte[] unidentifiedAccessKey) {
|
||||
this.number = number;
|
||||
this.uuid = uuid;
|
||||
this.phoneNumberIdentifier = phoneNumberIdentifier;
|
||||
this.devices = devices;
|
||||
this.unidentifiedAccessKey = unidentifiedAccessKey;
|
||||
}
|
||||
@@ -98,16 +105,11 @@ public class Account {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public void setNumber(String number) {
|
||||
// Optional only until initial migration is complete
|
||||
public Optional<UUID> getPhoneNumberIdentifier() {
|
||||
requireNotStale();
|
||||
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
public void setCanonicallyDiscoverable(boolean canonicallyDiscoverable) {
|
||||
requireNotStale();
|
||||
|
||||
this.canonicallyDiscoverable = canonicallyDiscoverable;
|
||||
return Optional.ofNullable(phoneNumberIdentifier);
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
@@ -116,6 +118,13 @@ public class Account {
|
||||
return number;
|
||||
}
|
||||
|
||||
public void setNumber(String number, UUID phoneNumberIdentifier) {
|
||||
requireNotStale();
|
||||
|
||||
this.number = number;
|
||||
this.phoneNumberIdentifier = phoneNumberIdentifier;
|
||||
}
|
||||
|
||||
public void addDevice(Device device) {
|
||||
requireNotStale();
|
||||
|
||||
@@ -247,6 +256,12 @@ public class Account {
|
||||
return canonicallyDiscoverable;
|
||||
}
|
||||
|
||||
public void setCanonicallyDiscoverable(boolean canonicallyDiscoverable) {
|
||||
requireNotStale();
|
||||
|
||||
this.canonicallyDiscoverable = canonicallyDiscoverable;
|
||||
}
|
||||
|
||||
public Optional<String> getRelay() {
|
||||
requireNotStale();
|
||||
|
||||
|
||||
@@ -13,23 +13,24 @@ import io.micrometer.core.instrument.Timer;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.CancellationReason;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Delete;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Put;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
|
||||
@@ -37,13 +38,13 @@ import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactionConflictException;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Update;
|
||||
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;
|
||||
|
||||
public class Accounts extends AbstractDynamoDbStore {
|
||||
|
||||
// uuid, primary key
|
||||
static final String KEY_ACCOUNT_UUID = "U";
|
||||
// uuid, attribute on account table, primary key for PNI table
|
||||
static final String ATTR_PNI_UUID = "PNI";
|
||||
// phone number
|
||||
static final String ATTR_ACCOUNT_E164 = "P";
|
||||
// account, serialized to JSON
|
||||
@@ -55,7 +56,8 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
|
||||
private final DynamoDbClient client;
|
||||
|
||||
private final String phoneNumbersTableName;
|
||||
private final String phoneNumberConstraintTableName;
|
||||
private final String phoneNumberIdentifierConstraintTableName;
|
||||
private final String accountsTableName;
|
||||
|
||||
private final int scanPageSize;
|
||||
@@ -64,19 +66,22 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
private static final Timer CHANGE_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "changeNumber"));
|
||||
private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update"));
|
||||
private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber"));
|
||||
private static final Timer GET_BY_PNI_TIMER = Metrics.timer(name(Accounts.class, "getByPni"));
|
||||
private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid"));
|
||||
private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(Accounts.class, "getAllFrom"));
|
||||
private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(Accounts.class, "getAllFromOffset"));
|
||||
private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete"));
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Accounts.class);
|
||||
|
||||
public Accounts(DynamoDbClient client, String accountsTableName, String phoneNumbersTableName,
|
||||
final int scanPageSize) {
|
||||
public Accounts(DynamoDbClient client, String accountsTableName, String phoneNumberConstraintTableName,
|
||||
String phoneNumberIdentifierConstraintTableName, final int scanPageSize) {
|
||||
|
||||
super(client);
|
||||
|
||||
this.client = client;
|
||||
this.phoneNumbersTableName = phoneNumbersTableName;
|
||||
this.phoneNumberConstraintTableName = phoneNumberConstraintTableName;
|
||||
this.phoneNumberIdentifierConstraintTableName = phoneNumberIdentifierConstraintTableName;
|
||||
this.accountsTableName = accountsTableName;
|
||||
this.scanPageSize = scanPageSize;
|
||||
}
|
||||
@@ -85,35 +90,98 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
return CREATE_TIMER.record(() -> {
|
||||
|
||||
try {
|
||||
TransactWriteItem phoneNumberConstraintPut = buildPutWriteItemForPhoneNumberConstraint(account, account.getUuid());
|
||||
TransactWriteItem accountPut = buildPutWriteItemForAccount(account, account.getUuid(), Put.builder()
|
||||
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber()))));
|
||||
TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
|
||||
.put(
|
||||
Put.builder()
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.item(Map.of(
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.conditionExpression(
|
||||
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
|
||||
.expressionAttributeNames(
|
||||
Map.of("#uuid", KEY_ACCOUNT_UUID,
|
||||
"#number", ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(
|
||||
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
assert account.getPhoneNumberIdentifier().isPresent();
|
||||
|
||||
if (account.getPhoneNumberIdentifier().isEmpty()) {
|
||||
log.error("Account {} is missing a phone number identifier", account.getUuid());
|
||||
}
|
||||
|
||||
TransactWriteItem phoneNumberIdentifierConstraintPut = TransactWriteItem.builder()
|
||||
.put(
|
||||
Put.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.item(Map.of(
|
||||
ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier().get()),
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.conditionExpression(
|
||||
"attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)")
|
||||
.expressionAttributeNames(
|
||||
Map.of("#uuid", KEY_ACCOUNT_UUID,
|
||||
"#pni", ATTR_PNI_UUID))
|
||||
.expressionAttributeValues(
|
||||
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
|
||||
ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())));
|
||||
|
||||
account.getPhoneNumberIdentifier().ifPresent(pni -> item.put(ATTR_PNI_UUID, AttributeValues.fromUUID(pni)));
|
||||
|
||||
TransactWriteItem accountPut = TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
|
||||
.tableName(accountsTableName)
|
||||
.item(item)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
|
||||
.transactItems(phoneNumberConstraintPut, accountPut)
|
||||
.transactItems(phoneNumberConstraintPut, phoneNumberIdentifierConstraintPut, accountPut)
|
||||
.build();
|
||||
|
||||
try {
|
||||
client.transactWriteItems(request);
|
||||
} catch (TransactionCanceledException e) {
|
||||
|
||||
final CancellationReason accountCancellationReason = e.cancellationReasons().get(1);
|
||||
final CancellationReason accountCancellationReason = e.cancellationReasons().get(2);
|
||||
|
||||
if ("ConditionalCheckFailed".equals(accountCancellationReason.code())) {
|
||||
throw new IllegalArgumentException("uuid present with different phone number");
|
||||
throw new IllegalArgumentException("account identifier present with different phone number");
|
||||
}
|
||||
|
||||
final CancellationReason phoneNumberConstraintCancellationReason = e.cancellationReasons().get(0);
|
||||
final CancellationReason phoneNumberIdentifierConstraintCancellationReason = e.cancellationReasons().get(1);
|
||||
|
||||
if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code())) {
|
||||
if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ||
|
||||
"ConditionalCheckFailed".equals(phoneNumberIdentifierConstraintCancellationReason.code())) {
|
||||
|
||||
ByteBuffer actualAccountUuid = phoneNumberConstraintCancellationReason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
|
||||
// In theory, both reasons should trip in tandem and either should give us the information we need. Even so,
|
||||
// we'll be cautious here and make sure we're choosing a condition check that really failed.
|
||||
final CancellationReason reason = "ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ?
|
||||
phoneNumberConstraintCancellationReason : phoneNumberIdentifierConstraintCancellationReason;
|
||||
|
||||
ByteBuffer actualAccountUuid = reason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
|
||||
account.setUuid(UUIDUtil.fromByteBuffer(actualAccountUuid));
|
||||
|
||||
final int version = get(account.getUuid()).get().getVersion();
|
||||
account.setVersion(version);
|
||||
final Account existingAccount = getByAccountIdentifier(account.getUuid()).orElseThrow();
|
||||
account.setNumber(existingAccount.getNumber(), existingAccount.getPhoneNumberIdentifier().orElse(account.getPhoneNumberIdentifier().orElseThrow()));
|
||||
account.setVersion(existingAccount.getVersion());
|
||||
|
||||
update(account);
|
||||
|
||||
@@ -125,7 +193,7 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
throw new ContestedOptimisticLockException();
|
||||
}
|
||||
|
||||
// this shouldn’t happen
|
||||
// this shouldn't happen
|
||||
throw new RuntimeException("could not create account: " + extractCancellationReasonCodes(e));
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
@@ -136,44 +204,11 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
});
|
||||
}
|
||||
|
||||
private TransactWriteItem buildPutWriteItemForAccount(Account account, UUID uuid, Put.Builder putBuilder) throws JsonProcessingException {
|
||||
return TransactWriteItem.builder()
|
||||
.put(putBuilder
|
||||
.tableName(accountsTableName)
|
||||
.item(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid),
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
|
||||
ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private TransactWriteItem buildPutWriteItemForPhoneNumberConstraint(Account account, UUID uuid) {
|
||||
return TransactWriteItem.builder()
|
||||
.put(
|
||||
Put.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.item(Map.of(
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)))
|
||||
.conditionExpression(
|
||||
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
|
||||
.expressionAttributeNames(
|
||||
Map.of("#uuid", KEY_ACCOUNT_UUID,
|
||||
"#number", ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(
|
||||
Map.of(":uuid", AttributeValues.fromUUID(uuid)))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the phone number for the given account. The given account's number should be its current, pre-change
|
||||
* number. If this method succeeds, the account's number will be changed to the new number. If the update fails for
|
||||
* any reason, the account's number will be unchanged.
|
||||
* number. If this method succeeds, the account's number will be changed to the new number and its phone number
|
||||
* identifier will be changed to the given phone number identifier. If the update fails for any reason, the account's
|
||||
* number and PNI will be unchanged.
|
||||
* <p/>
|
||||
* This method expects that any accounts with conflicting numbers will have been removed by the time this method is
|
||||
* called. This method may fail with an unspecified {@link RuntimeException} if another account with the same number
|
||||
@@ -182,26 +217,28 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
* @param account the account for which to change the phone number
|
||||
* @param number the new phone number
|
||||
*/
|
||||
public void changeNumber(final Account account, final String number) {
|
||||
public void changeNumber(final Account account, final String number, final UUID phoneNumberIdentifier) {
|
||||
CHANGE_NUMBER_TIMER.record(() -> {
|
||||
final String originalNumber = account.getNumber();
|
||||
final Optional<UUID> originalPni = account.getPhoneNumberIdentifier();
|
||||
|
||||
boolean succeeded = false;
|
||||
|
||||
account.setNumber(number);
|
||||
account.setNumber(number, phoneNumberIdentifier);
|
||||
|
||||
try {
|
||||
final List<TransactWriteItem> writeItems = new ArrayList<>();
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(originalNumber)))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.item(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
|
||||
@@ -211,20 +248,41 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
.build())
|
||||
.build());
|
||||
|
||||
originalPni.ifPresent(pni -> writeItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(pni)))
|
||||
.build())
|
||||
.build()));
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.item(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
|
||||
ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
|
||||
.conditionExpression("attribute_not_exists(#pni)")
|
||||
.expressionAttributeNames(Map.of("#pni", ATTR_PNI_UUID))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(
|
||||
TransactWriteItem.builder()
|
||||
.update(Update.builder()
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data, #number = :number, #cds = :cds ADD #version :version_increment")
|
||||
.updateExpression("SET #data = :data, #number = :number, #pni = :pni, #cds = :cds ADD #version :version_increment")
|
||||
.conditionExpression("attribute_exists(#number) AND #version = :version")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
|
||||
"#data", ATTR_ACCOUNT_DATA,
|
||||
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
|
||||
"#pni", ATTR_PNI_UUID,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":number", AttributeValues.fromString(number),
|
||||
":pni", AttributeValues.fromUUID(phoneNumberIdentifier),
|
||||
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
@@ -243,7 +301,7 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
throw new IllegalArgumentException(e);
|
||||
} finally {
|
||||
if (!succeeded) {
|
||||
account.setNumber(originalNumber);
|
||||
account.setNumber(originalNumber, originalPni.orElse(null));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -251,57 +309,121 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
|
||||
public void update(Account account) throws ContestedOptimisticLockException {
|
||||
UPDATE_TIMER.record(() -> {
|
||||
UpdateItemRequest updateItemRequest;
|
||||
try {
|
||||
updateItemRequest = UpdateItemRequest.builder()
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data, #cds = :cds ADD #version :version_increment")
|
||||
.conditionExpression("attribute_exists(#number) AND #version = :version")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
|
||||
"#data", ATTR_ACCOUNT_DATA,
|
||||
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
.returnValues(ReturnValue.UPDATED_NEW)
|
||||
.build();
|
||||
final List<TransactWriteItem> transactWriteItems = new ArrayList<>(2);
|
||||
|
||||
try {
|
||||
final TransactWriteItem updateAccountWriteItem;
|
||||
|
||||
if (account.getPhoneNumberIdentifier().isPresent()) {
|
||||
updateAccountWriteItem = TransactWriteItem.builder()
|
||||
.update(Update.builder()
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data, #cds = :cds, #pni = :pni ADD #version :version_increment")
|
||||
.conditionExpression("attribute_exists(#number) AND #version = :version")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
|
||||
"#data", ATTR_ACCOUNT_DATA,
|
||||
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
|
||||
"#version", ATTR_VERSION,
|
||||
"#pni", ATTR_PNI_UUID))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1),
|
||||
":pni", AttributeValues.fromUUID(account.getPhoneNumberIdentifier().get())))
|
||||
.build())
|
||||
.build();
|
||||
} else {
|
||||
updateAccountWriteItem = TransactWriteItem.builder()
|
||||
.update(Update.builder()
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data, #cds = :cds ADD #version :version_increment")
|
||||
.conditionExpression("attribute_exists(#number) AND #version = :version")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
|
||||
"#data", ATTR_ACCOUNT_DATA,
|
||||
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
transactWriteItems.add(updateAccountWriteItem);
|
||||
|
||||
// TODO Remove after initial migration to phone number identifiers
|
||||
account.getPhoneNumberIdentifier().ifPresent(phoneNumberIdentifier -> transactWriteItems.add(
|
||||
TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.item(Map.of(
|
||||
ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier().get()),
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.conditionExpression("attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)")
|
||||
.expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID, "#pni", ATTR_PNI_UUID))
|
||||
.expressionAttributeValues(
|
||||
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build()));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
UpdateItemResponse response = client.updateItem(updateItemRequest);
|
||||
client.transactWriteItems(TransactWriteItemsRequest.builder()
|
||||
.transactItems(transactWriteItems)
|
||||
.build());
|
||||
|
||||
account.setVersion(AttributeValues.getInt(response.attributes(), "V", account.getVersion() + 1));
|
||||
account.setVersion(account.getVersion() + 1);
|
||||
} catch (final TransactionConflictException e) {
|
||||
|
||||
throw new ContestedOptimisticLockException();
|
||||
|
||||
} catch (final ConditionalCheckFailedException e) {
|
||||
} catch (final TransactionCanceledException e) {
|
||||
|
||||
// the exception doesn’t give details about which condition failed,
|
||||
// but we can infer it was an optimistic locking failure if the UUID is known
|
||||
throw get(account.getUuid()).isPresent() ? new ContestedOptimisticLockException() : e;
|
||||
if ("ConditionalCheckFailed".equals(e.cancellationReasons().get(1).code())) {
|
||||
log.error("Conflicting phone number mapping exists for account {}, PNI {}", account.getUuid(), account.getPhoneNumberIdentifier());
|
||||
throw e;
|
||||
}
|
||||
|
||||
// We can infer an optimistic locking failure if the UUID is known
|
||||
throw getByAccountIdentifier(account.getUuid()).isPresent() ? new ContestedOptimisticLockException() : e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Account> get(String number) {
|
||||
public Optional<Account> getByE164(String number) {
|
||||
return GET_BY_NUMBER_TIMER.record(() -> {
|
||||
|
||||
final GetItemResponse response = client.getItem(GetItemRequest.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
|
||||
.build());
|
||||
|
||||
return Optional.ofNullable(response.item())
|
||||
.map(item -> item.get(KEY_ACCOUNT_UUID))
|
||||
.map(uuid -> accountByUuid(uuid))
|
||||
.map(this::accountByUuid)
|
||||
.map(Accounts::fromItem);
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Account> getByPhoneNumberIdentifier(final UUID phoneNumberIdentifier) {
|
||||
return GET_BY_PNI_TIMER.record(() -> {
|
||||
|
||||
final GetItemResponse response = client.getItem(GetItemRequest.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
|
||||
.build());
|
||||
|
||||
return Optional.ofNullable(response.item())
|
||||
.map(item -> item.get(KEY_ACCOUNT_UUID))
|
||||
.map(this::accountByUuid)
|
||||
.map(Accounts::fromItem);
|
||||
});
|
||||
}
|
||||
@@ -315,7 +437,7 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
return r.item().isEmpty() ? null : r.item();
|
||||
}
|
||||
|
||||
public Optional<Account> get(UUID uuid) {
|
||||
public Optional<Account> getByAccountIdentifier(UUID uuid) {
|
||||
return GET_BY_UUID_TIMER.record(() ->
|
||||
Optional.ofNullable(accountByUuid(AttributeValues.fromUUID(uuid)))
|
||||
.map(Accounts::fromItem));
|
||||
@@ -324,13 +446,11 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
public void delete(UUID uuid) {
|
||||
DELETE_TIMER.record(() -> {
|
||||
|
||||
Optional<Account> maybeAccount = get(uuid);
|
||||
|
||||
maybeAccount.ifPresent(account -> {
|
||||
getByAccountIdentifier(uuid).ifPresent(account -> {
|
||||
|
||||
TransactWriteItem phoneNumberDelete = TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber())))
|
||||
.build())
|
||||
.build();
|
||||
@@ -342,8 +462,17 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
.build())
|
||||
.build();
|
||||
|
||||
final List<TransactWriteItem> transactWriteItems = new ArrayList<>(List.of(phoneNumberDelete, accountDelete));
|
||||
|
||||
account.getPhoneNumberIdentifier().ifPresent(pni -> transactWriteItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(pni)))
|
||||
.build())
|
||||
.build()));
|
||||
|
||||
TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
|
||||
.transactItems(phoneNumberDelete, accountDelete).build();
|
||||
.transactItems(transactWriteItems).build();
|
||||
|
||||
client.transactWriteItems(request);
|
||||
});
|
||||
@@ -393,7 +522,7 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
}
|
||||
try {
|
||||
Account account = SystemMapper.getMapper().readValue(item.get(ATTR_ACCOUNT_DATA).b().asByteArray(), Account.class);
|
||||
account.setNumber(item.get(ATTR_ACCOUNT_E164).s());
|
||||
account.setNumber(item.get(ATTR_ACCOUNT_E164).s(), AttributeValues.getUUID(item, ATTR_PNI_UUID, null));
|
||||
account.setUuid(UUIDUtil.fromByteBuffer(item.get(KEY_ACCOUNT_UUID).b().asByteBuffer()));
|
||||
account.setVersion(Integer.parseInt(item.get(ATTR_VERSION).n()));
|
||||
account.setCanonicallyDiscoverable(Optional.ofNullable(item.get(ATTR_CANONICALLY_DISCOVERABLE)).map(av -> av.bool()).orElse(false));
|
||||
|
||||
@@ -52,6 +52,7 @@ public class AccountsManager {
|
||||
|
||||
private static final Timer redisSetTimer = metricRegistry.timer(name(AccountsManager.class, "redisSet" ));
|
||||
private static final Timer redisNumberGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisNumberGet"));
|
||||
private static final Timer redisPniGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisPniGet"));
|
||||
private static final Timer redisUuidGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUuidGet" ));
|
||||
private static final Timer redisDeleteTimer = metricRegistry.timer(name(AccountsManager.class, "redisDelete" ));
|
||||
|
||||
@@ -63,18 +64,19 @@ public class AccountsManager {
|
||||
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
|
||||
|
||||
private final Accounts accounts;
|
||||
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
|
||||
private final FaultTolerantRedisCluster cacheCluster;
|
||||
private final DeletedAccountsManager deletedAccountsManager;
|
||||
private final DirectoryQueue directoryQueue;
|
||||
private final KeysDynamoDb keysDynamoDb;
|
||||
private final DirectoryQueue directoryQueue;
|
||||
private final KeysDynamoDb keysDynamoDb;
|
||||
private final MessagesManager messagesManager;
|
||||
private final UsernamesManager usernamesManager;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final StoredVerificationCodeManager pendingAccounts;
|
||||
private final SecureStorageClient secureStorageClient;
|
||||
private final SecureBackupClient secureBackupClient;
|
||||
private final SecureStorageClient secureStorageClient;
|
||||
private final SecureBackupClient secureBackupClient;
|
||||
private final ClientPresenceManager clientPresenceManager;
|
||||
private final ObjectMapper mapper;
|
||||
private final ObjectMapper mapper;
|
||||
private final Clock clock;
|
||||
|
||||
public enum DeletionReason {
|
||||
@@ -89,10 +91,13 @@ public class AccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
public AccountsManager(Accounts accounts, FaultTolerantRedisCluster cacheCluster,
|
||||
public AccountsManager(final Accounts accounts,
|
||||
final PhoneNumberIdentifiers phoneNumberIdentifiers,
|
||||
final FaultTolerantRedisCluster cacheCluster,
|
||||
final DeletedAccountsManager deletedAccountsManager,
|
||||
final DirectoryQueue directoryQueue,
|
||||
final KeysDynamoDb keysDynamoDb, final MessagesManager messagesManager,
|
||||
final KeysDynamoDb keysDynamoDb,
|
||||
final MessagesManager messagesManager,
|
||||
final UsernamesManager usernamesManager,
|
||||
final ProfilesManager profilesManager,
|
||||
final StoredVerificationCodeManager pendingAccounts,
|
||||
@@ -101,6 +106,7 @@ public class AccountsManager {
|
||||
final ClientPresenceManager clientPresenceManager,
|
||||
final Clock clock) {
|
||||
this.accounts = accounts;
|
||||
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
|
||||
this.cacheCluster = cacheCluster;
|
||||
this.deletedAccountsManager = deletedAccountsManager;
|
||||
this.directoryQueue = directoryQueue;
|
||||
@@ -137,7 +143,7 @@ public class AccountsManager {
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setUserAgent(signalAgent);
|
||||
|
||||
account.setNumber(number);
|
||||
account.setNumber(number, phoneNumberIdentifiers.getPhoneNumberIdentifier(number));
|
||||
account.setUuid(maybeRecentlyDeletedUuid.orElseGet(UUID::randomUUID));
|
||||
account.addDevice(device);
|
||||
account.setRegistrationLockFromAttributes(accountAttributes);
|
||||
@@ -148,7 +154,7 @@ public class AccountsManager {
|
||||
|
||||
final UUID originalUuid = account.getUuid();
|
||||
|
||||
boolean freshUser = dynamoCreate(account);
|
||||
boolean freshUser = accounts.create(account);
|
||||
|
||||
// create() sometimes updates the UUID, if there was a number conflict.
|
||||
// for metrics, we want secondary to run with the same original UUID
|
||||
@@ -210,7 +216,7 @@ public class AccountsManager {
|
||||
deletedAccountsManager.lockAndPut(account.getNumber(), number, () -> {
|
||||
redisDelete(account);
|
||||
|
||||
final Optional<Account> maybeExistingAccount = get(number);
|
||||
final Optional<Account> maybeExistingAccount = getByE164(number);
|
||||
final Optional<UUID> displacedUuid;
|
||||
|
||||
if (maybeExistingAccount.isPresent()) {
|
||||
@@ -221,12 +227,13 @@ public class AccountsManager {
|
||||
}
|
||||
|
||||
final UUID uuid = account.getUuid();
|
||||
final UUID phoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
|
||||
|
||||
final Account numberChangedAccount = updateWithRetries(
|
||||
account,
|
||||
a -> true,
|
||||
a -> dynamoChangeNumber(a, number),
|
||||
() -> dynamoGet(uuid).orElseThrow());
|
||||
a -> accounts.changeNumber(a, number, phoneNumberIdentifier),
|
||||
() -> accounts.getByAccountIdentifier(uuid).orElseThrow());
|
||||
|
||||
updatedAccount.set(numberChangedAccount);
|
||||
directoryQueue.changePhoneNumber(numberChangedAccount, originalNumber, number);
|
||||
@@ -286,7 +293,10 @@ public class AccountsManager {
|
||||
final UUID uuid = account.getUuid();
|
||||
final String originalNumber = account.getNumber();
|
||||
|
||||
updatedAccount = updateWithRetries(account, updater, this::dynamoUpdate, () -> dynamoGet(uuid).get());
|
||||
updatedAccount = updateWithRetries(account,
|
||||
updater,
|
||||
accounts::update,
|
||||
() -> accounts.getByAccountIdentifier(uuid).orElseThrow());
|
||||
|
||||
assert updatedAccount.getNumber().equals(originalNumber);
|
||||
|
||||
@@ -355,12 +365,12 @@ public class AccountsManager {
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Account> get(String number) {
|
||||
public Optional<Account> getByE164(String number) {
|
||||
try (Timer.Context ignored = getByNumberTimer.time()) {
|
||||
Optional<Account> account = redisGet(number);
|
||||
Optional<Account> account = redisGetByE164(number);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
account = dynamoGet(number);
|
||||
account = accounts.getByE164(number);
|
||||
account.ifPresent(this::redisSet);
|
||||
}
|
||||
|
||||
@@ -368,12 +378,25 @@ public class AccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Account> get(UUID uuid) {
|
||||
try (Timer.Context ignored = getByUuidTimer.time()) {
|
||||
Optional<Account> account = redisGet(uuid);
|
||||
public Optional<Account> getByPhoneNumberIdentifier(UUID pni) {
|
||||
try (Timer.Context ignored = getByNumberTimer.time()) {
|
||||
Optional<Account> account = redisGetByPhoneNumberIdentifier(pni);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
account = dynamoGet(uuid);
|
||||
account = accounts.getByPhoneNumberIdentifier(pni);
|
||||
account.ifPresent(this::redisSet);
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Account> getByAccountIdentifier(UUID uuid) {
|
||||
try (Timer.Context ignored = getByUuidTimer.time()) {
|
||||
Optional<Account> account = redisGetByAccountIdentifier(uuid);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
account = accounts.getByAccountIdentifier(uuid);
|
||||
account.ifPresent(this::redisSet);
|
||||
}
|
||||
|
||||
@@ -417,19 +440,24 @@ public class AccountsManager {
|
||||
keysDynamoDb.delete(account.getUuid());
|
||||
messagesManager.clear(account.getUuid());
|
||||
|
||||
account.getPhoneNumberIdentifier().ifPresent(pni -> {
|
||||
keysDynamoDb.delete(pni);
|
||||
messagesManager.clear(pni);
|
||||
});
|
||||
|
||||
deleteStorageServiceDataFuture.join();
|
||||
deleteBackupServiceDataFuture.join();
|
||||
|
||||
redisDelete(account);
|
||||
dynamoDelete(account);
|
||||
accounts.delete(account.getUuid());
|
||||
|
||||
RedisOperation.unchecked(() ->
|
||||
account.getDevices().forEach(device ->
|
||||
clientPresenceManager.displacePresence(account.getUuid(), device.getId())));
|
||||
}
|
||||
|
||||
private String getAccountMapKey(String number) {
|
||||
return "AccountMap::" + number;
|
||||
private String getAccountMapKey(String key) {
|
||||
return "AccountMap::" + key;
|
||||
}
|
||||
|
||||
private String getAccountEntityKey(UUID uuid) {
|
||||
@@ -443,6 +471,9 @@ public class AccountsManager {
|
||||
cacheCluster.useCluster(connection -> {
|
||||
final RedisAdvancedClusterCommands<String, String> commands = connection.sync();
|
||||
|
||||
account.getPhoneNumberIdentifier().ifPresent(pni ->
|
||||
commands.set(getAccountMapKey(pni.toString()), account.getUuid().toString()));
|
||||
|
||||
commands.set(getAccountMapKey(account.getNumber()), account.getUuid().toString());
|
||||
commands.set(getAccountEntityKey(account.getUuid()), accountJson);
|
||||
});
|
||||
@@ -451,11 +482,19 @@ public class AccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Account> redisGet(String number) {
|
||||
try (Timer.Context ignored = redisNumberGetTimer.time()) {
|
||||
final String uuid = cacheCluster.withCluster(connection -> connection.sync().get(getAccountMapKey(number)));
|
||||
private Optional<Account> redisGetByPhoneNumberIdentifier(UUID uuid) {
|
||||
return redisGetBySecondaryKey(uuid.toString(), redisPniGetTimer);
|
||||
}
|
||||
|
||||
if (uuid != null) return redisGet(UUID.fromString(uuid));
|
||||
private Optional<Account> redisGetByE164(String e164) {
|
||||
return redisGetBySecondaryKey(e164, redisNumberGetTimer);
|
||||
}
|
||||
|
||||
private Optional<Account> redisGetBySecondaryKey(String secondaryKey, Timer timer) {
|
||||
try (Timer.Context ignored = timer.time()) {
|
||||
final String uuid = cacheCluster.withCluster(connection -> connection.sync().get(getAccountMapKey(secondaryKey)));
|
||||
|
||||
if (uuid != null) return redisGetByAccountIdentifier(UUID.fromString(uuid));
|
||||
else return Optional.empty();
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Deserialization error", e);
|
||||
@@ -466,7 +505,7 @@ public class AccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Account> redisGet(UUID uuid) {
|
||||
private Optional<Account> redisGetByAccountIdentifier(UUID uuid) {
|
||||
try (Timer.Context ignored = redisUuidGetTimer.time()) {
|
||||
final String json = cacheCluster.withCluster(connection -> connection.sync().get(getAccountEntityKey(uuid)));
|
||||
|
||||
@@ -489,32 +528,11 @@ public class AccountsManager {
|
||||
|
||||
private void redisDelete(final Account account) {
|
||||
try (final Timer.Context ignored = redisDeleteTimer.time()) {
|
||||
cacheCluster.useCluster(connection -> connection.sync()
|
||||
.del(getAccountMapKey(account.getNumber()), getAccountEntityKey(account.getUuid())));
|
||||
cacheCluster.useCluster(connection -> {
|
||||
connection.sync().del(getAccountMapKey(account.getNumber()), getAccountEntityKey(account.getUuid()));
|
||||
|
||||
account.getPhoneNumberIdentifier().ifPresent(pni -> connection.sync().del(getAccountMapKey(pni.toString())));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Account> dynamoGet(String number) {
|
||||
return accounts.get(number);
|
||||
}
|
||||
|
||||
private Optional<Account> dynamoGet(UUID uuid) {
|
||||
return accounts.get(uuid);
|
||||
}
|
||||
|
||||
private boolean dynamoCreate(Account account) {
|
||||
return accounts.create(account);
|
||||
}
|
||||
|
||||
private void dynamoUpdate(Account account) {
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
private void dynamoDelete(final Account account) {
|
||||
accounts.delete(account.getUuid());
|
||||
}
|
||||
|
||||
private void dynamoChangeNumber(final Account account, final String number) {
|
||||
accounts.changeNumber(account, number);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public class ContactDiscoveryWriter extends AccountDatabaseCrawlerListener {
|
||||
// It’s less than ideal, but crawler listeners currently must not call update()
|
||||
// with the accounts from the chunk, because updates cause the account instance to become stale. Instead, they
|
||||
// must get a new copy, which they are free to update.
|
||||
accounts.get(account.getUuid()).ifPresent(a -> accounts.update(a, NOOP_UPDATER));
|
||||
accounts.getByAccountIdentifier(account.getUuid()).ifPresent(a -> accounts.update(a, NOOP_UPDATER));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ public class MessagePersister implements Managed {
|
||||
|
||||
@VisibleForTesting
|
||||
void persistQueue(final UUID accountUuid, final long deviceId) {
|
||||
final Optional<Account> maybeAccount = accountsManager.get(accountUuid);
|
||||
final Optional<Account> maybeAccount = accountsManager.getByAccountIdentifier(accountUuid);
|
||||
|
||||
if (maybeAccount.isEmpty()) {
|
||||
logger.error("No account record found for account {}", accountUuid);
|
||||
|
||||
@@ -62,7 +62,7 @@ public class NonNormalizedAccountCrawlerListener extends AccountDatabaseCrawlerL
|
||||
workingNonNormalizedNumbers++;
|
||||
|
||||
try {
|
||||
final Optional<Account> maybeConflictingAccount = accountsManager.get(getNormalizedNumber(account));
|
||||
final Optional<Account> maybeConflictingAccount = accountsManager.getByE164(getNormalizedNumber(account));
|
||||
|
||||
if (maybeConflictingAccount.isPresent()) {
|
||||
workingConflictingNumbers++;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
/**
|
||||
* Manages a global, persistent mapping of phone numbers to phone number identifiers regardless of whether those
|
||||
* numbers/identifiers are actually associated with an account.
|
||||
*/
|
||||
public class PhoneNumberIdentifiers {
|
||||
|
||||
private final DynamoDbClient dynamoDbClient;
|
||||
private final String tableName;
|
||||
|
||||
@VisibleForTesting
|
||||
static final String KEY_E164 = "P";
|
||||
private static final String ATTR_PHONE_NUMBER_IDENTIFIER = "PNI";
|
||||
|
||||
private static final Timer GET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "get"));
|
||||
private static final Timer SET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "set"));
|
||||
|
||||
public PhoneNumberIdentifiers(final DynamoDbClient dynamoDbClient, final String tableName) {
|
||||
this.dynamoDbClient = dynamoDbClient;
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the phone number identifier (PNI) associated with the given phone number.
|
||||
*
|
||||
* @param phoneNumber the phone number for which to retrieve a phone number identifier
|
||||
* @return the phone number identifier associated with the given phone number
|
||||
*/
|
||||
public UUID getPhoneNumberIdentifier(final String phoneNumber) {
|
||||
final GetItemResponse response = GET_PNI_TIMER.record(() -> dynamoDbClient.getItem(GetItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(KEY_E164, AttributeValues.fromString(phoneNumber)))
|
||||
.projectionExpression(ATTR_PHONE_NUMBER_IDENTIFIER)
|
||||
.build()));
|
||||
|
||||
final UUID phoneNumberIdentifier;
|
||||
|
||||
if (response.hasItem()) {
|
||||
phoneNumberIdentifier = AttributeValues.getUUID(response.item(), ATTR_PHONE_NUMBER_IDENTIFIER, null);
|
||||
} else {
|
||||
phoneNumberIdentifier = generatePhoneNumberIdentifierIfNotExists(phoneNumber);
|
||||
}
|
||||
|
||||
if (phoneNumberIdentifier == null) {
|
||||
throw new RuntimeException("Could not retrieve phone number identifier from stored item");
|
||||
}
|
||||
|
||||
return phoneNumberIdentifier;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
UUID generatePhoneNumberIdentifierIfNotExists(final String phoneNumber) {
|
||||
final UpdateItemResponse response = SET_PNI_TIMER.record(() -> dynamoDbClient.updateItem(UpdateItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(KEY_E164, AttributeValues.fromString(phoneNumber)))
|
||||
.updateExpression("SET #pni = if_not_exists(#pni, :pni)")
|
||||
.expressionAttributeNames(Map.of("#pni", ATTR_PHONE_NUMBER_IDENTIFIER))
|
||||
.expressionAttributeValues(Map.of(":pni", AttributeValues.fromUUID(UUID.randomUUID())))
|
||||
.returnValues(ReturnValue.ALL_NEW)
|
||||
.build()));
|
||||
|
||||
return AttributeValues.getUUID(response.attributes(), ATTR_PHONE_NUMBER_IDENTIFIER, null);
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class PushFeedbackProcessor extends AccountDatabaseCrawlerListener {
|
||||
|
||||
if (update) {
|
||||
// fetch a new version, since the chunk is shared and implicitly read-only
|
||||
accountsManager.get(account.getUuid()).ifPresent(accountToUpdate -> {
|
||||
accountsManager.getByAccountIdentifier(account.getUuid()).ifPresent(accountToUpdate -> {
|
||||
accountsManager.update(accountToUpdate, a -> {
|
||||
for (Device device : a.getDevices()) {
|
||||
if (deviceNeedsUpdate(device)) {
|
||||
|
||||
@@ -24,7 +24,7 @@ public class RefreshingAccountAndDeviceSupplier implements Supplier<Pair<Account
|
||||
@Override
|
||||
public Pair<Account, Device> get() {
|
||||
if (account.isStale()) {
|
||||
account = accountsManager.get(account.getUuid())
|
||||
account = accountsManager.getByAccountIdentifier(account.getUuid())
|
||||
.orElseThrow(() -> new RuntimeException("Could not find account"));
|
||||
device = account.getDevice(device.getId())
|
||||
.orElseThrow(() -> new RefreshingAccountAndDeviceNotFoundException("Could not find device"));
|
||||
|
||||
@@ -47,7 +47,7 @@ public class DeadLetterHandler implements DispatchChannel {
|
||||
switch (pubSubMessage.getType().getNumber()) {
|
||||
case PubSubMessage.Type.DELIVER_VALUE:
|
||||
Envelope message = Envelope.parseFrom(pubSubMessage.getContent());
|
||||
Optional<Account> maybeAccount = accountsManager.get(address.getNumber());
|
||||
Optional<Account> maybeAccount = accountsManager.getByE164(address.getNumber());
|
||||
|
||||
if (maybeAccount.isPresent()) {
|
||||
messagesManager.insert(maybeAccount.get().getUuid(), address.getDeviceId(), message);
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||
import org.whispersystems.textsecuregcm.storage.Profiles;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
|
||||
@@ -112,6 +113,9 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
||||
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(
|
||||
configuration.getDeletedAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
|
||||
DynamoDbFromConfig.client(configuration.getPhoneNumberIdentifiersDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster",
|
||||
configuration.getCacheClusterConfiguration(), redisClusterClientResources);
|
||||
@@ -156,7 +160,10 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
||||
Accounts accounts = new Accounts(accountsDynamoDbClient,
|
||||
configuration.getAccountsDynamoDbConfiguration().getTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getScanPageSize());
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
|
||||
configuration.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
|
||||
@@ -199,12 +206,12 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
||||
deletedAccountsLockDynamoDbClient,
|
||||
configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
|
||||
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster,
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
|
||||
|
||||
for (String user : users) {
|
||||
Optional<Account> account = accountsManager.get(user);
|
||||
Optional<Account> account = accountsManager.getByE164(user);
|
||||
|
||||
if (account.isPresent()) {
|
||||
accountsManager.delete(account.get(), DeletionReason.ADMIN_DELETED);
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||
import org.whispersystems.textsecuregcm.storage.Profiles;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
|
||||
@@ -114,6 +115,9 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
||||
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig
|
||||
.client(configuration.getDeletedAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
|
||||
DynamoDbFromConfig.client(configuration.getPhoneNumberIdentifiersDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster",
|
||||
configuration.getCacheClusterConfiguration(), redisClusterClientResources);
|
||||
@@ -161,7 +165,10 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
||||
Accounts accounts = new Accounts(accountsDynamoDbClient,
|
||||
configuration.getAccountsDynamoDbConfiguration().getTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getScanPageSize());
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
|
||||
configuration.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
|
||||
@@ -202,16 +209,16 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
||||
deletedAccountsLockDynamoDbClient,
|
||||
configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
|
||||
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster,
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
|
||||
|
||||
Optional<Account> maybeAccount;
|
||||
|
||||
try {
|
||||
maybeAccount = accountsManager.get(UUID.fromString(namespace.getString("user")));
|
||||
maybeAccount = accountsManager.getByAccountIdentifier(UUID.fromString(namespace.getString("user")));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
maybeAccount = accountsManager.get(namespace.getString("user"));
|
||||
maybeAccount = accountsManager.getByE164(namespace.getString("user"));
|
||||
}
|
||||
|
||||
maybeAccount.ifPresentOrElse(account -> {
|
||||
|
||||
Reference in New Issue
Block a user