Remove from svrb on account deletion

This commit is contained in:
ravi-signal
2025-07-09 14:10:16 -05:00
parent 656b08f3b6
commit 58b9fa100d
13 changed files with 129 additions and 114 deletions

View File

@@ -205,7 +205,7 @@ import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
import org.whispersystems.textsecuregcm.s3.PolicySigner;
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecoveryClient;
import org.whispersystems.textsecuregcm.spam.ChallengeConstraintChecker;
import org.whispersystems.textsecuregcm.spam.RegistrationFraudChecker;
import org.whispersystems.textsecuregcm.spam.RegistrationRecoveryChecker;
@@ -508,8 +508,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.maxThreads(1).minThreads(1).build();
ExecutorService fcmSenderExecutor = environment.lifecycle().executorService(name(getClass(), "fcmSender-%d"))
.maxThreads(32).minThreads(32).workQueue(fcmSenderQueue).build();
ExecutorService secureValueRecovery2ServiceExecutor = environment.lifecycle()
.executorService(name(getClass(), "secureValueRecoveryService2-%d")).maxThreads(1).minThreads(1).build();
ExecutorService secureValueRecoveryServiceExecutor = environment.lifecycle()
.executorService(name(getClass(), "secureValueRecoveryService-%d")).maxThreads(1).minThreads(1).build();
ExecutorService storageServiceExecutor = environment.lifecycle()
.executorService(name(getClass(), "storageService-%d")).maxThreads(1).minThreads(1).build();
ExecutorService virtualThreadEventLoggerExecutor = environment.lifecycle()
@@ -624,8 +624,18 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getKeyTransparencyServiceConfiguration().tlsCertificate(),
config.getKeyTransparencyServiceConfiguration().clientCertificate(),
config.getKeyTransparencyServiceConfiguration().clientPrivateKey().value());
SecureValueRecovery2Client secureValueRecovery2Client = new SecureValueRecovery2Client(svr2CredentialsGenerator,
secureValueRecovery2ServiceExecutor, secureValueRecoveryServiceRetryExecutor, config.getSvr2Configuration());
SecureValueRecoveryClient secureValueRecovery2Client = new SecureValueRecoveryClient(
svr2CredentialsGenerator,
secureValueRecoveryServiceExecutor,
secureValueRecoveryServiceRetryExecutor,
config.getSvr2Configuration(),
() -> dynamicConfigurationManager.getConfiguration().getSvr2StatusCodesToIgnoreForAccountDeletion());
SecureValueRecoveryClient secureValueRecoveryBClient = new SecureValueRecoveryClient(
svrbCredentialsGenerator,
secureValueRecoveryServiceExecutor,
secureValueRecoveryServiceRetryExecutor,
config.getSvrbConfiguration(),
() -> dynamicConfigurationManager.getConfiguration().getSvrbStatusCodesToIgnoreForAccountDeletion());
SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator,
storageServiceExecutor, storageServiceRetryExecutor, config.getSecureStorageServiceConfiguration());
DisconnectionRequestManager disconnectionRequestManager = new DisconnectionRequestManager(pubsubClient, disconnectionRequestListenerExecutor);
@@ -646,7 +656,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new ClientPublicKeysManager(clientPublicKeys, accountLockManager, accountLockExecutor);
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
pubsubClient, accountLockManager, keysManager, messagesManager, profilesManager,
secureStorageClient, secureValueRecovery2Client, disconnectionRequestManager,
secureStorageClient, secureValueRecovery2Client, secureValueRecoveryBClient, disconnectionRequestManager,
registrationRecoveryPasswordsManager, clientPublicKeysManager, accountLockExecutor, messagePollExecutor,
clock, config.getLinkDeviceSecretConfiguration().secret().value(), dynamicConfigurationManager);
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);

View File

