diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcService.java index 283503832..581c8340a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcService.java @@ -16,6 +16,7 @@ import java.util.Optional; import java.util.stream.Collectors; import org.signal.chat.backup.GetBackupAuthCredentialsRequest; import org.signal.chat.backup.GetBackupAuthCredentialsResponse; +import org.signal.chat.backup.GetBackupIdLimitsResponse; import org.signal.chat.backup.RedeemReceiptRequest; import org.signal.chat.backup.RedeemReceiptResponse; import org.signal.chat.backup.SetBackupIdRequest; @@ -138,6 +139,16 @@ public class BackupsGrpcService extends SimpleBackupsGrpc.BackupsImplBase { } } + @Override + public GetBackupIdLimitsResponse getBackupIdLimits(final Empty request) { + final Account account = authenticatedAccount(); + final BackupAuthManager.BackupIdRotationLimit limit = backupAuthManager.checkBackupIdRotationLimit(account); + return GetBackupIdLimitsResponse.newBuilder() + .setHasPermitsRemaining(limit.hasPermitsRemaining()) + .setRetryAfterSeconds(limit.nextPermitAvailable().getSeconds()) + .build(); + } + @Override public Throwable mapException(final Throwable throwable) { return switch (throwable) { diff --git a/service/src/main/proto/org/signal/chat/backups.proto b/service/src/main/proto/org/signal/chat/backups.proto index 92b75064c..fec7709a9 100644 --- a/service/src/main/proto/org/signal/chat/backups.proto +++ b/service/src/main/proto/org/signal/chat/backups.proto @@ -59,6 +59,9 @@ service Backups { // Clients must validate the receipt level on the credential matches a known // receipt level before using it. rpc GetBackupAuthCredentials(GetBackupAuthCredentialsRequest) returns (GetBackupAuthCredentialsResponse) {} + + // Retrieve backup ID rotation limits + rpc GetBackupIdLimits(google.protobuf.Empty) returns (GetBackupIdLimitsResponse) {} } message SetBackupIdRequest { @@ -125,6 +128,13 @@ message GetBackupAuthCredentialsResponse { Credentials credentials = 1; } +message GetBackupIdLimitsResponse { + // If true, a call to SetBackupId may succeed without waiting + bool hasPermitsRemaining = 1; + // How long to wait before a permit becomes available, in seconds + uint64 retryAfterSeconds = 2; +} + // Service for backup operations with anonymous credentials // // This service never requires account authentication. It instead requires a diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcServiceTest.java index 8855fb455..f7e4cd54e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcServiceTest.java @@ -14,6 +14,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.protobuf.ByteString; +import com.google.protobuf.Empty; import io.grpc.Status; import java.time.Clock; import java.time.Duration; @@ -35,6 +36,7 @@ import org.mockito.Mock; import org.signal.chat.backup.BackupsGrpc; import org.signal.chat.backup.GetBackupAuthCredentialsRequest; import org.signal.chat.backup.GetBackupAuthCredentialsResponse; +import org.signal.chat.backup.GetBackupIdLimitsResponse; import org.signal.chat.backup.RedeemReceiptRequest; import org.signal.chat.backup.RedeemReceiptResponse; import org.signal.chat.backup.SetBackupIdRequest; @@ -263,4 +265,22 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().getBackupAuthCredentials(builder.build())); } + @ParameterizedTest + @CsvSource({ + "true, 0", + "false, 3600" + }) + void getBackupIdLimits(final boolean hasPermitsRemaining, final long retryAfterSeconds) { + when(backupAuthManager.checkBackupIdRotationLimit(account)) + .thenReturn(new BackupAuthManager.BackupIdRotationLimit( + hasPermitsRemaining, Duration.ofSeconds(retryAfterSeconds))); + + final GetBackupIdLimitsResponse response = + authenticatedServiceStub().getBackupIdLimits(Empty.getDefaultInstance()); + + assertThat(response.getHasPermitsRemaining()).isEqualTo(hasPermitsRemaining); + assertThat(response.getRetryAfterSeconds()).isEqualTo(retryAfterSeconds); + verify(backupAuthManager).checkBackupIdRotationLimit(account); + } + }