mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 03:08:04 +01:00
Migrate username storage from a relational database to DynamoDB
This commit is contained in:
@@ -202,8 +202,6 @@ import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReservedUsernames;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Usernames;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VerificationCodeStore;
|
||||
import org.whispersystems.textsecuregcm.stripe.StripeManager;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
@@ -384,10 +382,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
config.getAccountsDynamoDbConfiguration().getTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getUsernamesTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getScanPageSize());
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
|
||||
config.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(reservedUsernamesDynamoDbClient,
|
||||
config.getReservedUsernamesDynamoDbConfiguration().getTableName());
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
@@ -472,7 +470,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
DirectoryQueue directoryQueue = new DirectoryQueue(config.getDirectoryConfiguration().getSqsConfiguration());
|
||||
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
||||
StoredVerificationCodeManager pendingDevicesManager = new StoredVerificationCodeManager(pendingDevices);
|
||||
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
|
||||
ProfilesManager profilesManager = new ProfilesManager(profiles, profilesDynamoDb, cacheCluster, dynamicConfigurationManager);
|
||||
MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster, keyspaceNotificationDispatchExecutor);
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
|
||||
@@ -481,7 +478,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
|
||||
deletedAccountsLockDynamoDbClient, config.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keys, messagesManager, usernamesManager, profilesManager,
|
||||
deletedAccountsManager, directoryQueue, keys, messagesManager, reservedUsernames, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
|
||||
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(accountsManager, messagesManager);
|
||||
@@ -660,7 +657,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
// these should be common, but use @Auth DisabledPermittedAccount, which isn’t supported yet on websocket
|
||||
environment.jersey().register(
|
||||
new AccountController(pendingAccountsManager, accountsManager, usernamesManager, abusiveHostRules, rateLimiters,
|
||||
new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters,
|
||||
smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
|
||||
transitionalRecaptchaClient, gcmSender, apnSender, backupCredentialsGenerator,
|
||||
verifyExperimentEnrollmentManager));
|
||||
@@ -680,7 +677,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, unsealedSenderRateLimiter, apnFallbackManager,
|
||||
rateLimitChallengeManager, reportMessageManager, multiRecipientMessageExecutor),
|
||||
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
||||
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, usernamesManager, dynamicConfigurationManager, profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations),
|
||||
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager, profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations),
|
||||
new ProvisioningController(rateLimiters, provisioningManager),
|
||||
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().getAuthorizedTokens(), config.getRemoteConfigConfiguration().getGlobalConfig()),
|
||||
new SecureBackupController(backupCredentialsGenerator),
|
||||
|
||||
@@ -11,6 +11,9 @@ public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
@NotNull
|
||||
private String phoneNumberIdentifierTableName;
|
||||
|
||||
@NotNull
|
||||
private String usernamesTableName;
|
||||
|
||||
private int scanPageSize = 100;
|
||||
|
||||
@JsonProperty
|
||||
@@ -23,9 +26,13 @@ public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
return phoneNumberIdentifierTableName;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public String getUsernamesTableName() {
|
||||
return usernamesTableName;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public int getScanPageSize() {
|
||||
return scanPageSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernameNotAvailableException;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.ForwardedIpUtil;
|
||||
import org.whispersystems.textsecuregcm.util.Hex;
|
||||
@@ -123,7 +123,6 @@ public class AccountController {
|
||||
|
||||
private final StoredVerificationCodeManager pendingAccounts;
|
||||
private final AccountsManager accounts;
|
||||
private final UsernamesManager usernames;
|
||||
private final AbusiveHostRules abusiveHostRules;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final SmsSender smsSender;
|
||||
@@ -139,7 +138,6 @@ public class AccountController {
|
||||
|
||||
public AccountController(StoredVerificationCodeManager pendingAccounts,
|
||||
AccountsManager accounts,
|
||||
UsernamesManager usernames,
|
||||
AbusiveHostRules abusiveHostRules,
|
||||
RateLimiters rateLimiters,
|
||||
SmsSender smsSenderFactory,
|
||||
@@ -154,7 +152,6 @@ public class AccountController {
|
||||
{
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.accounts = accounts;
|
||||
this.usernames = usernames;
|
||||
this.abusiveHostRules = abusiveHostRules;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.smsSender = smsSenderFactory;
|
||||
@@ -614,7 +611,7 @@ public class AccountController {
|
||||
@Path("/username")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public void deleteUsername(@Auth AuthenticatedAccount auth) {
|
||||
usernames.delete(auth.getAccount().getUuid());
|
||||
accounts.clearUsername(auth.getAccount());
|
||||
}
|
||||
|
||||
@PUT
|
||||
@@ -634,7 +631,9 @@ public class AccountController {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
|
||||
if (!usernames.put(auth.getAccount().getUuid(), username)) {
|
||||
try {
|
||||
accounts.setUsername(auth.getAccount(), username);
|
||||
} catch (final UsernameNotAvailableException e) {
|
||||
return Response.status(Response.Status.CONFLICT).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,6 @@ import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
@@ -93,7 +92,6 @@ public class ProfileController {
|
||||
private final RateLimiters rateLimiters;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final AccountsManager accountsManager;
|
||||
private final UsernamesManager usernamesManager;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
private final ProfileBadgeConverter profileBadgeConverter;
|
||||
private final Map<String, BadgeConfiguration> badgeConfigurationMap;
|
||||
@@ -112,7 +110,6 @@ public class ProfileController {
|
||||
RateLimiters rateLimiters,
|
||||
AccountsManager accountsManager,
|
||||
ProfilesManager profilesManager,
|
||||
UsernamesManager usernamesManager,
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
ProfileBadgeConverter profileBadgeConverter,
|
||||
BadgesConfiguration badgesConfiguration,
|
||||
@@ -125,7 +122,6 @@ public class ProfileController {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.accountsManager = accountsManager;
|
||||
this.profilesManager = profilesManager;
|
||||
this.usernamesManager = usernamesManager;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.profileBadgeConverter = profileBadgeConverter;
|
||||
this.badgeConfigurationMap = badgesConfiguration.getBadges().stream().collect(Collectors.toMap(
|
||||
@@ -263,7 +259,7 @@ public class ProfileController {
|
||||
|
||||
assert(accountProfile.isPresent());
|
||||
|
||||
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());
|
||||
Optional<String> username = accountProfile.flatMap(Account::getUsername);
|
||||
Optional<VersionedProfile> profile = profilesManager.get(uuid, version);
|
||||
|
||||
String name = profile.map(VersionedProfile::getName).orElse(accountProfile.get().getProfileName());
|
||||
@@ -315,35 +311,26 @@ public class ProfileController {
|
||||
|
||||
username = username.toLowerCase();
|
||||
|
||||
Optional<UUID> uuid = usernamesManager.get(username);
|
||||
final Account accountProfile = accountsManager.getByUsername(username)
|
||||
.orElseThrow(() -> new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build()));
|
||||
|
||||
if (uuid.isEmpty()) {
|
||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||
}
|
||||
|
||||
final boolean isSelf = auth.getAccount().getUuid().equals(uuid.get());
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid.get());
|
||||
|
||||
if (accountProfile.isEmpty()) {
|
||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||
}
|
||||
final boolean isSelf = auth.getAccount().getUuid().equals(accountProfile.getUuid());
|
||||
|
||||
return new Profile(
|
||||
accountProfile.get().getProfileName(),
|
||||
accountProfile.getProfileName(),
|
||||
null,
|
||||
null,
|
||||
accountProfile.get().getAvatar(),
|
||||
accountProfile.getAvatar(),
|
||||
null,
|
||||
accountProfile.get().getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||
UserCapabilities.createForAccount(accountProfile.get()),
|
||||
accountProfile.getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(accountProfile.getUnidentifiedAccessKey()),
|
||||
accountProfile.isUnrestrictedUnidentifiedAccess(),
|
||||
UserCapabilities.createForAccount(accountProfile),
|
||||
username,
|
||||
accountProfile.get().getUuid(),
|
||||
accountProfile.getUuid(),
|
||||
profileBadgeConverter.convert(
|
||||
getAcceptableLanguagesForRequest(containerRequestContext),
|
||||
accountProfile.get().getBadges(),
|
||||
accountProfile.getBadges(),
|
||||
isSelf),
|
||||
null);
|
||||
}
|
||||
@@ -410,7 +397,7 @@ public class ProfileController {
|
||||
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(identifier);
|
||||
OptionalAccess.verify(auth.map(AuthenticatedAccount::getAccount), accessKey, accountProfile);
|
||||
|
||||
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());
|
||||
Optional<String> username = accountProfile.flatMap(Account::getUsername);
|
||||
|
||||
return new Profile(
|
||||
accountProfile.get().getProfileName(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -38,6 +39,10 @@ public class Account {
|
||||
@JsonProperty
|
||||
private String number;
|
||||
|
||||
@JsonProperty
|
||||
@Nullable
|
||||
private String username;
|
||||
|
||||
@JsonProperty
|
||||
private Set<Device> devices = new HashSet<>();
|
||||
|
||||
@@ -134,6 +139,18 @@ public class Account {
|
||||
this.phoneNumberIdentifier = phoneNumberIdentifier;
|
||||
}
|
||||
|
||||
public Optional<String> getUsername() {
|
||||
requireNotStale();
|
||||
|
||||
return Optional.ofNullable(username);
|
||||
}
|
||||
|
||||
public void setUsername(final String username) {
|
||||
requireNotStale();
|
||||
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public void addDevice(Device device) {
|
||||
requireNotStale();
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -57,19 +58,25 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
static final String ATTR_VERSION = "V";
|
||||
// canonically discoverable
|
||||
static final String ATTR_CANONICALLY_DISCOVERABLE = "C";
|
||||
// username; string
|
||||
static final String ATTR_USERNAME = "N";
|
||||
|
||||
private final DynamoDbClient client;
|
||||
|
||||
private final String phoneNumberConstraintTableName;
|
||||
private final String phoneNumberIdentifierConstraintTableName;
|
||||
private final String usernamesConstraintTableName;
|
||||
private final String accountsTableName;
|
||||
|
||||
private final int scanPageSize;
|
||||
|
||||
private static final Timer CREATE_TIMER = Metrics.timer(name(Accounts.class, "create"));
|
||||
private static final Timer CHANGE_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "changeNumber"));
|
||||
private static final Timer SET_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "setUsername"));
|
||||
private static final Timer CLEAR_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "clearUsername"));
|
||||
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_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "getByUsername"));
|
||||
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"));
|
||||
@@ -79,7 +86,8 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
private static final Logger log = LoggerFactory.getLogger(Accounts.class);
|
||||
|
||||
public Accounts(DynamoDbClient client, String accountsTableName, String phoneNumberConstraintTableName,
|
||||
String phoneNumberIdentifierConstraintTableName, final int scanPageSize) {
|
||||
String phoneNumberIdentifierConstraintTableName, final String usernamesConstraintTableName,
|
||||
final int scanPageSize) {
|
||||
|
||||
super(client);
|
||||
|
||||
@@ -87,6 +95,7 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
this.phoneNumberConstraintTableName = phoneNumberConstraintTableName;
|
||||
this.phoneNumberIdentifierConstraintTableName = phoneNumberIdentifierConstraintTableName;
|
||||
this.accountsTableName = accountsTableName;
|
||||
this.usernamesConstraintTableName = usernamesConstraintTableName;
|
||||
this.scanPageSize = scanPageSize;
|
||||
}
|
||||
|
||||
@@ -304,6 +313,141 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
});
|
||||
}
|
||||
|
||||
public void setUsername(final Account account, final String username)
|
||||
throws ContestedOptimisticLockException, UsernameNotAvailableException {
|
||||
final long startNanos = System.nanoTime();
|
||||
|
||||
final Optional<String> maybeOriginalUsername = account.getUsername();
|
||||
account.setUsername(username);
|
||||
|
||||
boolean succeeded = false;
|
||||
|
||||
try {
|
||||
final List<TransactWriteItem> writeItems = new ArrayList<>();
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.tableName(usernamesConstraintTableName)
|
||||
.item(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
|
||||
ATTR_USERNAME, AttributeValues.fromString(username)))
|
||||
.conditionExpression("attribute_not_exists(#username)")
|
||||
.expressionAttributeNames(Map.of("#username", ATTR_USERNAME))
|
||||
.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, #username = :username ADD #version :version_increment")
|
||||
.conditionExpression("#version = :version")
|
||||
.expressionAttributeNames(Map.of("#data", ATTR_ACCOUNT_DATA,
|
||||
"#username", ATTR_USERNAME,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":username", AttributeValues.fromString(username),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
maybeOriginalUsername.ifPresent(originalUsername -> writeItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(usernamesConstraintTableName)
|
||||
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(originalUsername)))
|
||||
.build())
|
||||
.build()));
|
||||
|
||||
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
|
||||
.transactItems(writeItems)
|
||||
.build();
|
||||
|
||||
client.transactWriteItems(request);
|
||||
|
||||
account.setVersion(account.getVersion() + 1);
|
||||
succeeded = true;
|
||||
} catch (final JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (final TransactionCanceledException e) {
|
||||
if ("ConditionalCheckFailed".equals(e.cancellationReasons().get(0).code())) {
|
||||
throw new UsernameNotAvailableException();
|
||||
} else if ("ConditionalCheckFailed".equals(e.cancellationReasons().get(1).code())) {
|
||||
throw new ContestedOptimisticLockException();
|
||||
}
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
if (!succeeded) {
|
||||
account.setUsername(maybeOriginalUsername.orElse(null));
|
||||
}
|
||||
|
||||
SET_USERNAME_TIMER.record(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearUsername(Account account) {
|
||||
account.getUsername().ifPresent(username -> {
|
||||
CLEAR_USERNAME_TIMER.record(() -> {
|
||||
account.setUsername(null);
|
||||
|
||||
boolean succeeded = false;
|
||||
|
||||
try {
|
||||
final List<TransactWriteItem> writeItems = new ArrayList<>();
|
||||
|
||||
writeItems.add(
|
||||
TransactWriteItem.builder()
|
||||
.update(Update.builder()
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data REMOVE #username ADD #version :version_increment")
|
||||
.conditionExpression("#version = :version")
|
||||
.expressionAttributeNames(Map.of("#data", ATTR_ACCOUNT_DATA,
|
||||
"#username", ATTR_USERNAME,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(usernamesConstraintTableName)
|
||||
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
|
||||
.transactItems(writeItems)
|
||||
.build();
|
||||
|
||||
client.transactWriteItems(request);
|
||||
|
||||
account.setVersion(account.getVersion() + 1);
|
||||
succeeded = true;
|
||||
} catch (final JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (final TransactionCanceledException e) {
|
||||
if ("ConditionalCheckFailed".equals(e.cancellationReasons().get(0).code())) {
|
||||
throw new ContestedOptimisticLockException();
|
||||
}
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
if (!succeeded) {
|
||||
account.setUsername(username);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void update(Account account) throws ContestedOptimisticLockException {
|
||||
UPDATE_TIMER.record(() -> {
|
||||
final UpdateItemRequest updateItemRequest;
|
||||
@@ -358,6 +502,21 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Account> getByUsername(final String username) {
|
||||
return GET_BY_USERNAME_TIMER.record(() -> {
|
||||
|
||||
final GetItemResponse response = client.getItem(GetItemRequest.builder()
|
||||
.tableName(usernamesConstraintTableName)
|
||||
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
|
||||
.build());
|
||||
|
||||
return Optional.ofNullable(response.item())
|
||||
.map(item -> item.get(KEY_ACCOUNT_UUID))
|
||||
.map(this::accountByUuid)
|
||||
.map(Accounts::fromItem);
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Account> getByPhoneNumberIdentifier(final UUID phoneNumberIdentifier) {
|
||||
return GET_BY_PNI_TIMER.record(() -> {
|
||||
|
||||
@@ -416,6 +575,13 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
.build())
|
||||
.build());
|
||||
|
||||
account.getUsername().ifPresent(username -> transactWriteItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(usernamesConstraintTableName)
|
||||
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
|
||||
.build())
|
||||
.build()));
|
||||
|
||||
TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
|
||||
.transactItems(transactWriteItems).build();
|
||||
|
||||
@@ -480,6 +646,7 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
|
||||
account.setNumber(item.get(ATTR_ACCOUNT_E164).s(), phoneNumberIdentifierFromAttribute);
|
||||
account.setUuid(accountIdentifier);
|
||||
account.setUsername(AttributeValues.getString(item, ATTR_USERNAME, null));
|
||||
account.setVersion(Integer.parseInt(item.get(ATTR_VERSION).n()));
|
||||
account.setCanonicallyDiscoverable(Optional.ofNullable(item.get(ATTR_CANONICALLY_DISCOVERABLE)).map(av -> av.bool()).orElse(false));
|
||||
|
||||
|
||||
@@ -44,18 +44,20 @@ import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class AccountsManager {
|
||||
|
||||
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private static final Timer createTimer = metricRegistry.timer(name(AccountsManager.class, "create" ));
|
||||
private static final Timer updateTimer = metricRegistry.timer(name(AccountsManager.class, "update" ));
|
||||
private static final Timer getByNumberTimer = metricRegistry.timer(name(AccountsManager.class, "getByNumber"));
|
||||
private static final Timer getByUuidTimer = metricRegistry.timer(name(AccountsManager.class, "getByUuid" ));
|
||||
private static final Timer deleteTimer = metricRegistry.timer(name(AccountsManager.class, "delete"));
|
||||
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private static final Timer createTimer = metricRegistry.timer(name(AccountsManager.class, "create"));
|
||||
private static final Timer updateTimer = metricRegistry.timer(name(AccountsManager.class, "update"));
|
||||
private static final Timer getByNumberTimer = metricRegistry.timer(name(AccountsManager.class, "getByNumber"));
|
||||
private static final Timer getByUsernameTimer = metricRegistry.timer(name(AccountsManager.class, "getByUsername"));
|
||||
private static final Timer getByUuidTimer = metricRegistry.timer(name(AccountsManager.class, "getByUuid"));
|
||||
private static final Timer deleteTimer = metricRegistry.timer(name(AccountsManager.class, "delete"));
|
||||
|
||||
private static final Timer redisSetTimer = metricRegistry.timer(name(AccountsManager.class, "redisSet" ));
|
||||
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" ));
|
||||
private static final Timer redisUsernameGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUsernameGet"));
|
||||
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"));
|
||||
|
||||
private static final String CREATE_COUNTER_NAME = name(AccountsManager.class, "createCounter");
|
||||
private static final String DELETE_COUNTER_NAME = name(AccountsManager.class, "deleteCounter");
|
||||
@@ -71,7 +73,7 @@ public class AccountsManager {
|
||||
private final DirectoryQueue directoryQueue;
|
||||
private final Keys keys;
|
||||
private final MessagesManager messagesManager;
|
||||
private final UsernamesManager usernamesManager;
|
||||
private final ReservedUsernames reservedUsernames;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final StoredVerificationCodeManager pendingAccounts;
|
||||
private final SecureStorageClient secureStorageClient;
|
||||
@@ -86,6 +88,11 @@ public class AccountsManager {
|
||||
// the owner.
|
||||
private static final long CACHE_TTL_SECONDS = Duration.ofDays(2).toSeconds();
|
||||
|
||||
@FunctionalInterface
|
||||
private interface AccountPersister {
|
||||
void persistAccount(Account account) throws UsernameNotAvailableException;
|
||||
}
|
||||
|
||||
public enum DeletionReason {
|
||||
ADMIN_DELETED("admin"),
|
||||
EXPIRED ("expired"),
|
||||
@@ -105,7 +112,7 @@ public class AccountsManager {
|
||||
final DirectoryQueue directoryQueue,
|
||||
final Keys keys,
|
||||
final MessagesManager messagesManager,
|
||||
final UsernamesManager usernamesManager,
|
||||
final ReservedUsernames reservedUsernames,
|
||||
final ProfilesManager profilesManager,
|
||||
final StoredVerificationCodeManager pendingAccounts,
|
||||
final SecureStorageClient secureStorageClient,
|
||||
@@ -119,12 +126,12 @@ public class AccountsManager {
|
||||
this.directoryQueue = directoryQueue;
|
||||
this.keys = keys;
|
||||
this.messagesManager = messagesManager;
|
||||
this.usernamesManager = usernamesManager;
|
||||
this.profilesManager = profilesManager;
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.secureStorageClient = secureStorageClient;
|
||||
this.secureBackupClient = secureBackupClient;
|
||||
this.clientPresenceManager = clientPresenceManager;
|
||||
this.reservedUsernames = reservedUsernames;
|
||||
this.mapper = SystemMapper.getMapper();
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
}
|
||||
@@ -236,11 +243,18 @@ public class AccountsManager {
|
||||
final UUID uuid = account.getUuid();
|
||||
final UUID phoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
|
||||
|
||||
final Account numberChangedAccount = updateWithRetries(
|
||||
account,
|
||||
a -> true,
|
||||
a -> accounts.changeNumber(a, number, phoneNumberIdentifier),
|
||||
() -> accounts.getByAccountIdentifier(uuid).orElseThrow());
|
||||
final Account numberChangedAccount;
|
||||
|
||||
try {
|
||||
numberChangedAccount = updateWithRetries(
|
||||
account,
|
||||
a -> true,
|
||||
a -> accounts.changeNumber(a, number, phoneNumberIdentifier),
|
||||
() -> accounts.getByAccountIdentifier(uuid).orElseThrow());
|
||||
} catch (UsernameNotAvailableException e) {
|
||||
// This should never happen when changing numbers
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
updatedAccount.set(numberChangedAccount);
|
||||
directoryQueue.changePhoneNumber(numberChangedAccount, originalNumber, number);
|
||||
@@ -251,13 +265,51 @@ public class AccountsManager {
|
||||
return updatedAccount.get();
|
||||
}
|
||||
|
||||
public Account setUsername(final Account account, final String username) throws UsernameNotAvailableException {
|
||||
if (account.getUsername().map(username::equals).orElse(false)) {
|
||||
return account;
|
||||
}
|
||||
|
||||
if (reservedUsernames.isReserved(username, account.getUuid())) {
|
||||
throw new UsernameNotAvailableException();
|
||||
}
|
||||
|
||||
redisDelete(account);
|
||||
|
||||
return updateWithRetries(
|
||||
account,
|
||||
a -> true,
|
||||
a -> accounts.setUsername(a, username),
|
||||
() -> accounts.getByAccountIdentifier(account.getUuid()).orElseThrow());
|
||||
}
|
||||
|
||||
public Account clearUsername(final Account account) {
|
||||
redisDelete(account);
|
||||
|
||||
try {
|
||||
return updateWithRetries(
|
||||
account,
|
||||
a -> true,
|
||||
accounts::clearUsername,
|
||||
() -> accounts.getByAccountIdentifier(account.getUuid()).orElseThrow());
|
||||
} catch (UsernameNotAvailableException e) {
|
||||
// This should never happen
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Account update(Account account, Consumer<Account> updater) {
|
||||
|
||||
return update(account, a -> {
|
||||
updater.accept(a);
|
||||
// assume that all updaters passed to the public method actually modify the account
|
||||
return true;
|
||||
});
|
||||
try {
|
||||
return update(account, a -> {
|
||||
updater.accept(a);
|
||||
// assume that all updaters passed to the public method actually modify the account
|
||||
return true;
|
||||
});
|
||||
} catch (UsernameNotAvailableException e) {
|
||||
// This should never happen for general-purpose, public account updates
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,28 +318,33 @@ public class AccountsManager {
|
||||
*/
|
||||
public Account updateDeviceLastSeen(Account account, Device device, final long lastSeen) {
|
||||
|
||||
return update(account, a -> {
|
||||
try {
|
||||
return update(account, a -> {
|
||||
|
||||
final Optional<Device> maybeDevice = a.getDevice(device.getId());
|
||||
final Optional<Device> maybeDevice = a.getDevice(device.getId());
|
||||
|
||||
return maybeDevice.map(d -> {
|
||||
if (d.getLastSeen() >= lastSeen) {
|
||||
return false;
|
||||
}
|
||||
return maybeDevice.map(d -> {
|
||||
if (d.getLastSeen() >= lastSeen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
d.setLastSeen(lastSeen);
|
||||
d.setLastSeen(lastSeen);
|
||||
|
||||
return true;
|
||||
return true;
|
||||
|
||||
}).orElse(false);
|
||||
});
|
||||
}).orElse(false);
|
||||
});
|
||||
} catch (UsernameNotAvailableException e) {
|
||||
// This should never happen when updating last-seen timestamps
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param account account to update
|
||||
* @param updater must return {@code true} if the account was actually updated
|
||||
*/
|
||||
private Account update(Account account, Function<Account, Boolean> updater) {
|
||||
private Account update(Account account, Function<Account, Boolean> updater) throws UsernameNotAvailableException {
|
||||
|
||||
final boolean wasVisibleBeforeUpdate = account.shouldBeVisibleInDirectory();
|
||||
|
||||
@@ -300,6 +357,7 @@ public class AccountsManager {
|
||||
final UUID uuid = account.getUuid();
|
||||
final String originalNumber = account.getNumber();
|
||||
final UUID originalPhoneNumberIdentifier = account.getPhoneNumberIdentifier();
|
||||
final Optional<String> originalUsername = account.getUsername();
|
||||
|
||||
updatedAccount = updateWithRetries(account,
|
||||
updater,
|
||||
@@ -320,6 +378,13 @@ public class AccountsManager {
|
||||
new RuntimeException());
|
||||
}
|
||||
|
||||
assert updatedAccount.getUsername().equals(originalUsername);
|
||||
|
||||
if (!updatedAccount.getUsername().equals(originalUsername)) {
|
||||
logger.error("Username changed via \"normal\" update; usernames must be changed via setUsername method",
|
||||
new RuntimeException());
|
||||
}
|
||||
|
||||
redisSet(updatedAccount);
|
||||
}
|
||||
|
||||
@@ -332,8 +397,8 @@ public class AccountsManager {
|
||||
return updatedAccount;
|
||||
}
|
||||
|
||||
private Account updateWithRetries(Account account, Function<Account, Boolean> updater, Consumer<Account> persister,
|
||||
Supplier<Account> retriever) {
|
||||
private Account updateWithRetries(Account account, Function<Account, Boolean> updater, AccountPersister persister,
|
||||
Supplier<Account> retriever) throws UsernameNotAvailableException {
|
||||
|
||||
if (!updater.apply(account)) {
|
||||
return account;
|
||||
@@ -345,7 +410,7 @@ public class AccountsManager {
|
||||
while (tries < maxTries) {
|
||||
|
||||
try {
|
||||
persister.accept(account);
|
||||
persister.persistAccount(account);
|
||||
|
||||
final Account updatedAccount;
|
||||
try {
|
||||
@@ -373,11 +438,16 @@ public class AccountsManager {
|
||||
}
|
||||
|
||||
public Account updateDevice(Account account, long deviceId, Consumer<Device> deviceUpdater) {
|
||||
return update(account, a -> {
|
||||
a.getDevice(deviceId).ifPresent(deviceUpdater);
|
||||
// assume that all updaters passed to the public method actually modify the device
|
||||
return true;
|
||||
});
|
||||
try {
|
||||
return update(account, a -> {
|
||||
a.getDevice(deviceId).ifPresent(deviceUpdater);
|
||||
// assume that all updaters passed to the public method actually modify the device
|
||||
return true;
|
||||
});
|
||||
} catch (UsernameNotAvailableException e) {
|
||||
// This should never happen when updating devices
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Account> getByE164(String number) {
|
||||
@@ -406,6 +476,19 @@ public class AccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Account> getByUsername(final String username) {
|
||||
try (final Timer.Context ignored = getByUsernameTimer.time()) {
|
||||
Optional<Account> account = redisGetByUsername(username);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
account = accounts.getByUsername(username);
|
||||
account.ifPresent(this::redisSet);
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Account> getByAccountIdentifier(UUID uuid) {
|
||||
try (Timer.Context ignored = getByUuidTimer.time()) {
|
||||
Optional<Account> account = redisGetByAccountIdentifier(uuid);
|
||||
@@ -450,7 +533,6 @@ public class AccountsManager {
|
||||
final CompletableFuture<Void> deleteStorageServiceDataFuture = secureStorageClient.deleteStoredData(account.getUuid());
|
||||
final CompletableFuture<Void> deleteBackupServiceDataFuture = secureBackupClient.deleteBackups(account.getUuid());
|
||||
|
||||
usernamesManager.delete(account.getUuid());
|
||||
profilesManager.deleteAll(account.getUuid());
|
||||
keys.delete(account.getUuid());
|
||||
keys.delete(account.getPhoneNumberIdentifier());
|
||||
@@ -486,6 +568,9 @@ public class AccountsManager {
|
||||
commands.setex(getAccountMapKey(account.getPhoneNumberIdentifier().toString()), CACHE_TTL_SECONDS, account.getUuid().toString());
|
||||
commands.setex(getAccountMapKey(account.getNumber()), CACHE_TTL_SECONDS, account.getUuid().toString());
|
||||
commands.setex(getAccountEntityKey(account.getUuid()), CACHE_TTL_SECONDS, accountJson);
|
||||
|
||||
account.getUsername().ifPresent(username ->
|
||||
commands.setex(getAccountMapKey(username), CACHE_TTL_SECONDS, account.getUuid().toString()));
|
||||
});
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
@@ -500,6 +585,10 @@ public class AccountsManager {
|
||||
return redisGetBySecondaryKey(e164, redisNumberGetTimer);
|
||||
}
|
||||
|
||||
private Optional<Account> redisGetByUsername(String username) {
|
||||
return redisGetBySecondaryKey(username, redisUsernameGetTimer);
|
||||
}
|
||||
|
||||
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)));
|
||||
@@ -542,10 +631,14 @@ 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()),
|
||||
getAccountMapKey(account.getPhoneNumberIdentifier().toString()),
|
||||
getAccountEntityKey(account.getUuid())));
|
||||
cacheCluster.useCluster(connection -> {
|
||||
connection.sync().del(
|
||||
getAccountMapKey(account.getNumber()),
|
||||
getAccountMapKey(account.getPhoneNumberIdentifier().toString()),
|
||||
getAccountEntityKey(account.getUuid()));
|
||||
|
||||
account.getUsername().ifPresent(username -> connection.sync().del(getAccountMapKey(username)));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
public class UsernameNotAvailableException extends Exception {
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.Timer;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.jdbi.v3.core.JdbiException;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
public class Usernames {
|
||||
|
||||
public static final String ID = "id";
|
||||
public static final String UID = "uuid";
|
||||
public static final String USERNAME = "username";
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Timer createTimer = metricRegistry.timer(name(Usernames.class, "create" ));
|
||||
private final Timer deleteTimer = metricRegistry.timer(name(Usernames.class, "delete" ));
|
||||
private final Timer getByUsernameTimer = metricRegistry.timer(name(Usernames.class, "getByUsername"));
|
||||
private final Timer getByUuidTimer = metricRegistry.timer(name(Usernames.class, "getByUuid" ));
|
||||
|
||||
private final FaultTolerantDatabase database;
|
||||
|
||||
public Usernames(FaultTolerantDatabase database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public boolean put(UUID uuid, String username) {
|
||||
return database.with(jdbi -> jdbi.withHandle(handle -> {
|
||||
try (Timer.Context ignored = createTimer.time()) {
|
||||
int modified = handle.createUpdate("INSERT INTO usernames (" + UID + ", " + USERNAME + ") VALUES (:uuid, :username) ON CONFLICT (" + UID + ") DO UPDATE SET " + USERNAME + " = EXCLUDED.username")
|
||||
.bind("uuid", uuid)
|
||||
.bind("username", username)
|
||||
.execute();
|
||||
|
||||
return modified > 0;
|
||||
} catch (JdbiException e) {
|
||||
if (e.getCause() instanceof SQLException) {
|
||||
if (((SQLException)e.getCause()).getSQLState().equals("23505")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public void delete(UUID uuid) {
|
||||
database.use(jdbi -> jdbi.useHandle(handle -> {
|
||||
try (Timer.Context ignored = deleteTimer.time()) {
|
||||
handle.createUpdate("DELETE FROM usernames WHERE " + UID + " = :uuid")
|
||||
.bind("uuid", uuid)
|
||||
.execute();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public Optional<UUID> get(String username) {
|
||||
return database.with(jdbi -> jdbi.withHandle(handle -> {
|
||||
try (Timer.Context ignored = getByUsernameTimer.time()) {
|
||||
return handle.createQuery("SELECT " + UID + " FROM usernames WHERE " + USERNAME + " = :username")
|
||||
.bind("username", username)
|
||||
.mapTo(UUID.class)
|
||||
.findFirst();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public Optional<String> get(UUID uuid) {
|
||||
return database.with(jdbi -> jdbi.withHandle(handle -> {
|
||||
try (Timer.Context ignored = getByUuidTimer.time()) {
|
||||
return handle.createQuery("SELECT " + USERNAME + " FROM usernames WHERE " + UID + " = :uuid")
|
||||
.bind("uuid", uuid)
|
||||
.mapTo(String.class)
|
||||
.findFirst();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.Timer;
|
||||
import io.lettuce.core.RedisException;
|
||||
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
public class UsernamesManager {
|
||||
|
||||
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private static final Timer createTimer = metricRegistry.timer(name(UsernamesManager.class, "create" ));
|
||||
private static final Timer deleteTimer = metricRegistry.timer(name(UsernamesManager.class, "delete" ));
|
||||
private static final Timer getByUuidTimer = metricRegistry.timer(name(UsernamesManager.class, "getByUuid" ));
|
||||
private static final Timer getByUsernameTimer = metricRegistry.timer(name(UsernamesManager.class, "getByUsername" ));
|
||||
|
||||
private static final Timer redisSetTimer = metricRegistry.timer(name(UsernamesManager.class, "redisSet" ));
|
||||
private static final Timer redisUuidGetTimer = metricRegistry.timer(name(UsernamesManager.class, "redisUuidGet" ));
|
||||
private static final Timer redisUsernameGetTimer = metricRegistry.timer(name(UsernamesManager.class, "redisUsernameGet"));
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(UsernamesManager.class);
|
||||
|
||||
private final Usernames usernames;
|
||||
private final ReservedUsernames reservedUsernames;
|
||||
private final FaultTolerantRedisCluster cacheCluster;
|
||||
|
||||
public UsernamesManager(Usernames usernames, ReservedUsernames reservedUsernames, FaultTolerantRedisCluster cacheCluster) {
|
||||
this.usernames = usernames;
|
||||
this.reservedUsernames = reservedUsernames;
|
||||
this.cacheCluster = cacheCluster;
|
||||
}
|
||||
|
||||
public boolean put(UUID uuid, String username) {
|
||||
try (Timer.Context ignored = createTimer.time()) {
|
||||
if (reservedUsernames.isReserved(username, uuid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (databasePut(uuid, username)) {
|
||||
redisSet(uuid, username, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<UUID> get(String username) {
|
||||
try (Timer.Context ignored = getByUsernameTimer.time()) {
|
||||
Optional<UUID> uuid = redisGet(username);
|
||||
|
||||
if (uuid.isPresent()) {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
Optional<UUID> retrieved = databaseGet(username);
|
||||
retrieved.ifPresent(retrievedUuid -> redisSet(retrievedUuid, username, false));
|
||||
|
||||
return retrieved;
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<String> get(UUID uuid) {
|
||||
try (Timer.Context ignored = getByUuidTimer.time()) {
|
||||
Optional<String> username = redisGet(uuid);
|
||||
|
||||
if (username.isPresent()) {
|
||||
return username;
|
||||
}
|
||||
|
||||
Optional<String> retrieved = databaseGet(uuid);
|
||||
retrieved.ifPresent(retrievedUsername -> redisSet(uuid, retrievedUsername, false));
|
||||
|
||||
return retrieved;
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(UUID uuid) {
|
||||
try (Timer.Context ignored = deleteTimer.time()) {
|
||||
redisDelete(uuid);
|
||||
databaseDelete(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean databasePut(UUID uuid, String username) {
|
||||
return usernames.put(uuid, username);
|
||||
}
|
||||
|
||||
private Optional<UUID> databaseGet(String username) {
|
||||
return usernames.get(username);
|
||||
}
|
||||
|
||||
private void databaseDelete(UUID uuid) {
|
||||
usernames.delete(uuid);
|
||||
}
|
||||
|
||||
private Optional<String> databaseGet(UUID uuid) {
|
||||
return usernames.get(uuid);
|
||||
}
|
||||
|
||||
private void redisSet(UUID uuid, String username, boolean required) {
|
||||
final String uuidMapKey = getUuidMapKey(uuid);
|
||||
final String usernameMapKey = getUsernameMapKey(username);
|
||||
|
||||
try (Timer.Context ignored = redisSetTimer.time()) {
|
||||
cacheCluster.useCluster(connection -> {
|
||||
final RedisAdvancedClusterCommands<String, String> commands = connection.sync();
|
||||
|
||||
final Optional<String> maybeOldUsername = Optional.ofNullable(commands.get(uuidMapKey));
|
||||
|
||||
maybeOldUsername.ifPresent(oldUsername -> commands.del(getUsernameMapKey(oldUsername)));
|
||||
commands.set(uuidMapKey, username);
|
||||
commands.set(usernameMapKey, uuid.toString());
|
||||
});
|
||||
} catch (RedisException e) {
|
||||
if (required) throw e;
|
||||
else logger.warn("Ignoring Redis failure", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<UUID> redisGet(String username) {
|
||||
try (Timer.Context ignored = redisUsernameGetTimer.time()) {
|
||||
final String result = cacheCluster.withCluster(connection -> connection.sync().get(getUsernameMapKey(username)));
|
||||
|
||||
if (result == null) return Optional.empty();
|
||||
else return Optional.of(UUID.fromString(result));
|
||||
} catch (RedisException e) {
|
||||
logger.warn("Redis get failure", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<String> redisGet(UUID uuid) {
|
||||
try (Timer.Context ignored = redisUuidGetTimer.time()) {
|
||||
final String result = cacheCluster.withCluster(connection -> connection.sync().get(getUuidMapKey(uuid)));
|
||||
|
||||
return Optional.ofNullable(result);
|
||||
} catch (RedisException e) {
|
||||
logger.warn("Redis get failure", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void redisDelete(UUID uuid) {
|
||||
try (Timer.Context ignored = redisUuidGetTimer.time()) {
|
||||
cacheCluster.useCluster(connection -> {
|
||||
final RedisAdvancedClusterCommands<String, String> commands = connection.sync();
|
||||
|
||||
commands.del(getUuidMapKey(uuid));
|
||||
|
||||
redisGet(uuid).ifPresent(username -> {
|
||||
commands.del(getUsernameMapKey(username));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private String getUuidMapKey(UUID uuid) {
|
||||
return "UsernameByUuid::" + uuid.toString();
|
||||
}
|
||||
|
||||
private String getUsernameMapKey(String username) {
|
||||
return "UsernameByUsername::" + username;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -56,8 +56,6 @@ import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReservedUsernames;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Usernames;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VerificationCodeStore;
|
||||
import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
@@ -175,10 +173,10 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
||||
configuration.getAccountsDynamoDbConfiguration().getTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getUsernamesTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getScanPageSize());
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
|
||||
configuration.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
ProfilesDynamoDb profilesDynamoDb = new ProfilesDynamoDb(dynamoDbClient, dynamoDbAsyncClient,
|
||||
configuration.getDynamoDbTables().getProfiles().getTableName());
|
||||
@@ -210,7 +208,6 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
|
||||
DirectoryQueue directoryQueue = new DirectoryQueue(
|
||||
configuration.getDirectoryConfiguration().getSqsConfiguration());
|
||||
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
|
||||
ProfilesManager profilesManager = new ProfilesManager(profiles, profilesDynamoDb, cacheCluster,
|
||||
dynamicConfigurationManager);
|
||||
ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(reportMessagesDynamoDb,
|
||||
@@ -225,7 +222,7 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
||||
configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
|
||||
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keys, messagesManager, usernamesManager, profilesManager,
|
||||
deletedAccountsManager, directoryQueue, keys, messagesManager, reservedUsernames, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
|
||||
|
||||
for (String user : users) {
|
||||
|
||||
@@ -54,8 +54,6 @@ import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReservedUsernames;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Usernames;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VerificationCodeStore;
|
||||
import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
@@ -179,10 +177,10 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
||||
configuration.getAccountsDynamoDbConfiguration().getTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getUsernamesTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getScanPageSize());
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
|
||||
configuration.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
ProfilesDynamoDb profilesDynamoDb = new ProfilesDynamoDb(dynamoDbClient, dynamoDbAsyncClient,
|
||||
configuration.getDynamoDbTables().getProfiles().getTableName());
|
||||
@@ -212,7 +210,6 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
|
||||
DirectoryQueue directoryQueue = new DirectoryQueue(
|
||||
configuration.getDirectoryConfiguration().getSqsConfiguration());
|
||||
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
|
||||
ProfilesManager profilesManager = new ProfilesManager(profiles, profilesDynamoDb, cacheCluster,
|
||||
dynamicConfigurationManager);
|
||||
ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(reportMessagesDynamoDb,
|
||||
@@ -227,7 +224,7 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
||||
configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
|
||||
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keys, messagesManager, usernamesManager, profilesManager,
|
||||
deletedAccountsManager, directoryQueue, keys, messagesManager, reservedUsernames, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
|
||||
|
||||
Optional<Account> maybeAccount;
|
||||
|
||||
Reference in New Issue
Block a user