@@ -6,7 +6,6 @@
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.vdurmont.semver4j.Semver;
import jakarta.validation.Valid;
import java.util.Collections;
import java.util.HashMap;
@@ -14,7 +13,6 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.whispersystems.textsecuregcm.limits.RateLimiterConfig;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
public class DynamicConfiguration {
@@ -60,7 +58,11 @@ public class DynamicConfiguration {
@JsonProperty
@Valid
List<String> svrStatusCodesToIgnoreForAccountDeletion = Collections.emptyList();
List<Integer> svr2StatusCodesToIgnoreForAccountDeletion = Collections.emptyList();
@JsonProperty
@Valid
List<Integer> svrbStatusCodesToIgnoreForAccountDeletion = Collections.emptyList();
@JsonProperty
@Valid
@@ -108,8 +110,12 @@ public class DynamicConfiguration {
return metricsConfiguration;
}
public List<String> getSvrStatusCodesToIgnoreForAccountDeletion() {
return svrStatusCodesToIgnoreForAccountDeletion;
public List<Integer> getSvr2StatusCodesToIgnoreForAccountDeletion() {
return svr2StatusCodesToIgnoreForAccountDeletion;
}
public List<Integer> getSvrbStatusCodesToIgnoreForAccountDeletion() {
return svrbStatusCodesToIgnoreForAccountDeletion;
}
public DynamicRestDeprecationConfiguration restDeprecation() {

View File

@@ -15,10 +15,14 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.cert.CertificateException;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
@@ -28,21 +32,26 @@ import org.whispersystems.textsecuregcm.util.HttpUtils;
/**
* A client for sending requests to Signal's secure value recovery v2 service on behalf of authenticated users.
*/
public class SecureValueRecovery2Client {
public class SecureValueRecoveryClient {
private static final Logger logger = LoggerFactory.getLogger(SecureValueRecoveryClient.class);
private final ExternalServiceCredentialsGenerator secureValueRecoveryCredentialsGenerator;
private final URI deleteUri;
private final Supplier<List<Integer>> allowedDeletionErrorStatusCodes;
private final FaultTolerantHttpClient httpClient;
@VisibleForTesting
static final String DELETE_PATH = "/v1/delete";
public SecureValueRecovery2Client(final ExternalServiceCredentialsGenerator secureValueRecoveryCredentialsGenerator,
public SecureValueRecoveryClient(
final ExternalServiceCredentialsGenerator secureValueRecoveryCredentialsGenerator,
final Executor executor, final ScheduledExecutorService retryExecutor,
final SecureValueRecoveryConfiguration configuration)
final SecureValueRecoveryConfiguration configuration,
Supplier<List<Integer>> allowedDeletionErrorStatusCodes)
throws CertificateException {
this.secureValueRecoveryCredentialsGenerator = secureValueRecoveryCredentialsGenerator;
this.deleteUri = URI.create(configuration.uri()).resolve(DELETE_PATH);
this.allowedDeletionErrorStatusCodes = allowedDeletionErrorStatusCodes;
this.httpClient = FaultTolerantHttpClient.newBuilder()
.withCircuitBreaker(configuration.circuitBreaker())
.withRetry(configuration.retry())
@@ -57,7 +66,7 @@ public class SecureValueRecovery2Client {
.build();
}
public CompletableFuture<Void> deleteBackups(final UUID accountUuid) {
public CompletableFuture<Void> removeData(final UUID accountUuid) {
final ExternalServiceCredentials credentials = secureValueRecoveryCredentialsGenerator.generateForUuid(accountUuid);
@@ -72,6 +81,13 @@ public class SecureValueRecovery2Client {
return null;
}
final List<Integer> allowedErrors = allowedDeletionErrorStatusCodes.get();
if (allowedErrors.contains(response.statusCode())) {
logger.warn("Ignoring failure to delete svr entry for account {} with status {}",
accountUuid, response.statusCode());
return null;
}
logger.warn("Failed to delete svr entry for account {} with status {}", accountUuid, response.statusCode());
throw new SecureValueRecoveryException("Failed to delete backup", String.valueOf(response.statusCode()));
});
}

View File

@@ -45,7 +45,6 @@ import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
@@ -84,8 +83,7 @@ import org.whispersystems.textsecuregcm.redis.FaultTolerantPubSubConnection;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClient;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClusterClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecoveryException;
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecoveryClient;
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.SystemMapper;
@@ -126,8 +124,8 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
private final MessagesManager messagesManager;
private final ProfilesManager profilesManager;
private final SecureStorageClient secureStorageClient;
private final SecureValueRecovery2Client secureValueRecovery2Client;
private final SecureValueRecoveryClient secureValueRecovery2Client;
private final SecureValueRecoveryClient secureValueRecoveryBClient;
private final DisconnectionRequestManager disconnectionRequestManager;
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
private final ClientPublicKeysManager clientPublicKeysManager;
@@ -209,7 +207,8 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
final MessagesManager messagesManager,
final ProfilesManager profilesManager,
final SecureStorageClient secureStorageClient,
final SecureValueRecovery2Client secureValueRecovery2Client,
final SecureValueRecoveryClient secureValueRecovery2Client,
final SecureValueRecoveryClient secureValueRecoveryBClient,
final DisconnectionRequestManager disconnectionRequestManager,
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
final ClientPublicKeysManager clientPublicKeysManager,
@@ -228,6 +227,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
this.profilesManager = profilesManager;
this.secureStorageClient = secureStorageClient;
this.secureValueRecovery2Client = secureValueRecovery2Client;
this.secureValueRecoveryBClient = secureValueRecoveryBClient;
this.disconnectionRequestManager = disconnectionRequestManager;
this.registrationRecoveryPasswordsManager = requireNonNull(registrationRecoveryPasswordsManager);
this.clientPublicKeysManager = clientPublicKeysManager;
@@ -1292,20 +1292,10 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
account.getIdentifier(IdentityType.ACI),
device.getId())))
.toList();
final CompletableFuture<Void> svr2DeleteBackupFuture = secureValueRecovery2Client.deleteBackups(account.getUuid())
.exceptionally(ExceptionUtils.exceptionallyHandler(SecureValueRecoveryException.class, exception -> {
final List<String> svrStatusCodesToIgnore = dynamicConfigurationManager.getConfiguration().getSvrStatusCodesToIgnoreForAccountDeletion();
if (svrStatusCodesToIgnore.contains(exception.getStatusCode())) {
logger.warn("Ignoring failure to delete svr2 backup for account: " + account.getUuid(), exception);
return null;
}
logger.warn("Failed to delete svr2 backup for account: " + account.getUuid(), exception);
throw new CompletionException(exception);
}));
return CompletableFuture.allOf(
secureStorageClient.deleteStoredData(account.getUuid()),
svr2DeleteBackupFuture,
secureValueRecovery2Client.removeData(account.getUuid()),
secureValueRecoveryBClient.removeData(account.getUuid()),
keysManager.deleteSingleUsePreKeys(account.getUuid()),
keysManager.deleteSingleUsePreKeys(account.getPhoneNumberIdentifier()),
messagesManager.clear(account.getUuid()),

View File

@@ -34,6 +34,7 @@ import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfigurati
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.controllers.SecureValueRecoveryBController;
import org.whispersystems.textsecuregcm.experiment.PushNotificationExperimentSamples;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.metrics.MicrometerAwsSdkMetricPublisher;
@@ -45,7 +46,7 @@ import org.whispersystems.textsecuregcm.push.WebSocketConnectionEventManager;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClient;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClusterClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecoveryClient;
import org.whispersystems.textsecuregcm.storage.AccountLockManager;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
@@ -173,6 +174,8 @@ record CommandDependencies(
configuration.getSecureStorageServiceConfiguration());
ExternalServiceCredentialsGenerator secureValueRecovery2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator(
configuration.getSvr2Configuration());
ExternalServiceCredentialsGenerator secureValueRecoveryBCredentialsGenerator = SecureValueRecoveryBController.credentialsGenerator(
configuration.getSvrbConfiguration());
final ExecutorService awsSdkMetricsExecutor = environment.lifecycle()
.virtualExecutorService(MetricRegistry.name(WhisperServerService.class, "awsSdkMetrics-%d"));
@@ -238,10 +241,18 @@ record CommandDependencies(
.getRedisClusterConfiguration().build("messages", redisClientResourcesBuilder);
FaultTolerantRedisClusterClient rateLimitersCluster = configuration.getRateLimitersCluster().build("rate_limiters",
redisClientResourcesBuilder);
SecureValueRecovery2Client secureValueRecovery2Client = new SecureValueRecovery2Client(
secureValueRecovery2CredentialsGenerator, secureValueRecoveryServiceExecutor,
SecureValueRecoveryClient secureValueRecovery2Client = new SecureValueRecoveryClient(
secureValueRecovery2CredentialsGenerator,
secureValueRecoveryServiceExecutor,
secureValueRecoveryServiceRetryExecutor,
configuration.getSvr2Configuration());
configuration.getSvr2Configuration(),
() -> dynamicConfigurationManager.getConfiguration().getSvr2StatusCodesToIgnoreForAccountDeletion());
SecureValueRecoveryClient secureValueRecoveryBClient = new SecureValueRecoveryClient(
secureValueRecoveryBCredentialsGenerator,
secureValueRecoveryServiceExecutor,
secureValueRecoveryServiceRetryExecutor,
configuration.getSvrbConfiguration(),
() -> dynamicConfigurationManager.getConfiguration().getSvrbStatusCodesToIgnoreForAccountDeletion());
SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator,
storageServiceExecutor, storageServiceRetryExecutor, configuration.getSecureStorageServiceConfiguration());
DisconnectionRequestManager disconnectionRequestManager = new DisconnectionRequestManager(pubsubClient, disconnectionRequestListenerExecutor);
@@ -264,7 +275,7 @@ record CommandDependencies(
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords);
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
pubsubClient, accountLockManager, keys, messagesManager, profilesManager,
secureStorageClient, secureValueRecovery2Client, disconnectionRequestManager,
secureStorageClient, secureValueRecovery2Client, secureValueRecoveryBClient, disconnectionRequestManager,
registrationRecoveryPasswordsManager, clientPublicKeysManager, accountLockExecutor, messagePollExecutor,
clock, configuration.getLinkDeviceSecretConfiguration().secret().value(), dynamicConfigurationManager);
RateLimiters rateLimiters = RateLimiters.create(dynamicConfigurationManager, rateLimitersCluster);