Track backup metrics on refreshes
This commit is contained in:
parent
030d8e8dd4
commit
4dc3b19d2a
|
@ -7,10 +7,14 @@ package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
import org.signal.libsignal.zkgroup.backups.BackupCredentialType;
|
import org.signal.libsignal.zkgroup.backups.BackupCredentialType;
|
||||||
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
public record AuthenticatedBackupUser(byte[] backupId,
|
public record AuthenticatedBackupUser(
|
||||||
|
byte[] backupId,
|
||||||
BackupCredentialType credentialType,
|
BackupCredentialType credentialType,
|
||||||
BackupLevel backupLevel,
|
BackupLevel backupLevel,
|
||||||
String backupDir,
|
String backupDir,
|
||||||
String mediaDir) {
|
String mediaDir,
|
||||||
|
@Nullable UserAgent userAgent) {
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import io.dropwizard.util.DataSize;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import io.micrometer.core.instrument.DistributionSummary;
|
import io.micrometer.core.instrument.DistributionSummary;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
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 io.micrometer.core.instrument.Timer;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
|
@ -36,9 +38,13 @@ import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.AsyncTimerUtil;
|
import org.whispersystems.textsecuregcm.util.AsyncTimerUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||||
import org.whispersystems.textsecuregcm.util.Pair;
|
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.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Scheduler;
|
import reactor.core.scheduler.Scheduler;
|
||||||
|
@ -316,7 +322,9 @@ public class BackupManager {
|
||||||
.thenApply(ignored -> usage))
|
.thenApply(ignored -> usage))
|
||||||
.whenComplete((newUsage, throwable) -> {
|
.whenComplete((newUsage, throwable) -> {
|
||||||
boolean usageChanged = throwable == null && !newUsage.equals(info.usageInfo());
|
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();
|
.increment();
|
||||||
})
|
})
|
||||||
.thenApply(newUsage -> MAX_TOTAL_BACKUP_MEDIA_BYTES - newUsage.bytesUsed());
|
.thenApply(newUsage -> MAX_TOTAL_BACKUP_MEDIA_BYTES - newUsage.bytesUsed());
|
||||||
|
@ -520,7 +528,8 @@ public class BackupManager {
|
||||||
*/
|
*/
|
||||||
public CompletableFuture<AuthenticatedBackupUser> authenticateBackupUser(
|
public CompletableFuture<AuthenticatedBackupUser> authenticateBackupUser(
|
||||||
final BackupAuthCredentialPresentation presentation,
|
final BackupAuthCredentialPresentation presentation,
|
||||||
final byte[] signature) {
|
final byte[] signature,
|
||||||
|
final String userAgentString) {
|
||||||
final PresentationSignatureVerifier signatureVerifier = verifyPresentation(presentation);
|
final PresentationSignatureVerifier signatureVerifier = verifyPresentation(presentation);
|
||||||
return backupsDb
|
return backupsDb
|
||||||
.retrieveAuthenticationData(presentation.getBackupId())
|
.retrieveAuthenticationData(presentation.getBackupId())
|
||||||
|
@ -538,12 +547,20 @@ public class BackupManager {
|
||||||
final Pair<BackupCredentialType, BackupLevel> credentialTypeAndBackupLevel =
|
final Pair<BackupCredentialType, BackupLevel> credentialTypeAndBackupLevel =
|
||||||
signatureVerifier.verifySignature(signature, authenticationData.publicKey());
|
signatureVerifier.verifySignature(signature, authenticationData.publicKey());
|
||||||
|
|
||||||
|
UserAgent userAgent;
|
||||||
|
try {
|
||||||
|
userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
|
||||||
|
} catch (UnrecognizedUserAgentException e) {
|
||||||
|
userAgent = null;
|
||||||
|
}
|
||||||
|
|
||||||
return new AuthenticatedBackupUser(
|
return new AuthenticatedBackupUser(
|
||||||
presentation.getBackupId(),
|
presentation.getBackupId(),
|
||||||
credentialTypeAndBackupLevel.first(),
|
credentialTypeAndBackupLevel.first(),
|
||||||
credentialTypeAndBackupLevel.second(),
|
credentialTypeAndBackupLevel.second(),
|
||||||
authenticationData.backupDir(),
|
authenticationData.backupDir(),
|
||||||
authenticationData.mediaDir());
|
authenticationData.mediaDir(),
|
||||||
|
userAgent);
|
||||||
})
|
})
|
||||||
.thenApply(result -> {
|
.thenApply(result -> {
|
||||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME, SUCCESS_TAG_NAME, String.valueOf(true)).increment();
|
Metrics.counter(ZK_AUTHN_COUNTER_NAME, SUCCESS_TAG_NAME, String.valueOf(true)).increment();
|
||||||
|
@ -673,8 +690,9 @@ public class BackupManager {
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static void checkBackupLevel(final AuthenticatedBackupUser backupUser, final BackupLevel backupLevel) {
|
static void checkBackupLevel(final AuthenticatedBackupUser backupUser, final BackupLevel backupLevel) {
|
||||||
if (backupUser.backupLevel().compareTo(backupLevel) < 0) {
|
if (backupUser.backupLevel().compareTo(backupLevel) < 0) {
|
||||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME,
|
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME, Tags.of(
|
||||||
FAILURE_REASON_TAG_NAME, "level")
|
UserAgentTagUtil.getPlatformTag(backupUser.userAgent()),
|
||||||
|
Tag.of(FAILURE_REASON_TAG_NAME, "level")))
|
||||||
.increment();
|
.increment();
|
||||||
|
|
||||||
throw Status.PERMISSION_DENIED
|
throw Status.PERMISSION_DENIED
|
||||||
|
|
|
@ -11,6 +11,7 @@ import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
@ -21,12 +22,16 @@ import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.function.Predicate;
|
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.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
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.ConditionalCheckFailedException;
|
||||||
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
|
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
|
||||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
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.ScanRequest;
|
||||||
import software.amazon.awssdk.services.dynamodb.model.Update;
|
import software.amazon.awssdk.services.dynamodb.model.Update;
|
||||||
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
||||||
|
@ -79,6 +85,10 @@ public class BackupsDb {
|
||||||
|
|
||||||
private final SecureRandom secureRandom;
|
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
|
// The backups table
|
||||||
|
|
||||||
// B: 16 bytes that identifies the backup
|
// B: 16 bytes that identifies the backup
|
||||||
|
@ -217,12 +227,10 @@ public class BackupsDb {
|
||||||
*/
|
*/
|
||||||
CompletableFuture<Void> trackMedia(final AuthenticatedBackupUser backupUser, final long mediaCountDelta,
|
CompletableFuture<Void> trackMedia(final AuthenticatedBackupUser backupUser, final long mediaCountDelta,
|
||||||
final long mediaBytesDelta) {
|
final long mediaBytesDelta) {
|
||||||
final Instant now = clock.instant();
|
|
||||||
return dynamoClient
|
return dynamoClient
|
||||||
.updateItem(
|
.updateItem(
|
||||||
// Update the media quota and TTL
|
// Update the media quota and TTL
|
||||||
UpdateBuilder.forUser(backupTableName, backupUser)
|
UpdateBuilder.forUser(backupTableName, backupUser)
|
||||||
.setRefreshTimes(now)
|
|
||||||
.incrementMediaBytes(mediaBytesDelta)
|
.incrementMediaBytes(mediaBytesDelta)
|
||||||
.incrementMediaCount(mediaCountDelta)
|
.incrementMediaCount(mediaCountDelta)
|
||||||
.updateItemBuilder()
|
.updateItemBuilder()
|
||||||
|
@ -237,12 +245,15 @@ public class BackupsDb {
|
||||||
* @param backupUser an already authorized backup user
|
* @param backupUser an already authorized backup user
|
||||||
*/
|
*/
|
||||||
CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
|
CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
|
||||||
|
final Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS);
|
||||||
// update message backup TTL
|
// update message backup TTL
|
||||||
return dynamoClient.updateItem(UpdateBuilder.forUser(backupTableName, backupUser)
|
return dynamoClient.updateItem(UpdateBuilder.forUser(backupTableName, backupUser)
|
||||||
.setRefreshTimes(clock)
|
.setRefreshTimes(today)
|
||||||
.updateItemBuilder()
|
.updateItemBuilder()
|
||||||
|
.returnValues(ReturnValue.ALL_OLD)
|
||||||
.build())
|
.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
|
* @param backupUser an already authorized backup user
|
||||||
*/
|
*/
|
||||||
CompletableFuture<Void> addMessageBackup(final AuthenticatedBackupUser backupUser) {
|
CompletableFuture<Void> 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
|
// this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
|
||||||
return dynamoClient.updateItem(
|
return dynamoClient.updateItem(
|
||||||
UpdateBuilder.forUser(backupTableName, backupUser)
|
UpdateBuilder.forUser(backupTableName, backupUser)
|
||||||
.setRefreshTimes(clock)
|
.setRefreshTimes(today)
|
||||||
.setCdn(BACKUP_CDN)
|
.setCdn(BACKUP_CDN)
|
||||||
.updateItemBuilder()
|
.updateItemBuilder()
|
||||||
|
.returnValues(ReturnValue.ALL_OLD)
|
||||||
.build())
|
.build())
|
||||||
.thenRun(Util.NOOP);
|
.thenAccept(updateItemResponse ->
|
||||||
|
updateMetricsAfterRefresh(backupUser, today, updateItemResponse.attributes()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMetricsAfterRefresh(final AuthenticatedBackupUser backupUser, final Instant today, final Map<String, AttributeValue> 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
|
* Set the lastRefresh time as part of the update
|
||||||
* <p>
|
* <p>
|
||||||
* This always updates lastRefreshTime, and updates lastMediaRefreshTime if the backup user has the appropriate
|
* This always updates lastRefreshTime, and updates lastMediaRefreshTime if the backup user has the appropriate
|
||||||
* level.
|
* level.
|
||||||
*/
|
*/
|
||||||
UpdateBuilder setRefreshTimes(final Clock clock) {
|
|
||||||
return this.setRefreshTimes(clock.instant());
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateBuilder setRefreshTimes(final Instant refreshTime) {
|
UpdateBuilder setRefreshTimes(final Instant refreshTime) {
|
||||||
|
if (!refreshTime.truncatedTo(ChronoUnit.DAYS).equals(refreshTime)) {
|
||||||
|
throw new IllegalArgumentException("Refresh time must be day aligned");
|
||||||
|
}
|
||||||
addSetExpression("#lastRefreshTime = :lastRefreshTime",
|
addSetExpression("#lastRefreshTime = :lastRefreshTime",
|
||||||
Map.entry("#lastRefreshTime", ATTR_LAST_REFRESH),
|
Map.entry("#lastRefreshTime", ATTR_LAST_REFRESH),
|
||||||
Map.entry(":lastRefreshTime", AttributeValues.n(refreshTime.getEpochSecond())));
|
Map.entry(":lastRefreshTime", AttributeValues.n(refreshTime.getEpochSecond())));
|
||||||
|
|
|
@ -5,8 +5,6 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.controllers;
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonValue;
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
import com.fasterxml.jackson.core.JsonParser;
|
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.annotations.VisibleForTesting;
|
||||||
import com.google.common.net.HttpHeaders;
|
import com.google.common.net.HttpHeaders;
|
||||||
import io.dropwizard.auth.Auth;
|
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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
@ -348,6 +343,7 @@ public class ArchiveController {
|
||||||
@ApiResponseZkAuth
|
@ApiResponseZkAuth
|
||||||
public CompletionStage<ReadAuthResponse> readAuth(
|
public CompletionStage<ReadAuthResponse> readAuth(
|
||||||
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
||||||
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||||
|
|
||||||
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ -361,7 +357,7 @@ public class ArchiveController {
|
||||||
if (account.isPresent()) {
|
if (account.isPresent()) {
|
||||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
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(user -> backupManager.generateReadAuth(user, cdn))
|
||||||
.thenApply(ReadAuthResponse::new);
|
.thenApply(ReadAuthResponse::new);
|
||||||
}
|
}
|
||||||
|
@ -399,6 +395,7 @@ public class ArchiveController {
|
||||||
@ApiResponseZkAuth
|
@ApiResponseZkAuth
|
||||||
public CompletionStage<BackupInfoResponse> backupInfo(
|
public CompletionStage<BackupInfoResponse> backupInfo(
|
||||||
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
||||||
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||||
|
|
||||||
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ -411,7 +408,7 @@ public class ArchiveController {
|
||||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
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)
|
.thenCompose(backupManager::backupInfo)
|
||||||
.thenApply(backupInfo -> new BackupInfoResponse(
|
.thenApply(backupInfo -> new BackupInfoResponse(
|
||||||
backupInfo.cdn(),
|
backupInfo.cdn(),
|
||||||
|
@ -454,6 +451,9 @@ public class ArchiveController {
|
||||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature,
|
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature,
|
||||||
|
|
||||||
@Valid @NotNull SetPublicKeyRequest setPublicKeyRequest) {
|
@Valid @NotNull SetPublicKeyRequest setPublicKeyRequest) {
|
||||||
|
if (account.isPresent()) {
|
||||||
|
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||||
|
}
|
||||||
return backupManager
|
return backupManager
|
||||||
.setPublicKey(presentation.presentation, signature.signature, setPublicKeyRequest.backupIdPublicKey)
|
.setPublicKey(presentation.presentation, signature.signature, setPublicKeyRequest.backupIdPublicKey)
|
||||||
.thenApply(Util.ASYNC_EMPTY_RESPONSE);
|
.thenApply(Util.ASYNC_EMPTY_RESPONSE);
|
||||||
|
@ -481,6 +481,7 @@ public class ArchiveController {
|
||||||
@ApiResponseZkAuth
|
@ApiResponseZkAuth
|
||||||
public CompletionStage<UploadDescriptorResponse> backup(
|
public CompletionStage<UploadDescriptorResponse> backup(
|
||||||
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
||||||
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||||
|
|
||||||
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ -492,7 +493,7 @@ public class ArchiveController {
|
||||||
if (account.isPresent()) {
|
if (account.isPresent()) {
|
||||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
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)
|
.thenCompose(backupManager::createMessageBackupUploadDescriptor)
|
||||||
.thenApply(result -> new UploadDescriptorResponse(
|
.thenApply(result -> new UploadDescriptorResponse(
|
||||||
result.cdn(),
|
result.cdn(),
|
||||||
|
@ -517,6 +518,8 @@ public class ArchiveController {
|
||||||
@ApiResponseZkAuth
|
@ApiResponseZkAuth
|
||||||
public CompletionStage<UploadDescriptorResponse> uploadTemporaryAttachment(
|
public CompletionStage<UploadDescriptorResponse> uploadTemporaryAttachment(
|
||||||
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
||||||
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||||
|
|
||||||
|
|
||||||
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ -528,7 +531,7 @@ public class ArchiveController {
|
||||||
if (account.isPresent()) {
|
if (account.isPresent()) {
|
||||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
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)
|
.thenCompose(backupManager::createTemporaryAttachmentUploadDescriptor)
|
||||||
.thenApply(result -> new UploadDescriptorResponse(
|
.thenApply(result -> new UploadDescriptorResponse(
|
||||||
result.cdn(),
|
result.cdn(),
|
||||||
|
@ -620,7 +623,7 @@ public class ArchiveController {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Mono
|
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()))
|
.flatMap(backupUser -> backupManager.copyToBackup(backupUser, List.of(copyMediaRequest.toCopyParameters()))
|
||||||
.next()
|
.next()
|
||||||
.doOnNext(result -> backupMetrics.updateCopyCounter(result, UserAgentTagUtil.getPlatformTag(userAgent)))
|
.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");
|
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||||
}
|
}
|
||||||
final Stream<CopyParameters> copyParams = copyMediaRequest.items().stream().map(CopyMediaRequest::toCopyParameters);
|
final Stream<CopyParameters> 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()))
|
.flatMapMany(backupUser -> backupManager.copyToBackup(backupUser, copyParams.toList()))
|
||||||
.doOnNext(result -> backupMetrics.updateCopyCounter(result, UserAgentTagUtil.getPlatformTag(userAgent)))
|
.doOnNext(result -> backupMetrics.updateCopyCounter(result, UserAgentTagUtil.getPlatformTag(userAgent)))
|
||||||
.map(CopyMediaBatchResponse.Entry::fromCopyResult)
|
.map(CopyMediaBatchResponse.Entry::fromCopyResult)
|
||||||
|
@ -741,6 +744,7 @@ public class ArchiveController {
|
||||||
@ApiResponseZkAuth
|
@ApiResponseZkAuth
|
||||||
public CompletionStage<Response> refresh(
|
public CompletionStage<Response> refresh(
|
||||||
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
||||||
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||||
|
|
||||||
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ -753,7 +757,7 @@ public class ArchiveController {
|
||||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||||
}
|
}
|
||||||
return backupManager
|
return backupManager
|
||||||
.authenticateBackupUser(presentation.presentation, signature.signature)
|
.authenticateBackupUser(presentation.presentation, signature.signature, userAgent)
|
||||||
.thenCompose(backupManager::ttlRefresh)
|
.thenCompose(backupManager::ttlRefresh)
|
||||||
.thenApply(Util.ASYNC_EMPTY_RESPONSE);
|
.thenApply(Util.ASYNC_EMPTY_RESPONSE);
|
||||||
}
|
}
|
||||||
|
@ -807,6 +811,7 @@ public class ArchiveController {
|
||||||
@ApiResponseZkAuth
|
@ApiResponseZkAuth
|
||||||
public CompletionStage<ListResponse> listMedia(
|
public CompletionStage<ListResponse> listMedia(
|
||||||
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
||||||
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||||
|
|
||||||
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ -825,7 +830,7 @@ public class ArchiveController {
|
||||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||||
}
|
}
|
||||||
return backupManager
|
return backupManager
|
||||||
.authenticateBackupUser(presentation.presentation, signature.signature)
|
.authenticateBackupUser(presentation.presentation, signature.signature, userAgent)
|
||||||
.thenCompose(backupUser -> backupManager.list(backupUser, cursor, limit.orElse(1000))
|
.thenCompose(backupUser -> backupManager.list(backupUser, cursor, limit.orElse(1000))
|
||||||
.thenApply(result -> new ListResponse(
|
.thenApply(result -> new ListResponse(
|
||||||
result.media()
|
result.media()
|
||||||
|
@ -862,6 +867,7 @@ public class ArchiveController {
|
||||||
@ApiResponseZkAuth
|
@ApiResponseZkAuth
|
||||||
public CompletionStage<Response> deleteMedia(
|
public CompletionStage<Response> deleteMedia(
|
||||||
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
||||||
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||||
|
|
||||||
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ -881,7 +887,7 @@ public class ArchiveController {
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return backupManager
|
return backupManager
|
||||||
.authenticateBackupUser(presentation.presentation, signature.signature)
|
.authenticateBackupUser(presentation.presentation, signature.signature, userAgent)
|
||||||
.thenCompose(authenticatedBackupUser -> backupManager
|
.thenCompose(authenticatedBackupUser -> backupManager
|
||||||
.deleteMedia(authenticatedBackupUser, toDelete)
|
.deleteMedia(authenticatedBackupUser, toDelete)
|
||||||
.then().toFuture())
|
.then().toFuture())
|
||||||
|
@ -898,6 +904,7 @@ public class ArchiveController {
|
||||||
@ApiResponseZkAuth
|
@ApiResponseZkAuth
|
||||||
public CompletionStage<Response> deleteBackup(
|
public CompletionStage<Response> deleteBackup(
|
||||||
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
@ReadOnly @Auth final Optional<AuthenticatedDevice> account,
|
||||||
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||||
|
|
||||||
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ -910,7 +917,7 @@ public class ArchiveController {
|
||||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||||
}
|
}
|
||||||
return backupManager
|
return backupManager
|
||||||
.authenticateBackupUser(presentation.presentation, signature.signature)
|
.authenticateBackupUser(presentation.presentation, signature.signature, userAgent)
|
||||||
.thenCompose(backupManager::deleteEntireBackup)
|
.thenCompose(backupManager::deleteEntireBackup)
|
||||||
.thenApply(Util.ASYNC_EMPTY_RESPONSE);
|
.thenApply(Util.ASYNC_EMPTY_RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,7 +206,8 @@ public class BackupsAnonymousGrpcService extends ReactorBackupsAnonymousGrpc.Bac
|
||||||
try {
|
try {
|
||||||
return backupManager.authenticateBackupUser(
|
return backupManager.authenticateBackupUser(
|
||||||
new BackupAuthCredentialPresentation(signedPresentation.getPresentation().toByteArray()),
|
new BackupAuthCredentialPresentation(signedPresentation.getPresentation().toByteArray()),
|
||||||
signedPresentation.getPresentationSignature().toByteArray());
|
signedPresentation.getPresentationSignature().toByteArray(),
|
||||||
|
RequestAttributesUtil.getUserAgent().orElse(null));
|
||||||
} catch (InvalidInputException e) {
|
} catch (InvalidInputException e) {
|
||||||
throw Status.UNAUTHENTICATED.withDescription("Could not deserialize presentation").asRuntimeException();
|
throw Status.UNAUTHENTICATED.withDescription("Could not deserialize presentation").asRuntimeException();
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import io.micrometer.core.instrument.DistributionSummary;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
import net.sourceforge.argparse4j.inf.Namespace;
|
import net.sourceforge.argparse4j.inf.Namespace;
|
||||||
import net.sourceforge.argparse4j.inf.Subparser;
|
import net.sourceforge.argparse4j.inf.Subparser;
|
||||||
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
||||||
|
@ -64,43 +63,17 @@ public class BackupMetricsCommand extends AbstractCommandWithDependencies {
|
||||||
segments,
|
segments,
|
||||||
Runtime.getRuntime().availableProcessors());
|
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(),
|
final DistributionSummary timeSinceLastRefresh = Metrics.summary(name(getClass(),
|
||||||
"timeSinceLastRefresh"));
|
"timeSinceLastRefresh"));
|
||||||
final DistributionSummary timeSinceLastMediaRefresh = Metrics.summary(name(getClass(),
|
final DistributionSummary timeSinceLastMediaRefresh = Metrics.summary(name(getClass(),
|
||||||
"timeSinceLastMediaRefresh"));
|
"timeSinceLastMediaRefresh"));
|
||||||
final String backupsCounterName = name(getClass(), "backups");
|
|
||||||
|
|
||||||
final BackupManager backupManager = commandDependencies.backupManager();
|
final BackupManager backupManager = commandDependencies.backupManager();
|
||||||
final Long backupsExpired = backupManager
|
final Long backupsExpired = backupManager
|
||||||
.listBackupAttributes(segments, Schedulers.parallel())
|
.listBackupAttributes(segments, Schedulers.parallel())
|
||||||
.doOnNext(backupMetadata -> {
|
.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());
|
timeSinceLastRefresh.record(timeSince(backupMetadata.lastRefresh()).getSeconds());
|
||||||
timeSinceLastMediaRefresh.record(timeSince(backupMetadata.lastMediaRefresh()).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()
|
.count()
|
||||||
.block();
|
.block();
|
||||||
|
|
|
@ -30,6 +30,7 @@ import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
@ -261,7 +262,7 @@ public class BackupManagerTest {
|
||||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, backupLevel);
|
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, backupLevel);
|
||||||
|
|
||||||
final Instant tstart = Instant.ofEpochSecond(1).plus(Duration.ofDays(1));
|
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
|
// create backup at t=tstart
|
||||||
testClock.pin(tstart);
|
testClock.pin(tstart);
|
||||||
|
@ -272,8 +273,8 @@ public class BackupManagerTest {
|
||||||
backupManager.ttlRefresh(backupUser).join();
|
backupManager.ttlRefresh(backupUser).join();
|
||||||
|
|
||||||
checkExpectedExpirations(
|
checkExpectedExpirations(
|
||||||
tnext,
|
tnext.truncatedTo(ChronoUnit.DAYS),
|
||||||
backupLevel == BackupLevel.PAID ? tnext : null,
|
backupLevel == BackupLevel.PAID ? tnext.truncatedTo(ChronoUnit.DAYS) : null,
|
||||||
backupUser);
|
backupUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,7 +282,7 @@ public class BackupManagerTest {
|
||||||
@EnumSource
|
@EnumSource
|
||||||
public void createBackupRefreshesTtl(final BackupLevel backupLevel) {
|
public void createBackupRefreshesTtl(final BackupLevel backupLevel) {
|
||||||
final Instant tstart = Instant.ofEpochSecond(1).plus(Duration.ofDays(1));
|
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);
|
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, backupLevel);
|
||||||
|
|
||||||
|
@ -294,8 +295,8 @@ public class BackupManagerTest {
|
||||||
backupManager.createMessageBackupUploadDescriptor(backupUser).join();
|
backupManager.createMessageBackupUploadDescriptor(backupUser).join();
|
||||||
|
|
||||||
checkExpectedExpirations(
|
checkExpectedExpirations(
|
||||||
tnext,
|
tnext.truncatedTo(ChronoUnit.DAYS),
|
||||||
backupLevel == BackupLevel.PAID ? tnext : null,
|
backupLevel == BackupLevel.PAID ? tnext.truncatedTo(ChronoUnit.DAYS) : null,
|
||||||
backupUser);
|
backupUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +312,8 @@ public class BackupManagerTest {
|
||||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||||
.isThrownBy(() -> backupManager.authenticateBackupUser(
|
.isThrownBy(() -> backupManager.authenticateBackupUser(
|
||||||
invalidPresentation,
|
invalidPresentation,
|
||||||
keyPair.getPrivateKey().calculateSignature(invalidPresentation.serialize())))
|
keyPair.getPrivateKey().calculateSignature(invalidPresentation.serialize()),
|
||||||
|
null))
|
||||||
.extracting(StatusRuntimeException::getStatus)
|
.extracting(StatusRuntimeException::getStatus)
|
||||||
.extracting(Status::getCode)
|
.extracting(Status::getCode)
|
||||||
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
||||||
|
@ -335,7 +337,8 @@ public class BackupManagerTest {
|
||||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||||
.isThrownBy(() -> backupManager.authenticateBackupUser(
|
.isThrownBy(() -> backupManager.authenticateBackupUser(
|
||||||
invalidPresentation,
|
invalidPresentation,
|
||||||
keyPair.getPrivateKey().calculateSignature(invalidPresentation.serialize())))
|
keyPair.getPrivateKey().calculateSignature(invalidPresentation.serialize()),
|
||||||
|
null))
|
||||||
.extracting(StatusRuntimeException::getStatus)
|
.extracting(StatusRuntimeException::getStatus)
|
||||||
.extracting(Status::getCode)
|
.extracting(Status::getCode)
|
||||||
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
||||||
|
@ -352,7 +355,7 @@ public class BackupManagerTest {
|
||||||
// haven't set a public key yet
|
// haven't set a public key yet
|
||||||
assertThat(CompletableFutureTestUtil.assertFailsWithCause(
|
assertThat(CompletableFutureTestUtil.assertFailsWithCause(
|
||||||
StatusRuntimeException.class,
|
StatusRuntimeException.class,
|
||||||
backupManager.authenticateBackupUser(presentation, signature))
|
backupManager.authenticateBackupUser(presentation, signature, null))
|
||||||
.getStatus().getCode())
|
.getStatus().getCode())
|
||||||
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
||||||
}
|
}
|
||||||
|
@ -403,12 +406,12 @@ public class BackupManagerTest {
|
||||||
// shouldn't be able to authenticate with an invalid signature
|
// shouldn't be able to authenticate with an invalid signature
|
||||||
assertThat(CompletableFutureTestUtil.assertFailsWithCause(
|
assertThat(CompletableFutureTestUtil.assertFailsWithCause(
|
||||||
StatusRuntimeException.class,
|
StatusRuntimeException.class,
|
||||||
backupManager.authenticateBackupUser(presentation, wrongSignature))
|
backupManager.authenticateBackupUser(presentation, wrongSignature, null))
|
||||||
.getStatus().getCode())
|
.getStatus().getCode())
|
||||||
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
||||||
|
|
||||||
// correct signature
|
// 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.backupId()).isEqualTo(presentation.getBackupId());
|
||||||
assertThat(user.backupLevel()).isEqualTo(BackupLevel.FREE);
|
assertThat(user.backupLevel()).isEqualTo(BackupLevel.FREE);
|
||||||
}
|
}
|
||||||
|
@ -426,16 +429,16 @@ public class BackupManagerTest {
|
||||||
|
|
||||||
// should be accepted the day before to forgive clock skew
|
// should be accepted the day before to forgive clock skew
|
||||||
testClock.pin(Instant.ofEpochSecond(1));
|
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
|
// should be accepted the day after to forgive clock skew
|
||||||
testClock.pin(Instant.ofEpochSecond(1).plus(Duration.ofDays(2)));
|
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
|
// should be rejected the day after that
|
||||||
testClock.pin(Instant.ofEpochSecond(1).plus(Duration.ofDays(3)));
|
testClock.pin(Instant.ofEpochSecond(1).plus(Duration.ofDays(3)));
|
||||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||||
.isThrownBy(() -> backupManager.authenticateBackupUser(oldCredential, signature))
|
.isThrownBy(() -> backupManager.authenticateBackupUser(oldCredential, signature, null))
|
||||||
.extracting(StatusRuntimeException::getStatus)
|
.extracting(StatusRuntimeException::getStatus)
|
||||||
.extracting(Status::getCode)
|
.extracting(Status::getCode)
|
||||||
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
||||||
|
@ -856,7 +859,7 @@ public class BackupManagerTest {
|
||||||
.mapToObj(i -> backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID))
|
.mapToObj(i -> backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID))
|
||||||
.toList();
|
.toList();
|
||||||
for (int i = 0; i < backupUsers.size(); i++) {
|
for (int i = 0; i < backupUsers.size(); i++) {
|
||||||
testClock.pin(Instant.ofEpochSecond(i));
|
testClock.pin(days(i));
|
||||||
backupManager.createMessageBackupUploadDescriptor(backupUsers.get(i)).join();
|
backupManager.createMessageBackupUploadDescriptor(backupUsers.get(i)).join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -864,11 +867,12 @@ public class BackupManagerTest {
|
||||||
final Set<ByteBuffer> expectedHashes = new HashSet<>();
|
final Set<ByteBuffer> expectedHashes = new HashSet<>();
|
||||||
|
|
||||||
for (int i = 0; i < backupUsers.size(); i++) {
|
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
|
// get backups expired at t=i
|
||||||
final List<ExpiredBackup> expired = backupManager
|
final List<ExpiredBackup> expired = backupManager
|
||||||
.getExpiredBackups(1, Schedulers.immediate(), Instant.ofEpochSecond(i))
|
.getExpiredBackups(1, Schedulers.immediate(), day)
|
||||||
.collectList()
|
.collectList()
|
||||||
.block();
|
.block();
|
||||||
|
|
||||||
|
@ -890,24 +894,24 @@ public class BackupManagerTest {
|
||||||
final byte[] backupId = TestRandomUtil.nextBytes(16);
|
final byte[] backupId = TestRandomUtil.nextBytes(16);
|
||||||
|
|
||||||
// refreshed media timestamp at t=5
|
// refreshed media timestamp at t=5
|
||||||
testClock.pin(Instant.ofEpochSecond(5));
|
testClock.pin(days(5));
|
||||||
backupManager.createMessageBackupUploadDescriptor(backupUser(backupId, BackupCredentialType.MESSAGES, BackupLevel.PAID)).join();
|
backupManager.createMessageBackupUploadDescriptor(backupUser(backupId, BackupCredentialType.MESSAGES, BackupLevel.PAID)).join();
|
||||||
|
|
||||||
// refreshed messages timestamp at t=6
|
// refreshed messages timestamp at t=6
|
||||||
testClock.pin(Instant.ofEpochSecond(6));
|
testClock.pin(days(6));
|
||||||
backupManager.createMessageBackupUploadDescriptor(backupUser(backupId, BackupCredentialType.MESSAGES, BackupLevel.FREE)).join();
|
backupManager.createMessageBackupUploadDescriptor(backupUser(backupId, BackupCredentialType.MESSAGES, BackupLevel.FREE)).join();
|
||||||
|
|
||||||
Function<Instant, List<ExpiredBackup>> getExpired = time -> backupManager
|
Function<Instant, List<ExpiredBackup>> getExpired = time -> backupManager
|
||||||
.getExpiredBackups(1, Schedulers.immediate(), time)
|
.getExpiredBackups(1, Schedulers.immediate(), time)
|
||||||
.collectList().block();
|
.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()
|
.hasSize(1).first()
|
||||||
.matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.MEDIA, "is media tier");
|
.matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.MEDIA, "is media tier");
|
||||||
|
|
||||||
assertThat(getExpired.apply(Instant.ofEpochSecond(7)))
|
assertThat(getExpired.apply(days(7)))
|
||||||
.hasSize(1).first()
|
.hasSize(1).first()
|
||||||
.matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.ALL, "is messages tier");
|
.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) {
|
private AuthenticatedBackupUser retrieveBackupUser(final byte[] backupId, final BackupCredentialType credentialType, final BackupLevel backupLevel) {
|
||||||
final BackupsDb.AuthenticationData authData = backupsDb.retrieveAuthenticationData(backupId).join().get();
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,16 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import io.grpc.StatusRuntimeException;
|
import io.grpc.StatusRuntimeException;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
import org.assertj.core.util.Streams;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
@ -89,13 +93,13 @@ public class BackupsDbTest {
|
||||||
@Test
|
@Test
|
||||||
public void expirationDetectedOnce() {
|
public void expirationDetectedOnce() {
|
||||||
final byte[] backupId = TestRandomUtil.nextBytes(16);
|
final byte[] backupId = TestRandomUtil.nextBytes(16);
|
||||||
// Refresh media/messages at t=0
|
// Refresh media/messages at t=0D
|
||||||
testClock.pin(Instant.ofEpochSecond(0L));
|
testClock.pin(days(0));
|
||||||
backupsDb.setPublicKey(backupId, BackupLevel.PAID, Curve.generateKeyPair().getPublicKey()).join();
|
backupsDb.setPublicKey(backupId, BackupLevel.PAID, Curve.generateKeyPair().getPublicKey()).join();
|
||||||
this.backupsDb.ttlRefresh(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.PAID)).join();
|
this.backupsDb.ttlRefresh(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.PAID)).join();
|
||||||
|
|
||||||
// refresh only messages at t=2
|
// refresh only messages on t=2D
|
||||||
testClock.pin(Instant.ofEpochSecond(2L));
|
testClock.pin(days(2).plus(Duration.ofSeconds(123)));
|
||||||
this.backupsDb.ttlRefresh(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.FREE)).join();
|
this.backupsDb.ttlRefresh(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.FREE)).join();
|
||||||
|
|
||||||
final Function<Instant, List<ExpiredBackup>> expiredBackups = purgeTime -> backupsDb
|
final Function<Instant, List<ExpiredBackup>> expiredBackups = purgeTime -> backupsDb
|
||||||
|
@ -103,7 +107,8 @@ public class BackupsDbTest {
|
||||||
.collectList()
|
.collectList()
|
||||||
.block();
|
.block();
|
||||||
|
|
||||||
List<ExpiredBackup> expired = expiredBackups.apply(Instant.ofEpochSecond(1));
|
// the media should be expired at t=1D
|
||||||
|
List<ExpiredBackup> expired = expiredBackups.apply(days(1));
|
||||||
assertThat(expired).hasSize(1).first()
|
assertThat(expired).hasSize(1).first()
|
||||||
.matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.MEDIA);
|
.matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.MEDIA);
|
||||||
|
|
||||||
|
@ -111,11 +116,11 @@ public class BackupsDbTest {
|
||||||
backupsDb.startExpiration(expired.getFirst()).join();
|
backupsDb.startExpiration(expired.getFirst()).join();
|
||||||
backupsDb.finishExpiration(expired.getFirst()).join();
|
backupsDb.finishExpiration(expired.getFirst()).join();
|
||||||
|
|
||||||
// should be nothing to expire at t=1
|
// should be nothing left to expire at t=1D
|
||||||
assertThat(expiredBackups.apply(Instant.ofEpochSecond(1))).isEmpty();
|
assertThat(expiredBackups.apply(days(1))).isEmpty();
|
||||||
|
|
||||||
// at t=3, should now expire messages as well
|
// at t=3D, should now expire messages as well
|
||||||
expired = expiredBackups.apply(Instant.ofEpochSecond(3));
|
expired = expiredBackups.apply(days(3));
|
||||||
assertThat(expired).hasSize(1).first()
|
assertThat(expired).hasSize(1).first()
|
||||||
.matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.ALL);
|
.matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.ALL);
|
||||||
|
|
||||||
|
@ -124,21 +129,21 @@ public class BackupsDbTest {
|
||||||
backupsDb.finishExpiration(expired.getFirst()).join();
|
backupsDb.finishExpiration(expired.getFirst()).join();
|
||||||
|
|
||||||
// should be nothing to expire at t=3
|
// should be nothing to expire at t=3
|
||||||
assertThat(expiredBackups.apply(Instant.ofEpochSecond(3))).isEmpty();
|
assertThat(expiredBackups.apply(days(3))).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@EnumSource(names = {"MEDIA", "ALL"})
|
@EnumSource(names = {"MEDIA", "ALL"})
|
||||||
public void expirationFailed(ExpiredBackup.ExpirationType expirationType) {
|
public void expirationFailed(ExpiredBackup.ExpirationType expirationType) {
|
||||||
final byte[] backupId = TestRandomUtil.nextBytes(16);
|
final byte[] backupId = TestRandomUtil.nextBytes(16);
|
||||||
// Refresh media/messages at t=0
|
// Refresh media/messages at t=0D
|
||||||
testClock.pin(Instant.ofEpochSecond(0L));
|
testClock.pin(days(0));
|
||||||
backupsDb.setPublicKey(backupId, BackupLevel.PAID, Curve.generateKeyPair().getPublicKey()).join();
|
backupsDb.setPublicKey(backupId, BackupLevel.PAID, Curve.generateKeyPair().getPublicKey()).join();
|
||||||
this.backupsDb.ttlRefresh(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.PAID)).join();
|
this.backupsDb.ttlRefresh(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.PAID)).join();
|
||||||
|
|
||||||
if (expirationType == ExpiredBackup.ExpirationType.MEDIA) {
|
if (expirationType == ExpiredBackup.ExpirationType.MEDIA) {
|
||||||
// refresh only messages at t=2 so that we only expire media at t=1
|
// refresh only messages at t=2D so that we only expire media at t=1D
|
||||||
testClock.pin(Instant.ofEpochSecond(2L));
|
testClock.pin(days(2));
|
||||||
this.backupsDb.ttlRefresh(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.FREE)).join();
|
this.backupsDb.ttlRefresh(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.FREE)).join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +160,7 @@ public class BackupsDbTest {
|
||||||
final String originalBackupDir = info.backupDir();
|
final String originalBackupDir = info.backupDir();
|
||||||
final String originalMediaDir = info.mediaDir();
|
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);
|
assertThat(expired).matches(eb -> eb.expirationType() == expirationType);
|
||||||
|
|
||||||
// expire but fail (don't call finishExpiration)
|
// expire but fail (don't call finishExpiration)
|
||||||
|
@ -179,7 +184,7 @@ public class BackupsDbTest {
|
||||||
final String expiredPrefix = expired.prefixToDelete();
|
final String expiredPrefix = expired.prefixToDelete();
|
||||||
|
|
||||||
// We failed, so we should see the same prefix on the next expiration listing
|
// 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,
|
assertThat(expired).matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.GARBAGE_COLLECTION,
|
||||||
"Expiration should be garbage collection ");
|
"Expiration should be garbage collection ");
|
||||||
assertThat(expired.prefixToDelete()).isEqualTo(expiredPrefix);
|
assertThat(expired.prefixToDelete()).isEqualTo(expiredPrefix);
|
||||||
|
@ -188,7 +193,7 @@ public class BackupsDbTest {
|
||||||
// Successfully finish the expiration
|
// Successfully finish the expiration
|
||||||
backupsDb.finishExpiration(expired).join();
|
backupsDb.finishExpiration(expired).join();
|
||||||
|
|
||||||
Optional<ExpiredBackup> opt = expiredBackups.apply(Instant.ofEpochSecond(1));
|
Optional<ExpiredBackup> opt = expiredBackups.apply(days(1));
|
||||||
if (expirationType == ExpiredBackup.ExpirationType.MEDIA) {
|
if (expirationType == ExpiredBackup.ExpirationType.MEDIA) {
|
||||||
// should be nothing to expire at t=1
|
// should be nothing to expire at t=1
|
||||||
assertThat(opt).isEmpty();
|
assertThat(opt).isEmpty();
|
||||||
|
@ -212,19 +217,24 @@ public class BackupsDbTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void list() {
|
public void list() {
|
||||||
final AuthenticatedBackupUser u1 = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.FREE);
|
final List<AuthenticatedBackupUser> users = List.of(
|
||||||
final AuthenticatedBackupUser u2 = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.FREE),
|
||||||
final AuthenticatedBackupUser u3 = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID),
|
||||||
|
backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID));
|
||||||
|
|
||||||
|
final List<Instant> 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
|
// add at least one message backup, so we can describe it
|
||||||
testClock.pin(Instant.ofEpochSecond(10));
|
for (int i = 0; i < users.size(); i++) {
|
||||||
Stream.of(u1, u2, u3).forEach(u -> backupsDb.addMessageBackup(u).join());
|
testClock.pin(lastRefreshTimes.get(i));
|
||||||
|
backupsDb.addMessageBackup(users.get(i)).join();
|
||||||
|
}
|
||||||
|
|
||||||
testClock.pin(Instant.ofEpochSecond(20));
|
backupsDb.trackMedia(users.get(1), 10, 100).join();
|
||||||
backupsDb.trackMedia(u2, 10, 100).join();
|
backupsDb.trackMedia(users.get(2), 1, 1000).join();
|
||||||
|
|
||||||
testClock.pin(Instant.ofEpochSecond(30));
|
|
||||||
backupsDb.trackMedia(u3, 1, 1000).join();
|
|
||||||
|
|
||||||
final List<StoredBackupAttributes> sbms = backupsDb.listBackupAttributes(1, Schedulers.immediate())
|
final List<StoredBackupAttributes> sbms = backupsDb.listBackupAttributes(1, Schedulers.immediate())
|
||||||
.sort(Comparator.comparing(StoredBackupAttributes::lastRefresh))
|
.sort(Comparator.comparing(StoredBackupAttributes::lastRefresh))
|
||||||
|
@ -234,22 +244,28 @@ public class BackupsDbTest {
|
||||||
final StoredBackupAttributes sbm1 = sbms.get(0);
|
final StoredBackupAttributes sbm1 = sbms.get(0);
|
||||||
assertThat(sbm1.bytesUsed()).isEqualTo(0);
|
assertThat(sbm1.bytesUsed()).isEqualTo(0);
|
||||||
assertThat(sbm1.numObjects()).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);
|
assertThat(sbm1.lastMediaRefresh()).isEqualTo(Instant.EPOCH);
|
||||||
|
|
||||||
|
|
||||||
final StoredBackupAttributes sbm2 = sbms.get(1);
|
final StoredBackupAttributes sbm2 = sbms.get(1);
|
||||||
assertThat(sbm2.bytesUsed()).isEqualTo(100);
|
assertThat(sbm2.bytesUsed()).isEqualTo(100);
|
||||||
assertThat(sbm2.numObjects()).isEqualTo(10);
|
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);
|
final StoredBackupAttributes sbm3 = sbms.get(2);
|
||||||
assertThat(sbm3.bytesUsed()).isEqualTo(1000);
|
assertThat(sbm3.bytesUsed()).isEqualTo(1000);
|
||||||
assertThat(sbm3.numObjects()).isEqualTo(1);
|
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) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,7 +156,7 @@ public class ArchiveControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void setBackupId() throws RateLimitExceededException {
|
public void setBackupId() {
|
||||||
when(backupAuthManager.commitBackupId(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
when(backupAuthManager.commitBackupId(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||||
|
|
||||||
final Response response = resources.getJerseyTest()
|
final Response response = resources.getJerseyTest()
|
||||||
|
@ -356,7 +356,7 @@ public class ArchiveControllerTest {
|
||||||
public void getBackupInfo() throws VerificationFailedException {
|
public void getBackupInfo() throws VerificationFailedException {
|
||||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||||
BackupLevel.PAID, messagesBackupKey, aci);
|
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)));
|
.thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
||||||
when(backupManager.backupInfo(any())).thenReturn(CompletableFuture.completedFuture(new BackupManager.BackupInfo(
|
when(backupManager.backupInfo(any())).thenReturn(CompletableFuture.completedFuture(new BackupManager.BackupInfo(
|
||||||
1, "myBackupDir", "myMediaDir", "filename", Optional.empty())));
|
1, "myBackupDir", "myMediaDir", "filename", Optional.empty())));
|
||||||
|
@ -376,7 +376,7 @@ public class ArchiveControllerTest {
|
||||||
public void putMediaBatchSuccess() throws VerificationFailedException {
|
public void putMediaBatchSuccess() throws VerificationFailedException {
|
||||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||||
BackupLevel.PAID, messagesBackupKey, aci);
|
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)));
|
.thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
||||||
final byte[][] mediaIds = new byte[][]{TestRandomUtil.nextBytes(15), TestRandomUtil.nextBytes(15)};
|
final byte[][] mediaIds = new byte[][]{TestRandomUtil.nextBytes(15), TestRandomUtil.nextBytes(15)};
|
||||||
when(backupManager.copyToBackup(any(), any()))
|
when(backupManager.copyToBackup(any(), any()))
|
||||||
|
@ -421,7 +421,7 @@ public class ArchiveControllerTest {
|
||||||
|
|
||||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||||
BackupLevel.PAID, messagesBackupKey, aci);
|
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)));
|
.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);
|
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 {
|
public void copyMediaWithNegativeLength() throws VerificationFailedException {
|
||||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||||
BackupLevel.PAID, messagesBackupKey, aci);
|
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)));
|
.thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
||||||
final byte[][] mediaIds = new byte[][]{TestRandomUtil.nextBytes(15), TestRandomUtil.nextBytes(15)};
|
final byte[][] mediaIds = new byte[][]{TestRandomUtil.nextBytes(15), TestRandomUtil.nextBytes(15)};
|
||||||
final Response r = resources.getJerseyTest()
|
final Response r = resources.getJerseyTest()
|
||||||
|
@ -512,7 +512,7 @@ public class ArchiveControllerTest {
|
||||||
throws VerificationFailedException {
|
throws VerificationFailedException {
|
||||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||||
BackupLevel.PAID, messagesBackupKey, aci);
|
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)));
|
.thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
||||||
|
|
||||||
final byte[] mediaId = TestRandomUtil.nextBytes(15);
|
final byte[] mediaId = TestRandomUtil.nextBytes(15);
|
||||||
|
@ -547,7 +547,7 @@ public class ArchiveControllerTest {
|
||||||
public void delete() throws VerificationFailedException {
|
public void delete() throws VerificationFailedException {
|
||||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(BackupLevel.PAID,
|
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(BackupLevel.PAID,
|
||||||
messagesBackupKey, aci);
|
messagesBackupKey, aci);
|
||||||
when(backupManager.authenticateBackupUser(any(), any()))
|
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||||
.thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
.thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
||||||
|
|
||||||
final ArchiveController.DeleteMedia deleteRequest = new ArchiveController.DeleteMedia(
|
final ArchiveController.DeleteMedia deleteRequest = new ArchiveController.DeleteMedia(
|
||||||
|
@ -573,7 +573,7 @@ public class ArchiveControllerTest {
|
||||||
public void mediaUploadForm() throws VerificationFailedException {
|
public void mediaUploadForm() throws VerificationFailedException {
|
||||||
final BackupAuthCredentialPresentation presentation =
|
final BackupAuthCredentialPresentation presentation =
|
||||||
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
|
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)));
|
.thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
||||||
when(backupManager.createTemporaryAttachmentUploadDescriptor(any()))
|
when(backupManager.createTemporaryAttachmentUploadDescriptor(any()))
|
||||||
.thenReturn(CompletableFuture.completedFuture(
|
.thenReturn(CompletableFuture.completedFuture(
|
||||||
|
@ -605,7 +605,7 @@ public class ArchiveControllerTest {
|
||||||
public void readAuth() throws VerificationFailedException {
|
public void readAuth() throws VerificationFailedException {
|
||||||
final BackupAuthCredentialPresentation presentation =
|
final BackupAuthCredentialPresentation presentation =
|
||||||
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
|
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)));
|
.thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
||||||
when(backupManager.generateReadAuth(any(), eq(3))).thenReturn(Map.of("key", "value"));
|
when(backupManager.generateReadAuth(any(), eq(3))).thenReturn(Map.of("key", "value"));
|
||||||
final ArchiveController.ReadAuthResponse response = resources.getJerseyTest()
|
final ArchiveController.ReadAuthResponse response = resources.getJerseyTest()
|
||||||
|
@ -644,7 +644,7 @@ public class ArchiveControllerTest {
|
||||||
public void deleteEntireBackup() throws VerificationFailedException {
|
public void deleteEntireBackup() throws VerificationFailedException {
|
||||||
final BackupAuthCredentialPresentation presentation =
|
final BackupAuthCredentialPresentation presentation =
|
||||||
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
|
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)));
|
.thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
||||||
when(backupManager.deleteEntireBackup(any())).thenReturn(CompletableFuture.completedFuture(null));
|
when(backupManager.deleteEntireBackup(any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||||
Response response = resources.getJerseyTest()
|
Response response = resources.getJerseyTest()
|
||||||
|
@ -660,7 +660,7 @@ public class ArchiveControllerTest {
|
||||||
public void invalidSourceAttachmentKey() throws VerificationFailedException {
|
public void invalidSourceAttachmentKey() throws VerificationFailedException {
|
||||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||||
BackupLevel.PAID, messagesBackupKey, aci);
|
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)));
|
.thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
||||||
final Response r = resources.getJerseyTest()
|
final Response r = resources.getJerseyTest()
|
||||||
.target("v1/archives/media")
|
.target("v1/archives/media")
|
||||||
|
@ -677,6 +677,6 @@ public class ArchiveControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupCredentialType credentialType, final BackupLevel backupLevel) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setup() {
|
void setup() {
|
||||||
when(backupManager.authenticateBackupUser(any(), any()))
|
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||||
.thenReturn(CompletableFuture.completedFuture(
|
.thenReturn(CompletableFuture.completedFuture(
|
||||||
backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
||||||
}
|
}
|
||||||
|
@ -308,7 +308,7 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||||
|
|
||||||
private static AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupCredentialType credentialType,
|
private static AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupCredentialType credentialType,
|
||||||
final BackupLevel backupLevel) {
|
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,
|
private static BackupAuthCredentialPresentation presentation(BackupAuthTestUtil backupAuthTestUtil,
|
||||||
|
|
Loading…
Reference in New Issue