mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 09:28:03 +01:00
Update to libsignal 0.45 and use libsignal's BackupLevel
This commit is contained in:
committed by
ravi-signal
parent
c8efcf5105
commit
19944bfdb2
@@ -5,6 +5,6 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.backup.BackupTier;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
||||
|
||||
public record AuthenticatedBackupUser(byte[] backupId, BackupTier backupTier, String backupDir, String mediaDir) {}
|
||||
public record AuthenticatedBackupUser(byte[] backupId, BackupLevel backupLevel, String backupDir, String mediaDir) {}
|
||||
|
||||
@@ -11,7 +11,6 @@ import java.time.Clock;
|
||||
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.CompletableFuture;
|
||||
@@ -21,6 +20,7 @@ import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptSerial;
|
||||
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||
@@ -89,7 +89,7 @@ public class BackupAuthManager {
|
||||
*/
|
||||
public CompletableFuture<Void> commitBackupId(final Account account,
|
||||
final BackupAuthCredentialRequest backupAuthCredentialRequest) throws RateLimitExceededException {
|
||||
if (configuredReceiptLevel(account).isEmpty()) {
|
||||
if (configuredBackupLevel(account).isEmpty()) {
|
||||
throw Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException();
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ public class BackupAuthManager {
|
||||
}
|
||||
|
||||
// If this account isn't allowed some level of backup access via configuration, don't continue
|
||||
final long configuredReceiptLevel = configuredReceiptLevel(account).orElseThrow(() ->
|
||||
final BackupLevel configuredBackupLevel = configuredBackupLevel(account).orElseThrow(() ->
|
||||
Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException());
|
||||
|
||||
final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS);
|
||||
@@ -169,9 +169,9 @@ public class BackupAuthManager {
|
||||
.map(redemptionTime -> {
|
||||
// Check if the account has a voucher that's good for a certain receiptLevel at redemption time, otherwise
|
||||
// use the default receipt level
|
||||
final long receiptLevel = storedReceiptLevel(account, redemptionTime).orElse(configuredReceiptLevel);
|
||||
final BackupLevel backupLevel = storedBackupLevel(account, redemptionTime).orElse(configuredBackupLevel);
|
||||
return new Credential(
|
||||
credentialReq.issueCredential(redemptionTime, receiptLevel, serverSecretParams),
|
||||
credentialReq.issueCredential(redemptionTime, backupLevel, serverSecretParams),
|
||||
redemptionTime);
|
||||
})
|
||||
.toList());
|
||||
@@ -208,10 +208,11 @@ public class BackupAuthManager {
|
||||
|
||||
final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();
|
||||
|
||||
BackupTier.fromReceiptLevel(receiptLevel).filter(BackupTier.MEDIA::equals)
|
||||
.orElseThrow(() -> Status.INVALID_ARGUMENT
|
||||
.withDescription("server does not recognize the requested receipt level")
|
||||
.asRuntimeException());
|
||||
if (BackupLevelUtil.fromReceiptLevel(receiptLevel) != BackupLevel.MEDIA) {
|
||||
throw Status.INVALID_ARGUMENT
|
||||
.withDescription("server does not recognize the requested receipt level")
|
||||
.asRuntimeException();
|
||||
}
|
||||
|
||||
return redeemedReceiptsManager
|
||||
.put(receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, account.getUuid())
|
||||
@@ -262,10 +263,11 @@ public class BackupAuthManager {
|
||||
* @param redemptionTime The time to check against the expiration time
|
||||
* @return The receipt level on the backup voucher, or empty if the account does not have one or it is expired
|
||||
*/
|
||||
private Optional<Long> storedReceiptLevel(final Account account, final Instant redemptionTime) {
|
||||
private Optional<BackupLevel> storedBackupLevel(final Account account, final Instant redemptionTime) {
|
||||
return Optional.ofNullable(account.getBackupVoucher())
|
||||
.filter(backupVoucher -> !redemptionTime.isAfter(backupVoucher.expiration()))
|
||||
.map(Account.BackupVoucher::receiptLevel);
|
||||
.map(Account.BackupVoucher::receiptLevel)
|
||||
.map(BackupLevelUtil::fromReceiptLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -275,12 +277,12 @@ public class BackupAuthManager {
|
||||
* @return If present, the default receipt level that should be used for the account if the account does not have a
|
||||
* BackupVoucher. Empty if the account should never have backup access
|
||||
*/
|
||||
private Optional<Long> configuredReceiptLevel(final Account account) {
|
||||
private Optional<BackupLevel> configuredBackupLevel(final Account account) {
|
||||
if (inExperiment(BACKUP_MEDIA_EXPERIMENT_NAME, account)) {
|
||||
return Optional.of(BackupTier.MEDIA.getReceiptLevel());
|
||||
return Optional.of(BackupLevel.MEDIA);
|
||||
}
|
||||
if (inExperiment(BACKUP_EXPERIMENT_NAME, account)) {
|
||||
return Optional.of(BackupTier.MESSAGES.getReceiptLevel());
|
||||
return Optional.of(BackupLevel.MESSAGES);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
||||
|
||||
public class BackupLevelUtil {
|
||||
public static BackupLevel fromReceiptLevel(long receiptLevel) {
|
||||
try {
|
||||
return BackupLevel.fromValue(Math.toIntExact(receiptLevel));
|
||||
} catch (ArithmeticException e) {
|
||||
throw new IllegalArgumentException("Invalid receipt level: " + receiptLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.attachments.AttachmentGenerator;
|
||||
@@ -126,14 +127,8 @@ public class BackupManager {
|
||||
// Note: this is a special case where we can't validate the presentation signature against the stored public key
|
||||
// because we are currently setting it. We check against the provided public key, but we must also verify that
|
||||
// there isn't an existing, different stored public key for the backup-id (verified with a condition expression)
|
||||
final BackupTier backupTier = verifyPresentation(presentation).verifySignature(signature, publicKey);
|
||||
if (backupTier.compareTo(BackupTier.MESSAGES) < 0) {
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||
throw Status.PERMISSION_DENIED
|
||||
.withDescription("credential does not support setting public key")
|
||||
.asRuntimeException();
|
||||
}
|
||||
return backupsDb.setPublicKey(presentation.getBackupId(), backupTier, publicKey)
|
||||
final BackupLevel backupLevel = verifyPresentation(presentation).verifySignature(signature, publicKey);
|
||||
return backupsDb.setPublicKey(presentation.getBackupId(), backupLevel, publicKey)
|
||||
.exceptionally(ExceptionUtils.exceptionallyHandler(PublicKeyConflictException.class, ex -> {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||
@@ -156,7 +151,7 @@ public class BackupManager {
|
||||
*/
|
||||
public CompletableFuture<BackupUploadDescriptor> createMessageBackupUploadDescriptor(
|
||||
final AuthenticatedBackupUser backupUser) {
|
||||
checkBackupTier(backupUser, BackupTier.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
|
||||
// this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
|
||||
return backupsDb
|
||||
@@ -166,7 +161,7 @@ public class BackupManager {
|
||||
|
||||
public BackupUploadDescriptor createTemporaryAttachmentUploadDescriptor(final AuthenticatedBackupUser backupUser)
|
||||
throws RateLimitExceededException {
|
||||
checkBackupTier(backupUser, BackupTier.MEDIA);
|
||||
checkBackupLevel(backupUser, BackupLevel.MEDIA);
|
||||
|
||||
RateLimiter.adaptLegacyException(() -> rateLimiters
|
||||
.forDescriptor(RateLimiters.For.BACKUP_ATTACHMENT)
|
||||
@@ -185,7 +180,7 @@ public class BackupManager {
|
||||
* @param backupUser an already ZK authenticated backup user
|
||||
*/
|
||||
public CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
|
||||
checkBackupTier(backupUser, BackupTier.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
// update message backup TTL
|
||||
return backupsDb.ttlRefresh(backupUser);
|
||||
}
|
||||
@@ -200,7 +195,7 @@ public class BackupManager {
|
||||
* @return Information about the existing backup
|
||||
*/
|
||||
public CompletableFuture<BackupInfo> backupInfo(final AuthenticatedBackupUser backupUser) {
|
||||
checkBackupTier(backupUser, BackupTier.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
return backupsDb.describeBackup(backupUser)
|
||||
.thenApply(backupDescription -> new BackupInfo(
|
||||
backupDescription.cdn(),
|
||||
@@ -218,7 +213,7 @@ public class BackupManager {
|
||||
* @return true if mediaLength bytes can be stored
|
||||
*/
|
||||
public CompletableFuture<Boolean> canStoreMedia(final AuthenticatedBackupUser backupUser, final long mediaLength) {
|
||||
checkBackupTier(backupUser, BackupTier.MEDIA);
|
||||
checkBackupLevel(backupUser, BackupLevel.MEDIA);
|
||||
return backupsDb.getMediaUsage(backupUser)
|
||||
.thenComposeAsync(info -> {
|
||||
final boolean canStore = MAX_TOTAL_BACKUP_MEDIA_BYTES - info.usageInfo().bytesUsed() >= mediaLength;
|
||||
@@ -269,7 +264,7 @@ public class BackupManager {
|
||||
final int sourceLength,
|
||||
final MediaEncryptionParameters encryptionParameters,
|
||||
final byte[] destinationMediaId) {
|
||||
checkBackupTier(backupUser, BackupTier.MEDIA);
|
||||
checkBackupLevel(backupUser, BackupLevel.MEDIA);
|
||||
if (sourceLength > MAX_MEDIA_OBJECT_SIZE) {
|
||||
throw Status.INVALID_ARGUMENT
|
||||
.withDescription("Invalid sourceObject size")
|
||||
@@ -331,7 +326,7 @@ public class BackupManager {
|
||||
* @return A map of headers to include with CDN requests
|
||||
*/
|
||||
public Map<String, String> generateReadAuth(final AuthenticatedBackupUser backupUser, final int cdnNumber) {
|
||||
checkBackupTier(backupUser, BackupTier.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
if (cdnNumber != 3) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("unknown cdn").asRuntimeException();
|
||||
}
|
||||
@@ -359,7 +354,7 @@ public class BackupManager {
|
||||
final AuthenticatedBackupUser backupUser,
|
||||
final Optional<String> cursor,
|
||||
final int limit) {
|
||||
checkBackupTier(backupUser, BackupTier.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
return remoteStorageManager.list(cdnMediaDirectory(backupUser), cursor, limit)
|
||||
.thenApply(result ->
|
||||
new ListMediaResult(
|
||||
@@ -377,7 +372,7 @@ public class BackupManager {
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> deleteEntireBackup(final AuthenticatedBackupUser backupUser) {
|
||||
checkBackupTier(backupUser, BackupTier.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
return backupsDb
|
||||
// Try to swap out the backupDir for the user
|
||||
.scheduleBackupDeletion(backupUser)
|
||||
@@ -395,7 +390,7 @@ public class BackupManager {
|
||||
|
||||
public CompletableFuture<Void> delete(final AuthenticatedBackupUser backupUser,
|
||||
final List<StorageDescriptor> storageDescriptors) {
|
||||
checkBackupTier(backupUser, BackupTier.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
|
||||
if (storageDescriptors.stream().anyMatch(sd -> sd.cdn() != remoteStorageManager.cdnNumber())) {
|
||||
throw Status.INVALID_ARGUMENT
|
||||
@@ -556,7 +551,7 @@ public class BackupManager {
|
||||
|
||||
interface PresentationSignatureVerifier {
|
||||
|
||||
BackupTier verifySignature(byte[] signature, ECPublicKey publicKey);
|
||||
BackupLevel verifySignature(byte[] signature, ECPublicKey publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -588,27 +583,19 @@ public class BackupManager {
|
||||
.withDescription("backup auth credential presentation signature verification failed")
|
||||
.asRuntimeException();
|
||||
}
|
||||
return BackupTier
|
||||
.fromReceiptLevel(presentation.getReceiptLevel())
|
||||
.orElseThrow(() -> {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||
FAILURE_REASON_TAG_NAME, "invalid_receipt_level")
|
||||
.increment();
|
||||
return Status.PERMISSION_DENIED.withDescription("invalid receipt level").asRuntimeException();
|
||||
});
|
||||
return presentation.getBackupLevel();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the authenticated backup user is authorized to use the provided backupTier
|
||||
* Check that the authenticated backup user is authorized to use the provided backupLevel
|
||||
*
|
||||
* @param backupUser The backup user to check
|
||||
* @param backupTier The authorization level to verify the backupUser has access to
|
||||
* @throws {@link Status#PERMISSION_DENIED} error if the backup user is not authorized to access {@code backupTier}
|
||||
* @param backupLevel The authorization level to verify the backupUser has access to
|
||||
* @throws {@link Status#PERMISSION_DENIED} error if the backup user is not authorized to access {@code backupLevel}
|
||||
*/
|
||||
private static void checkBackupTier(final AuthenticatedBackupUser backupUser, final BackupTier backupTier) {
|
||||
if (backupUser.backupTier().compareTo(backupTier) < 0) {
|
||||
private static void checkBackupLevel(final AuthenticatedBackupUser backupUser, final BackupLevel backupLevel) {
|
||||
if (backupUser.backupLevel().compareTo(backupLevel) < 0) {
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||
throw Status.PERMISSION_DENIED
|
||||
.withDescription("credential does not support the requested operation")
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Maps receipt levels to BackupTiers. Existing receipt levels should never be remapped to a different tier.
|
||||
* <p>
|
||||
* Today, receipt levels 1:1 correspond to tiers, but in the future multiple receipt levels may be accepted for access
|
||||
* to a single tier.
|
||||
*/
|
||||
public enum BackupTier {
|
||||
NONE(0),
|
||||
MESSAGES(200),
|
||||
MEDIA(201);
|
||||
|
||||
private static Map<Long, BackupTier> LOOKUP = Arrays.stream(BackupTier.values())
|
||||
.collect(Collectors.toMap(BackupTier::getReceiptLevel, Function.identity()));
|
||||
private long receiptLevel;
|
||||
|
||||
BackupTier(long receiptLevel) {
|
||||
this.receiptLevel = receiptLevel;
|
||||
}
|
||||
|
||||
long getReceiptLevel() {
|
||||
return receiptLevel;
|
||||
}
|
||||
|
||||
public static Optional<BackupTier> fromReceiptLevel(long receiptLevel) {
|
||||
return Optional.ofNullable(LOOKUP.get(receiptLevel));
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
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;
|
||||
@@ -45,10 +46,10 @@ import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
||||
* Tracks backup metadata in a persistent store.
|
||||
* <p>
|
||||
* It's assumed that the caller has already validated that the backupUser being operated on has valid credentials and
|
||||
* possesses the appropriate {@link BackupTier} to perform the current operation.
|
||||
* possesses the appropriate {@link BackupLevel} to perform the current operation.
|
||||
* <p>
|
||||
* Backup records track two timestamps indicating the last time that a user interacted with their backup. One for the
|
||||
* last refresh that contained a credential including media tier, and the other for any access. After a period of
|
||||
* last refresh that contained a credential including media level, and the other for any access. After a period of
|
||||
* inactivity stale backups can be purged (either just the media, or the entire backup). Callers can discover what
|
||||
* backups are stale and whether only the media or the entire backup is stale via {@link #getExpiredBackups}.
|
||||
* <p>
|
||||
@@ -86,7 +87,7 @@ public class BackupsDb {
|
||||
// garbage collection of archive objects.
|
||||
public static final String ATTR_LAST_REFRESH = "R";
|
||||
// N: Time in seconds since epoch of the last backup media refresh. This timestamp can only be updated if the client
|
||||
// has BackupTier.MEDIA, and must be periodically updated to avoid garbage collection of media objects.
|
||||
// has BackupLevel.MEDIA, and must be periodically updated to avoid garbage collection of media objects.
|
||||
public static final String ATTR_LAST_MEDIA_REFRESH = "MR";
|
||||
// B: A 32 byte public key that should be used to sign the presentation used to authenticate requests against the
|
||||
// backup-id
|
||||
@@ -120,18 +121,18 @@ public class BackupsDb {
|
||||
/**
|
||||
* Set the public key associated with a backupId.
|
||||
*
|
||||
* @param authenticatedBackupId The backup-id bytes that should be associated with the provided public key
|
||||
* @param authenticatedBackupTier The backup tier
|
||||
* @param publicKey The public key to associate with the backup id
|
||||
* @param authenticatedBackupId The backup-id bytes that should be associated with the provided public key
|
||||
* @param authenticatedBackupLevel The backup level
|
||||
* @param publicKey The public key to associate with the backup id
|
||||
* @return A stage that completes when the public key has been set. If the backup-id already has a set public key that
|
||||
* does not match, the stage will be completed exceptionally with a {@link PublicKeyConflictException}
|
||||
*/
|
||||
CompletableFuture<Void> setPublicKey(
|
||||
final byte[] authenticatedBackupId,
|
||||
final BackupTier authenticatedBackupTier,
|
||||
final BackupLevel authenticatedBackupLevel,
|
||||
final ECPublicKey publicKey) {
|
||||
final byte[] hashedBackupId = hashedBackupId(authenticatedBackupId);
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, authenticatedBackupTier, hashedBackupId)
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, authenticatedBackupLevel, hashedBackupId)
|
||||
.addSetExpression("#publicKey = :publicKey",
|
||||
Map.entry("#publicKey", ATTR_PUBLIC_KEY),
|
||||
Map.entry(":publicKey", AttributeValues.b(publicKey.serialize())))
|
||||
@@ -284,7 +285,7 @@ public class BackupsDb {
|
||||
final byte[] hashedBackupId = hashedBackupId(backupUser);
|
||||
|
||||
// Clear usage metadata, swap names of things we intend to delete, and record our intent to delete in attr_expired_prefix
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupTier.MEDIA, hashedBackupId)
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.MEDIA, hashedBackupId)
|
||||
.clearMediaUsage(clock)
|
||||
.expireDirectoryNames(secureRandom, ExpiredBackup.ExpirationType.ALL)
|
||||
.setRefreshTimes(Instant.ofEpochSecond(0))
|
||||
@@ -299,7 +300,7 @@ public class BackupsDb {
|
||||
// is toggling backups on and off. In this case, it should be pretty cheap to directly delete the backup.
|
||||
// Instead of changing the backupDir, just make sure the row has expired/ timestamps and tell the caller we
|
||||
// couldn't schedule the deletion.
|
||||
dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupTier.MEDIA, hashedBackupId)
|
||||
dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.MEDIA, hashedBackupId)
|
||||
.setRefreshTimes(Instant.ofEpochSecond(0))
|
||||
.updateItemBuilder()
|
||||
.build())
|
||||
@@ -398,7 +399,7 @@ public class BackupsDb {
|
||||
}
|
||||
|
||||
// Clear usage metadata, swap names of things we intend to delete, and record our intent to delete in attr_expired_prefix
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupTier.MEDIA, expiredBackup.hashedBackupId())
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.MEDIA, expiredBackup.hashedBackupId())
|
||||
.clearMediaUsage(clock)
|
||||
.expireDirectoryNames(secureRandom, expiredBackup.expirationType())
|
||||
.addRemoveExpression(Map.entry("#mediaRefresh", ATTR_LAST_MEDIA_REFRESH))
|
||||
@@ -432,7 +433,7 @@ public class BackupsDb {
|
||||
.build())
|
||||
.thenRun(Util.NOOP);
|
||||
} else {
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupTier.MEDIA, hashedBackupId)
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.MEDIA, hashedBackupId)
|
||||
.addRemoveExpression(Map.entry("#expiredPrefixes", ATTR_EXPIRED_PREFIX))
|
||||
.updateItemBuilder()
|
||||
.build())
|
||||
@@ -539,17 +540,17 @@ public class BackupsDb {
|
||||
private final Map<String, String> attrNames = new HashMap<>();
|
||||
|
||||
private final String tableName;
|
||||
private final BackupTier backupTier;
|
||||
private final BackupLevel backupLevel;
|
||||
private final byte[] hashedBackupId;
|
||||
private String conditionExpression = null;
|
||||
|
||||
static UpdateBuilder forUser(String tableName, AuthenticatedBackupUser backupUser) {
|
||||
return new UpdateBuilder(tableName, backupUser.backupTier(), hashedBackupId(backupUser));
|
||||
return new UpdateBuilder(tableName, backupUser.backupLevel(), hashedBackupId(backupUser));
|
||||
}
|
||||
|
||||
UpdateBuilder(String tableName, BackupTier backupTier, byte[] hashedBackupId) {
|
||||
UpdateBuilder(String tableName, BackupLevel backupLevel, byte[] hashedBackupId) {
|
||||
this.tableName = tableName;
|
||||
this.backupTier = backupTier;
|
||||
this.backupLevel = backupLevel;
|
||||
this.hashedBackupId = hashedBackupId;
|
||||
}
|
||||
|
||||
@@ -679,7 +680,7 @@ public class BackupsDb {
|
||||
* Set the lastRefresh time as part of the update
|
||||
* <p>
|
||||
* This always updates lastRefreshTime, and updates lastMediaRefreshTime if the backup user has the appropriate
|
||||
* tier.
|
||||
* level.
|
||||
*/
|
||||
UpdateBuilder setRefreshTimes(final Clock clock) {
|
||||
return this.setRefreshTimes(clock.instant());
|
||||
@@ -690,8 +691,8 @@ public class BackupsDb {
|
||||
Map.entry("#lastRefreshTime", ATTR_LAST_REFRESH),
|
||||
Map.entry(":lastRefreshTime", AttributeValues.n(refreshTime.getEpochSecond())));
|
||||
|
||||
if (backupTier.compareTo(BackupTier.MEDIA) >= 0) {
|
||||
// update the media time if we have the appropriate tier
|
||||
if (backupLevel.compareTo(BackupLevel.MEDIA) >= 0) {
|
||||
// update the media time if we have the appropriate level
|
||||
addSetExpression("#lastMediaRefreshTime = :lastMediaRefreshTime",
|
||||
Map.entry("#lastMediaRefreshTime", ATTR_LAST_MEDIA_REFRESH),
|
||||
Map.entry(":lastMediaRefreshTime", AttributeValues.n(refreshTime.getEpochSecond())));
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a backup that requires some or all of its content to be deleted
|
||||
|
||||
@@ -10,17 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.collect.Sets;
|
||||
import io.dropwizard.validation.ValidationMethod;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupLevelUtil;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupTier;
|
||||
|
||||
public class SubscriptionConfiguration {
|
||||
|
||||
@@ -78,11 +79,11 @@ public class SubscriptionConfiguration {
|
||||
// We have a tier for all configured backup levels
|
||||
final boolean backupLevelsMatch = backupLevels.keySet()
|
||||
.stream()
|
||||
.allMatch(level -> BackupTier.fromReceiptLevel(level).orElse(BackupTier.NONE) != BackupTier.NONE);
|
||||
.allMatch(SubscriptionConfiguration::isValidBackupLevel);
|
||||
|
||||
// None of the donation levels correspond to backup levels
|
||||
final boolean donationLevelsDontMatch = donationLevels.keySet().stream()
|
||||
.allMatch(level -> BackupTier.fromReceiptLevel(level).orElse(BackupTier.NONE) == BackupTier.NONE);
|
||||
.allMatch(Predicate.not(SubscriptionConfiguration::isValidBackupLevel));
|
||||
|
||||
// The configured donation and backup levels don't intersect
|
||||
final boolean levelsDontIntersect = Sets.intersection(backupLevels.keySet(), donationLevels.keySet()).isEmpty();
|
||||
@@ -105,4 +106,13 @@ public class SubscriptionConfiguration {
|
||||
Set<String> currencies = any.get().prices().keySet();
|
||||
return subscriptionLevels.values().stream().allMatch(level -> currencies.equals(level.prices().keySet()));
|
||||
}
|
||||
|
||||
private static boolean isValidBackupLevel(final long receiptLevel) {
|
||||
try {
|
||||
BackupLevelUtil.fromReceiptLevel(receiptLevel);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user