From 4dc3b19d2ab3f48900a6e12cfd496686006a9c1d Mon Sep 17 00:00:00 2001 From: Ravi Khadiwala Date: Tue, 27 May 2025 18:10:24 -0500 Subject: [PATCH] Track backup metrics on refreshes --- .../auth/AuthenticatedBackupUser.java | 14 ++-- .../textsecuregcm/backup/BackupManager.java | 28 +++++-- .../textsecuregcm/backup/BackupsDb.java | 60 +++++++++++--- .../controllers/ArchiveController.java | 37 +++++---- .../grpc/BackupsAnonymousGrpcService.java | 3 +- .../workers/BackupMetricsCommand.java | 27 ------- .../backup/BackupManagerTest.java | 54 +++++++------ .../textsecuregcm/backup/BackupsDbTest.java | 78 +++++++++++-------- .../controllers/ArchiveControllerTest.java | 24 +++--- .../grpc/BackupsAnonymousGrpcServiceTest.java | 4 +- 10 files changed, 198 insertions(+), 131 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticatedBackupUser.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticatedBackupUser.java index 20709c806..cbd12cd25 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticatedBackupUser.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticatedBackupUser.java @@ -7,10 +7,14 @@ package org.whispersystems.textsecuregcm.auth; import org.signal.libsignal.zkgroup.backups.BackupCredentialType; import org.signal.libsignal.zkgroup.backups.BackupLevel; +import org.whispersystems.textsecuregcm.util.ua.UserAgent; +import javax.annotation.Nullable; -public record AuthenticatedBackupUser(byte[] backupId, - BackupCredentialType credentialType, - BackupLevel backupLevel, - String backupDir, - String mediaDir) { +public record AuthenticatedBackupUser( + byte[] backupId, + BackupCredentialType credentialType, + BackupLevel backupLevel, + String backupDir, + String mediaDir, + @Nullable UserAgent userAgent) { } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupManager.java index c71b1e28b..b0cb3b1f5 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupManager.java @@ -10,6 +10,8 @@ import io.dropwizard.util.DataSize; import io.grpc.Status; import io.micrometer.core.instrument.DistributionSummary; import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Timer; import java.security.SecureRandom; import java.time.Clock; @@ -36,9 +38,13 @@ import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator; import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; import org.whispersystems.textsecuregcm.util.AsyncTimerUtil; import org.whispersystems.textsecuregcm.util.ExceptionUtils; import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; +import org.whispersystems.textsecuregcm.util.ua.UserAgent; +import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; @@ -316,7 +322,9 @@ public class BackupManager { .thenApply(ignored -> usage)) .whenComplete((newUsage, throwable) -> { boolean usageChanged = throwable == null && !newUsage.equals(info.usageInfo()); - Metrics.counter(USAGE_RECALCULATION_COUNTER_NAME, "usageChanged", String.valueOf(usageChanged)) + Metrics.counter(USAGE_RECALCULATION_COUNTER_NAME, Tags.of( + UserAgentTagUtil.getPlatformTag(backupUser.userAgent()), + Tag.of("usageChanged", String.valueOf(usageChanged)))) .increment(); }) .thenApply(newUsage -> MAX_TOTAL_BACKUP_MEDIA_BYTES - newUsage.bytesUsed()); @@ -520,7 +528,8 @@ public class BackupManager { */ public CompletableFuture authenticateBackupUser( final BackupAuthCredentialPresentation presentation, - final byte[] signature) { + final byte[] signature, + final String userAgentString) { final PresentationSignatureVerifier signatureVerifier = verifyPresentation(presentation); return backupsDb .retrieveAuthenticationData(presentation.getBackupId()) @@ -538,12 +547,20 @@ public class BackupManager { final Pair credentialTypeAndBackupLevel = signatureVerifier.verifySignature(signature, authenticationData.publicKey()); + UserAgent userAgent; + try { + userAgent = UserAgentUtil.parseUserAgentString(userAgentString); + } catch (UnrecognizedUserAgentException e) { + userAgent = null; + } + return new AuthenticatedBackupUser( presentation.getBackupId(), credentialTypeAndBackupLevel.first(), credentialTypeAndBackupLevel.second(), authenticationData.backupDir(), - authenticationData.mediaDir()); + authenticationData.mediaDir(), + userAgent); }) .thenApply(result -> { Metrics.counter(ZK_AUTHN_COUNTER_NAME, SUCCESS_TAG_NAME, String.valueOf(true)).increment(); @@ -673,8 +690,9 @@ public class BackupManager { @VisibleForTesting static void checkBackupLevel(final AuthenticatedBackupUser backupUser, final BackupLevel backupLevel) { if (backupUser.backupLevel().compareTo(backupLevel) < 0) { - Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME, - FAILURE_REASON_TAG_NAME, "level") + Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME, Tags.of( + UserAgentTagUtil.getPlatformTag(backupUser.userAgent()), + Tag.of(FAILURE_REASON_TAG_NAME, "level"))) .increment(); throw Status.PERMISSION_DENIED diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java index c705e40b3..251c64506 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java @@ -11,6 +11,7 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.time.Clock; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -21,12 +22,16 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.zkgroup.backups.BackupLevel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; import org.whispersystems.textsecuregcm.util.AttributeValues; import org.whispersystems.textsecuregcm.util.ExceptionUtils; import org.whispersystems.textsecuregcm.util.Util; @@ -38,6 +43,7 @@ import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; import software.amazon.awssdk.services.dynamodb.model.ScanRequest; import software.amazon.awssdk.services.dynamodb.model.Update; import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; @@ -79,6 +85,10 @@ public class BackupsDb { private final SecureRandom secureRandom; + private static final String NUM_OBJECTS_SUMMARY_NAME = "numObjects"; + private static final String BYTES_USED_SUMMARY_NAME = "bytesUsed"; + private static final String BACKUPS_COUNTER_NAME = "backups"; + // The backups table // B: 16 bytes that identifies the backup @@ -217,12 +227,10 @@ public class BackupsDb { */ CompletableFuture trackMedia(final AuthenticatedBackupUser backupUser, final long mediaCountDelta, final long mediaBytesDelta) { - final Instant now = clock.instant(); return dynamoClient .updateItem( // Update the media quota and TTL UpdateBuilder.forUser(backupTableName, backupUser) - .setRefreshTimes(now) .incrementMediaBytes(mediaBytesDelta) .incrementMediaCount(mediaCountDelta) .updateItemBuilder() @@ -237,12 +245,15 @@ public class BackupsDb { * @param backupUser an already authorized backup user */ CompletableFuture ttlRefresh(final AuthenticatedBackupUser backupUser) { + final Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS); // update message backup TTL return dynamoClient.updateItem(UpdateBuilder.forUser(backupTableName, backupUser) - .setRefreshTimes(clock) + .setRefreshTimes(today) .updateItemBuilder() + .returnValues(ReturnValue.ALL_OLD) .build()) - .thenRun(Util.NOOP); + .thenAccept(updateItemResponse -> + updateMetricsAfterRefresh(backupUser, today, updateItemResponse.attributes())); } /** @@ -251,14 +262,40 @@ public class BackupsDb { * @param backupUser an already authorized backup user */ CompletableFuture addMessageBackup(final AuthenticatedBackupUser backupUser) { + final Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS); // this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp return dynamoClient.updateItem( UpdateBuilder.forUser(backupTableName, backupUser) - .setRefreshTimes(clock) + .setRefreshTimes(today) .setCdn(BACKUP_CDN) .updateItemBuilder() + .returnValues(ReturnValue.ALL_OLD) .build()) - .thenRun(Util.NOOP); + .thenAccept(updateItemResponse -> + updateMetricsAfterRefresh(backupUser, today, updateItemResponse.attributes())); + } + + private void updateMetricsAfterRefresh(final AuthenticatedBackupUser backupUser, final Instant today, final Map item) { + final Instant previousRefreshTime = Instant.ofEpochSecond( + AttributeValues.getLong(item, ATTR_LAST_REFRESH, 0L)); + // Only publish a metric update once per day + if (previousRefreshTime.isBefore(today)) { + final long mediaCount = AttributeValues.getLong(item, ATTR_MEDIA_COUNT, 0L); + final long bytesUsed = AttributeValues.getLong(item, ATTR_MEDIA_BYTES_USED, 0L); + final Tags tags = Tags.of( + UserAgentTagUtil.getPlatformTag(backupUser.userAgent()), + Tag.of("tier", backupUser.backupLevel().name())); + Metrics.summary(NUM_OBJECTS_SUMMARY_NAME, tags).record(mediaCount); + Metrics.summary(BYTES_USED_SUMMARY_NAME, tags).record(bytesUsed); + + // Report that the backup is out of quota if it cannot store a max size media object + final boolean quotaExhausted = bytesUsed >= + (BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES - BackupManager.MAX_MEDIA_OBJECT_SIZE); + + Metrics.counter(BACKUPS_COUNTER_NAME, + tags.and("quotaExhausted", String.valueOf(quotaExhausted))) + .increment(); + } } /** @@ -707,17 +744,20 @@ public class BackupsDb { }; } + UpdateBuilder setRefreshTimes(final Clock clock) { + return setRefreshTimes(clock.instant().truncatedTo(ChronoUnit.DAYS)); + } + /** * Set the lastRefresh time as part of the update *

* This always updates lastRefreshTime, and updates lastMediaRefreshTime if the backup user has the appropriate * level. */ - UpdateBuilder setRefreshTimes(final Clock clock) { - return this.setRefreshTimes(clock.instant()); - } - UpdateBuilder setRefreshTimes(final Instant refreshTime) { + if (!refreshTime.truncatedTo(ChronoUnit.DAYS).equals(refreshTime)) { + throw new IllegalArgumentException("Refresh time must be day aligned"); + } addSetExpression("#lastRefreshTime = :lastRefreshTime", Map.entry("#lastRefreshTime", ATTR_LAST_REFRESH), Map.entry(":lastRefreshTime", AttributeValues.n(refreshTime.getEpochSecond()))); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java index 4021dc802..ca4ce1727 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java @@ -5,8 +5,6 @@ package org.whispersystems.textsecuregcm.controllers; -import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; - import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.core.JsonParser; @@ -17,9 +15,6 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.annotations.VisibleForTesting; import com.google.common.net.HttpHeaders; import io.dropwizard.auth.Auth; -import io.micrometer.core.instrument.Metrics; -import io.micrometer.core.instrument.Tag; -import io.micrometer.core.instrument.Tags; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -348,6 +343,7 @@ public class ArchiveController { @ApiResponseZkAuth public CompletionStage readAuth( @ReadOnly @Auth final Optional account, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, @Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class)) @NotNull @@ -361,7 +357,7 @@ public class ArchiveController { if (account.isPresent()) { throw new BadRequestException("must not use authenticated connection for anonymous operations"); } - return backupManager.authenticateBackupUser(presentation.presentation, signature.signature) + return backupManager.authenticateBackupUser(presentation.presentation, signature.signature, userAgent) .thenApply(user -> backupManager.generateReadAuth(user, cdn)) .thenApply(ReadAuthResponse::new); } @@ -399,6 +395,7 @@ public class ArchiveController { @ApiResponseZkAuth public CompletionStage backupInfo( @ReadOnly @Auth final Optional account, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, @Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class)) @NotNull @@ -411,7 +408,7 @@ public class ArchiveController { throw new BadRequestException("must not use authenticated connection for anonymous operations"); } - return backupManager.authenticateBackupUser(presentation.presentation, signature.signature) + return backupManager.authenticateBackupUser(presentation.presentation, signature.signature, userAgent) .thenCompose(backupManager::backupInfo) .thenApply(backupInfo -> new BackupInfoResponse( backupInfo.cdn(), @@ -454,6 +451,9 @@ public class ArchiveController { @HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature, @Valid @NotNull SetPublicKeyRequest setPublicKeyRequest) { + if (account.isPresent()) { + throw new BadRequestException("must not use authenticated connection for anonymous operations"); + } return backupManager .setPublicKey(presentation.presentation, signature.signature, setPublicKeyRequest.backupIdPublicKey) .thenApply(Util.ASYNC_EMPTY_RESPONSE); @@ -481,6 +481,7 @@ public class ArchiveController { @ApiResponseZkAuth public CompletionStage backup( @ReadOnly @Auth final Optional account, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, @Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class)) @NotNull @@ -492,7 +493,7 @@ public class ArchiveController { if (account.isPresent()) { throw new BadRequestException("must not use authenticated connection for anonymous operations"); } - return backupManager.authenticateBackupUser(presentation.presentation, signature.signature) + return backupManager.authenticateBackupUser(presentation.presentation, signature.signature, userAgent) .thenCompose(backupManager::createMessageBackupUploadDescriptor) .thenApply(result -> new UploadDescriptorResponse( result.cdn(), @@ -517,6 +518,8 @@ public class ArchiveController { @ApiResponseZkAuth public CompletionStage uploadTemporaryAttachment( @ReadOnly @Auth final Optional account, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, + @Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class)) @NotNull @@ -528,7 +531,7 @@ public class ArchiveController { if (account.isPresent()) { throw new BadRequestException("must not use authenticated connection for anonymous operations"); } - return backupManager.authenticateBackupUser(presentation.presentation, signature.signature) + return backupManager.authenticateBackupUser(presentation.presentation, signature.signature, userAgent) .thenCompose(backupManager::createTemporaryAttachmentUploadDescriptor) .thenApply(result -> new UploadDescriptorResponse( result.cdn(), @@ -620,7 +623,7 @@ public class ArchiveController { } return Mono - .fromFuture(backupManager.authenticateBackupUser(presentation.presentation, signature.signature)) + .fromFuture(backupManager.authenticateBackupUser(presentation.presentation, signature.signature, userAgent)) .flatMap(backupUser -> backupManager.copyToBackup(backupUser, List.of(copyMediaRequest.toCopyParameters())) .next() .doOnNext(result -> backupMetrics.updateCopyCounter(result, UserAgentTagUtil.getPlatformTag(userAgent))) @@ -719,7 +722,7 @@ public class ArchiveController { throw new BadRequestException("must not use authenticated connection for anonymous operations"); } final Stream copyParams = copyMediaRequest.items().stream().map(CopyMediaRequest::toCopyParameters); - return Mono.fromFuture(backupManager.authenticateBackupUser(presentation.presentation, signature.signature)) + return Mono.fromFuture(backupManager.authenticateBackupUser(presentation.presentation, signature.signature, userAgent)) .flatMapMany(backupUser -> backupManager.copyToBackup(backupUser, copyParams.toList())) .doOnNext(result -> backupMetrics.updateCopyCounter(result, UserAgentTagUtil.getPlatformTag(userAgent))) .map(CopyMediaBatchResponse.Entry::fromCopyResult) @@ -741,6 +744,7 @@ public class ArchiveController { @ApiResponseZkAuth public CompletionStage refresh( @ReadOnly @Auth final Optional account, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, @Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class)) @NotNull @@ -753,7 +757,7 @@ public class ArchiveController { throw new BadRequestException("must not use authenticated connection for anonymous operations"); } return backupManager - .authenticateBackupUser(presentation.presentation, signature.signature) + .authenticateBackupUser(presentation.presentation, signature.signature, userAgent) .thenCompose(backupManager::ttlRefresh) .thenApply(Util.ASYNC_EMPTY_RESPONSE); } @@ -807,6 +811,7 @@ public class ArchiveController { @ApiResponseZkAuth public CompletionStage listMedia( @ReadOnly @Auth final Optional account, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, @Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class)) @NotNull @@ -825,7 +830,7 @@ public class ArchiveController { throw new BadRequestException("must not use authenticated connection for anonymous operations"); } return backupManager - .authenticateBackupUser(presentation.presentation, signature.signature) + .authenticateBackupUser(presentation.presentation, signature.signature, userAgent) .thenCompose(backupUser -> backupManager.list(backupUser, cursor, limit.orElse(1000)) .thenApply(result -> new ListResponse( result.media() @@ -862,6 +867,7 @@ public class ArchiveController { @ApiResponseZkAuth public CompletionStage deleteMedia( @ReadOnly @Auth final Optional account, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, @Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class)) @NotNull @@ -881,7 +887,7 @@ public class ArchiveController { .toList(); return backupManager - .authenticateBackupUser(presentation.presentation, signature.signature) + .authenticateBackupUser(presentation.presentation, signature.signature, userAgent) .thenCompose(authenticatedBackupUser -> backupManager .deleteMedia(authenticatedBackupUser, toDelete) .then().toFuture()) @@ -898,6 +904,7 @@ public class ArchiveController { @ApiResponseZkAuth public CompletionStage deleteBackup( @ReadOnly @Auth final Optional account, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, @Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class)) @NotNull @@ -910,7 +917,7 @@ public class ArchiveController { throw new BadRequestException("must not use authenticated connection for anonymous operations"); } return backupManager - .authenticateBackupUser(presentation.presentation, signature.signature) + .authenticateBackupUser(presentation.presentation, signature.signature, userAgent) .thenCompose(backupManager::deleteEntireBackup) .thenApply(Util.ASYNC_EMPTY_RESPONSE); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcService.java index 737aaf1c1..4666265fe 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcService.java @@ -206,7 +206,8 @@ public class BackupsAnonymousGrpcService extends ReactorBackupsAnonymousGrpc.Bac try { return backupManager.authenticateBackupUser( new BackupAuthCredentialPresentation(signedPresentation.getPresentation().toByteArray()), - signedPresentation.getPresentationSignature().toByteArray()); + signedPresentation.getPresentationSignature().toByteArray(), + RequestAttributesUtil.getUserAgent().orElse(null)); } catch (InvalidInputException e) { throw Status.UNAUTHENTICATED.withDescription("Could not deserialize presentation").asRuntimeException(); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/BackupMetricsCommand.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/BackupMetricsCommand.java index 3a989be86..a2d2d912e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/BackupMetricsCommand.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/BackupMetricsCommand.java @@ -11,7 +11,6 @@ import io.micrometer.core.instrument.DistributionSummary; import io.micrometer.core.instrument.Metrics; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.signal.libsignal.zkgroup.backups.BackupLevel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.WhisperServerConfiguration; @@ -64,43 +63,17 @@ public class BackupMetricsCommand extends AbstractCommandWithDependencies { segments, Runtime.getRuntime().availableProcessors()); - final DistributionSummary numObjectsMediaTier = Metrics.summary(name(getClass(), "numObjects"), - "tier", BackupLevel.PAID.name()); - final DistributionSummary bytesUsedMediaTier = Metrics.summary(name(getClass(), "bytesUsed"), - "tier", BackupLevel.PAID.name()); - final DistributionSummary numObjectsMessagesTier = Metrics.summary(name(getClass(), "numObjects"), - "tier", BackupLevel.FREE.name()); - final DistributionSummary bytesUsedMessagesTier = Metrics.summary(name(getClass(), "bytesUsed"), - "tier", BackupLevel.FREE.name()); - final DistributionSummary timeSinceLastRefresh = Metrics.summary(name(getClass(), "timeSinceLastRefresh")); final DistributionSummary timeSinceLastMediaRefresh = Metrics.summary(name(getClass(), "timeSinceLastMediaRefresh")); - final String backupsCounterName = name(getClass(), "backups"); final BackupManager backupManager = commandDependencies.backupManager(); final Long backupsExpired = backupManager .listBackupAttributes(segments, Schedulers.parallel()) .doOnNext(backupMetadata -> { - final boolean subscribed = backupMetadata.lastMediaRefresh().equals(backupMetadata.lastRefresh()); - if (subscribed) { - numObjectsMediaTier.record(backupMetadata.numObjects()); - bytesUsedMediaTier.record(backupMetadata.bytesUsed()); - } else { - numObjectsMessagesTier.record(backupMetadata.numObjects()); - bytesUsedMessagesTier.record(backupMetadata.bytesUsed()); - } timeSinceLastRefresh.record(timeSince(backupMetadata.lastRefresh()).getSeconds()); timeSinceLastMediaRefresh.record(timeSince(backupMetadata.lastMediaRefresh()).getSeconds()); - - // Report that the backup is out of quota if it cannot store a max size media object - final boolean quotaExhausted = backupMetadata.bytesUsed() >= - (BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES - BackupManager.MAX_MEDIA_OBJECT_SIZE); - - Metrics.counter(backupsCounterName, - "subscribed", String.valueOf(subscribed), - "quotaExhausted", String.valueOf(quotaExhausted)).increment(); }) .count() .block(); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java index 5454a5cef..69263a35c 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java @@ -30,6 +30,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -261,7 +262,7 @@ public class BackupManagerTest { final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, backupLevel); final Instant tstart = Instant.ofEpochSecond(1).plus(Duration.ofDays(1)); - final Instant tnext = tstart.plus(Duration.ofSeconds(1)); + final Instant tnext = tstart.plus(Duration.ofDays(1)); // create backup at t=tstart testClock.pin(tstart); @@ -272,8 +273,8 @@ public class BackupManagerTest { backupManager.ttlRefresh(backupUser).join(); checkExpectedExpirations( - tnext, - backupLevel == BackupLevel.PAID ? tnext : null, + tnext.truncatedTo(ChronoUnit.DAYS), + backupLevel == BackupLevel.PAID ? tnext.truncatedTo(ChronoUnit.DAYS) : null, backupUser); } @@ -281,7 +282,7 @@ public class BackupManagerTest { @EnumSource public void createBackupRefreshesTtl(final BackupLevel backupLevel) { final Instant tstart = Instant.ofEpochSecond(1).plus(Duration.ofDays(1)); - final Instant tnext = tstart.plus(Duration.ofSeconds(1)); + final Instant tnext = tstart.plus(Duration.ofDays(1)); final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, backupLevel); @@ -294,8 +295,8 @@ public class BackupManagerTest { backupManager.createMessageBackupUploadDescriptor(backupUser).join(); checkExpectedExpirations( - tnext, - backupLevel == BackupLevel.PAID ? tnext : null, + tnext.truncatedTo(ChronoUnit.DAYS), + backupLevel == BackupLevel.PAID ? tnext.truncatedTo(ChronoUnit.DAYS) : null, backupUser); } @@ -311,7 +312,8 @@ public class BackupManagerTest { assertThatExceptionOfType(StatusRuntimeException.class) .isThrownBy(() -> backupManager.authenticateBackupUser( invalidPresentation, - keyPair.getPrivateKey().calculateSignature(invalidPresentation.serialize()))) + keyPair.getPrivateKey().calculateSignature(invalidPresentation.serialize()), + null)) .extracting(StatusRuntimeException::getStatus) .extracting(Status::getCode) .isEqualTo(Status.UNAUTHENTICATED.getCode()); @@ -335,7 +337,8 @@ public class BackupManagerTest { assertThatExceptionOfType(StatusRuntimeException.class) .isThrownBy(() -> backupManager.authenticateBackupUser( invalidPresentation, - keyPair.getPrivateKey().calculateSignature(invalidPresentation.serialize()))) + keyPair.getPrivateKey().calculateSignature(invalidPresentation.serialize()), + null)) .extracting(StatusRuntimeException::getStatus) .extracting(Status::getCode) .isEqualTo(Status.UNAUTHENTICATED.getCode()); @@ -352,7 +355,7 @@ public class BackupManagerTest { // haven't set a public key yet assertThat(CompletableFutureTestUtil.assertFailsWithCause( StatusRuntimeException.class, - backupManager.authenticateBackupUser(presentation, signature)) + backupManager.authenticateBackupUser(presentation, signature, null)) .getStatus().getCode()) .isEqualTo(Status.UNAUTHENTICATED.getCode()); } @@ -403,12 +406,12 @@ public class BackupManagerTest { // shouldn't be able to authenticate with an invalid signature assertThat(CompletableFutureTestUtil.assertFailsWithCause( StatusRuntimeException.class, - backupManager.authenticateBackupUser(presentation, wrongSignature)) + backupManager.authenticateBackupUser(presentation, wrongSignature, null)) .getStatus().getCode()) .isEqualTo(Status.UNAUTHENTICATED.getCode()); // correct signature - final AuthenticatedBackupUser user = backupManager.authenticateBackupUser(presentation, signature).join(); + final AuthenticatedBackupUser user = backupManager.authenticateBackupUser(presentation, signature, null).join(); assertThat(user.backupId()).isEqualTo(presentation.getBackupId()); assertThat(user.backupLevel()).isEqualTo(BackupLevel.FREE); } @@ -426,16 +429,16 @@ public class BackupManagerTest { // should be accepted the day before to forgive clock skew testClock.pin(Instant.ofEpochSecond(1)); - assertThatNoException().isThrownBy(() -> backupManager.authenticateBackupUser(oldCredential, signature).join()); + assertThatNoException().isThrownBy(() -> backupManager.authenticateBackupUser(oldCredential, signature, null).join()); // should be accepted the day after to forgive clock skew testClock.pin(Instant.ofEpochSecond(1).plus(Duration.ofDays(2))); - assertThatNoException().isThrownBy(() -> backupManager.authenticateBackupUser(oldCredential, signature).join()); + assertThatNoException().isThrownBy(() -> backupManager.authenticateBackupUser(oldCredential, signature, null).join()); // should be rejected the day after that testClock.pin(Instant.ofEpochSecond(1).plus(Duration.ofDays(3))); assertThatExceptionOfType(StatusRuntimeException.class) - .isThrownBy(() -> backupManager.authenticateBackupUser(oldCredential, signature)) + .isThrownBy(() -> backupManager.authenticateBackupUser(oldCredential, signature, null)) .extracting(StatusRuntimeException::getStatus) .extracting(Status::getCode) .isEqualTo(Status.UNAUTHENTICATED.getCode()); @@ -856,7 +859,7 @@ public class BackupManagerTest { .mapToObj(i -> backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID)) .toList(); for (int i = 0; i < backupUsers.size(); i++) { - testClock.pin(Instant.ofEpochSecond(i)); + testClock.pin(days(i)); backupManager.createMessageBackupUploadDescriptor(backupUsers.get(i)).join(); } @@ -864,11 +867,12 @@ public class BackupManagerTest { final Set expectedHashes = new HashSet<>(); for (int i = 0; i < backupUsers.size(); i++) { - testClock.pin(Instant.ofEpochSecond(i)); + final Instant day = days(i); + testClock.pin(day); // get backups expired at t=i final List expired = backupManager - .getExpiredBackups(1, Schedulers.immediate(), Instant.ofEpochSecond(i)) + .getExpiredBackups(1, Schedulers.immediate(), day) .collectList() .block(); @@ -890,24 +894,24 @@ public class BackupManagerTest { final byte[] backupId = TestRandomUtil.nextBytes(16); // refreshed media timestamp at t=5 - testClock.pin(Instant.ofEpochSecond(5)); + testClock.pin(days(5)); backupManager.createMessageBackupUploadDescriptor(backupUser(backupId, BackupCredentialType.MESSAGES, BackupLevel.PAID)).join(); // refreshed messages timestamp at t=6 - testClock.pin(Instant.ofEpochSecond(6)); + testClock.pin(days(6)); backupManager.createMessageBackupUploadDescriptor(backupUser(backupId, BackupCredentialType.MESSAGES, BackupLevel.FREE)).join(); Function> getExpired = time -> backupManager .getExpiredBackups(1, Schedulers.immediate(), time) .collectList().block(); - assertThat(getExpired.apply(Instant.ofEpochSecond(5))).isEmpty(); + assertThat(getExpired.apply(days(5))).isEmpty(); - assertThat(getExpired.apply(Instant.ofEpochSecond(6))) + assertThat(getExpired.apply(days(6))) .hasSize(1).first() .matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.MEDIA, "is media tier"); - assertThat(getExpired.apply(Instant.ofEpochSecond(7))) + assertThat(getExpired.apply(days(7))) .hasSize(1).first() .matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.ALL, "is messages tier"); } @@ -1075,6 +1079,10 @@ public class BackupManagerTest { */ private AuthenticatedBackupUser retrieveBackupUser(final byte[] backupId, final BackupCredentialType credentialType, final BackupLevel backupLevel) { final BackupsDb.AuthenticationData authData = backupsDb.retrieveAuthenticationData(backupId).join().get(); - return new AuthenticatedBackupUser(backupId, credentialType, backupLevel, authData.backupDir(), authData.mediaDir()); + return new AuthenticatedBackupUser(backupId, credentialType, backupLevel, authData.backupDir(), authData.mediaDir(), null); + } + + private static Instant days(int n) { + return Instant.EPOCH.plus(Duration.ofDays(n)); } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java index ab7dd1b25..299f778c7 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java @@ -10,12 +10,16 @@ import static org.assertj.core.api.Assertions.assertThat; import io.grpc.Status; import io.grpc.StatusRuntimeException; +import java.time.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Stream; +import org.assertj.core.util.Streams; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -89,13 +93,13 @@ public class BackupsDbTest { @Test public void expirationDetectedOnce() { final byte[] backupId = TestRandomUtil.nextBytes(16); - // Refresh media/messages at t=0 - testClock.pin(Instant.ofEpochSecond(0L)); + // Refresh media/messages at t=0D + testClock.pin(days(0)); backupsDb.setPublicKey(backupId, BackupLevel.PAID, Curve.generateKeyPair().getPublicKey()).join(); this.backupsDb.ttlRefresh(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.PAID)).join(); - // refresh only messages at t=2 - testClock.pin(Instant.ofEpochSecond(2L)); + // refresh only messages on t=2D + testClock.pin(days(2).plus(Duration.ofSeconds(123))); this.backupsDb.ttlRefresh(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.FREE)).join(); final Function> expiredBackups = purgeTime -> backupsDb @@ -103,7 +107,8 @@ public class BackupsDbTest { .collectList() .block(); - List expired = expiredBackups.apply(Instant.ofEpochSecond(1)); + // the media should be expired at t=1D + List expired = expiredBackups.apply(days(1)); assertThat(expired).hasSize(1).first() .matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.MEDIA); @@ -111,11 +116,11 @@ public class BackupsDbTest { backupsDb.startExpiration(expired.getFirst()).join(); backupsDb.finishExpiration(expired.getFirst()).join(); - // should be nothing to expire at t=1 - assertThat(expiredBackups.apply(Instant.ofEpochSecond(1))).isEmpty(); + // should be nothing left to expire at t=1D + assertThat(expiredBackups.apply(days(1))).isEmpty(); - // at t=3, should now expire messages as well - expired = expiredBackups.apply(Instant.ofEpochSecond(3)); + // at t=3D, should now expire messages as well + expired = expiredBackups.apply(days(3)); assertThat(expired).hasSize(1).first() .matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.ALL); @@ -124,21 +129,21 @@ public class BackupsDbTest { backupsDb.finishExpiration(expired.getFirst()).join(); // should be nothing to expire at t=3 - assertThat(expiredBackups.apply(Instant.ofEpochSecond(3))).isEmpty(); + assertThat(expiredBackups.apply(days(3))).isEmpty(); } @ParameterizedTest @EnumSource(names = {"MEDIA", "ALL"}) public void expirationFailed(ExpiredBackup.ExpirationType expirationType) { final byte[] backupId = TestRandomUtil.nextBytes(16); - // Refresh media/messages at t=0 - testClock.pin(Instant.ofEpochSecond(0L)); + // Refresh media/messages at t=0D + testClock.pin(days(0)); backupsDb.setPublicKey(backupId, BackupLevel.PAID, Curve.generateKeyPair().getPublicKey()).join(); this.backupsDb.ttlRefresh(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.PAID)).join(); if (expirationType == ExpiredBackup.ExpirationType.MEDIA) { - // refresh only messages at t=2 so that we only expire media at t=1 - testClock.pin(Instant.ofEpochSecond(2L)); + // refresh only messages at t=2D so that we only expire media at t=1D + testClock.pin(days(2)); this.backupsDb.ttlRefresh(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.FREE)).join(); } @@ -155,7 +160,7 @@ public class BackupsDbTest { final String originalBackupDir = info.backupDir(); final String originalMediaDir = info.mediaDir(); - ExpiredBackup expired = expiredBackups.apply(Instant.ofEpochSecond(1)).get(); + ExpiredBackup expired = expiredBackups.apply(days(1)).get(); assertThat(expired).matches(eb -> eb.expirationType() == expirationType); // expire but fail (don't call finishExpiration) @@ -179,7 +184,7 @@ public class BackupsDbTest { final String expiredPrefix = expired.prefixToDelete(); // We failed, so we should see the same prefix on the next expiration listing - expired = expiredBackups.apply(Instant.ofEpochSecond(1)).get(); + expired = expiredBackups.apply(days(1)).get(); assertThat(expired).matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.GARBAGE_COLLECTION, "Expiration should be garbage collection "); assertThat(expired.prefixToDelete()).isEqualTo(expiredPrefix); @@ -188,7 +193,7 @@ public class BackupsDbTest { // Successfully finish the expiration backupsDb.finishExpiration(expired).join(); - Optional opt = expiredBackups.apply(Instant.ofEpochSecond(1)); + Optional opt = expiredBackups.apply(days(1)); if (expirationType == ExpiredBackup.ExpirationType.MEDIA) { // should be nothing to expire at t=1 assertThat(opt).isEmpty(); @@ -212,19 +217,24 @@ public class BackupsDbTest { @Test public void list() { - final AuthenticatedBackupUser u1 = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.FREE); - final AuthenticatedBackupUser u2 = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID); - final AuthenticatedBackupUser u3 = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID); + final List users = List.of( + backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.FREE), + backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID), + backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID)); + + final List lastRefreshTimes = List.of( + days(1).plus(Duration.ofSeconds(12)), + days(2).plus(Duration.ofSeconds(34)), + days(3).plus(Duration.ofSeconds(56))); // add at least one message backup, so we can describe it - testClock.pin(Instant.ofEpochSecond(10)); - Stream.of(u1, u2, u3).forEach(u -> backupsDb.addMessageBackup(u).join()); + for (int i = 0; i < users.size(); i++) { + testClock.pin(lastRefreshTimes.get(i)); + backupsDb.addMessageBackup(users.get(i)).join(); + } - testClock.pin(Instant.ofEpochSecond(20)); - backupsDb.trackMedia(u2, 10, 100).join(); - - testClock.pin(Instant.ofEpochSecond(30)); - backupsDb.trackMedia(u3, 1, 1000).join(); + backupsDb.trackMedia(users.get(1), 10, 100).join(); + backupsDb.trackMedia(users.get(2), 1, 1000).join(); final List sbms = backupsDb.listBackupAttributes(1, Schedulers.immediate()) .sort(Comparator.comparing(StoredBackupAttributes::lastRefresh)) @@ -234,22 +244,28 @@ public class BackupsDbTest { final StoredBackupAttributes sbm1 = sbms.get(0); assertThat(sbm1.bytesUsed()).isEqualTo(0); assertThat(sbm1.numObjects()).isEqualTo(0); - assertThat(sbm1.lastRefresh()).isEqualTo(Instant.ofEpochSecond(10)); + assertThat(sbm1.lastRefresh()).isEqualTo(lastRefreshTimes.get(0).truncatedTo(ChronoUnit.DAYS)); assertThat(sbm1.lastMediaRefresh()).isEqualTo(Instant.EPOCH); final StoredBackupAttributes sbm2 = sbms.get(1); assertThat(sbm2.bytesUsed()).isEqualTo(100); assertThat(sbm2.numObjects()).isEqualTo(10); - assertThat(sbm2.lastRefresh()).isEqualTo(sbm2.lastMediaRefresh()).isEqualTo(Instant.ofEpochSecond(20)); + assertThat(sbm2.lastRefresh()).isEqualTo(lastRefreshTimes.get(1).truncatedTo(ChronoUnit.DAYS)); + assertThat(sbm2.lastMediaRefresh()).isEqualTo(lastRefreshTimes.get(1).truncatedTo(ChronoUnit.DAYS)); final StoredBackupAttributes sbm3 = sbms.get(2); assertThat(sbm3.bytesUsed()).isEqualTo(1000); assertThat(sbm3.numObjects()).isEqualTo(1); - assertThat(sbm3.lastRefresh()).isEqualTo(sbm3.lastMediaRefresh()).isEqualTo(Instant.ofEpochSecond(30)); + assertThat(sbm3.lastRefresh()).isEqualTo(lastRefreshTimes.get(2).truncatedTo(ChronoUnit.DAYS)); + assertThat(sbm3.lastMediaRefresh()).isEqualTo(lastRefreshTimes.get(2).truncatedTo(ChronoUnit.DAYS)); + } + + private static Instant days(int n) { + return Instant.EPOCH.plus(Duration.ofDays(n)); } private AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupCredentialType credentialType, final BackupLevel backupLevel) { - return new AuthenticatedBackupUser(backupId, credentialType, backupLevel, "myBackupDir", "myMediaDir"); + return new AuthenticatedBackupUser(backupId, credentialType, backupLevel, "myBackupDir", "myMediaDir", null); } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java index 660b7ec5c..be34a1f5e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java @@ -156,7 +156,7 @@ public class ArchiveControllerTest { } @Test - public void setBackupId() throws RateLimitExceededException { + public void setBackupId() { when(backupAuthManager.commitBackupId(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null)); final Response response = resources.getJerseyTest() @@ -356,7 +356,7 @@ public class ArchiveControllerTest { public void getBackupInfo() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( BackupLevel.PAID, messagesBackupKey, aci); - when(backupManager.authenticateBackupUser(any(), any())) + when(backupManager.authenticateBackupUser(any(), any(), any())) .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID))); when(backupManager.backupInfo(any())).thenReturn(CompletableFuture.completedFuture(new BackupManager.BackupInfo( 1, "myBackupDir", "myMediaDir", "filename", Optional.empty()))); @@ -376,7 +376,7 @@ public class ArchiveControllerTest { public void putMediaBatchSuccess() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( BackupLevel.PAID, messagesBackupKey, aci); - when(backupManager.authenticateBackupUser(any(), any())) + when(backupManager.authenticateBackupUser(any(), any(), any())) .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID))); final byte[][] mediaIds = new byte[][]{TestRandomUtil.nextBytes(15), TestRandomUtil.nextBytes(15)}; when(backupManager.copyToBackup(any(), any())) @@ -421,7 +421,7 @@ public class ArchiveControllerTest { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( BackupLevel.PAID, messagesBackupKey, aci); - when(backupManager.authenticateBackupUser(any(), any())) + when(backupManager.authenticateBackupUser(any(), any(), any())) .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID))); final byte[][] mediaIds = IntStream.range(0, 4).mapToObj(i -> TestRandomUtil.nextBytes(15)).toArray(byte[][]::new); @@ -479,7 +479,7 @@ public class ArchiveControllerTest { public void copyMediaWithNegativeLength() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( BackupLevel.PAID, messagesBackupKey, aci); - when(backupManager.authenticateBackupUser(any(), any())) + when(backupManager.authenticateBackupUser(any(), any(), any())) .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID))); final byte[][] mediaIds = new byte[][]{TestRandomUtil.nextBytes(15), TestRandomUtil.nextBytes(15)}; final Response r = resources.getJerseyTest() @@ -512,7 +512,7 @@ public class ArchiveControllerTest { throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( BackupLevel.PAID, messagesBackupKey, aci); - when(backupManager.authenticateBackupUser(any(), any())) + when(backupManager.authenticateBackupUser(any(), any(), any())) .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID))); final byte[] mediaId = TestRandomUtil.nextBytes(15); @@ -547,7 +547,7 @@ public class ArchiveControllerTest { public void delete() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci); - when(backupManager.authenticateBackupUser(any(), any())) + when(backupManager.authenticateBackupUser(any(), any(), any())) .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID))); final ArchiveController.DeleteMedia deleteRequest = new ArchiveController.DeleteMedia( @@ -573,7 +573,7 @@ public class ArchiveControllerTest { public void mediaUploadForm() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci); - when(backupManager.authenticateBackupUser(any(), any())) + when(backupManager.authenticateBackupUser(any(), any(), any())) .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID))); when(backupManager.createTemporaryAttachmentUploadDescriptor(any())) .thenReturn(CompletableFuture.completedFuture( @@ -605,7 +605,7 @@ public class ArchiveControllerTest { public void readAuth() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci); - when(backupManager.authenticateBackupUser(any(), any())) + when(backupManager.authenticateBackupUser(any(), any(), any())) .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID))); when(backupManager.generateReadAuth(any(), eq(3))).thenReturn(Map.of("key", "value")); final ArchiveController.ReadAuthResponse response = resources.getJerseyTest() @@ -644,7 +644,7 @@ public class ArchiveControllerTest { public void deleteEntireBackup() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci); - when(backupManager.authenticateBackupUser(any(), any())) + when(backupManager.authenticateBackupUser(any(), any(), any())) .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID))); when(backupManager.deleteEntireBackup(any())).thenReturn(CompletableFuture.completedFuture(null)); Response response = resources.getJerseyTest() @@ -660,7 +660,7 @@ public class ArchiveControllerTest { public void invalidSourceAttachmentKey() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( BackupLevel.PAID, messagesBackupKey, aci); - when(backupManager.authenticateBackupUser(any(), any())) + when(backupManager.authenticateBackupUser(any(), any(), any())) .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID))); final Response r = resources.getJerseyTest() .target("v1/archives/media") @@ -677,6 +677,6 @@ public class ArchiveControllerTest { } private static AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupCredentialType credentialType, final BackupLevel backupLevel) { - return new AuthenticatedBackupUser(backupId, credentialType, backupLevel, "myBackupDir", "myMediaDir"); + return new AuthenticatedBackupUser(backupId, credentialType, backupLevel, "myBackupDir", "myMediaDir", null); } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcServiceTest.java index 4297201cb..f925c0607 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcServiceTest.java @@ -80,7 +80,7 @@ class BackupsAnonymousGrpcServiceTest extends @BeforeEach void setup() { - when(backupManager.authenticateBackupUser(any(), any())) + when(backupManager.authenticateBackupUser(any(), any(), any())) .thenReturn(CompletableFuture.completedFuture( backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID))); } @@ -308,7 +308,7 @@ class BackupsAnonymousGrpcServiceTest extends private static AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupCredentialType credentialType, final BackupLevel backupLevel) { - return new AuthenticatedBackupUser(backupId, credentialType, backupLevel, "myBackupDir", "myMediaDir"); + return new AuthenticatedBackupUser(backupId, credentialType, backupLevel, "myBackupDir", "myMediaDir", null); } private static BackupAuthCredentialPresentation presentation(BackupAuthTestUtil backupAuthTestUtil,