mirror of
https://github.com/signalapp/Signal-Server
synced 2026-08-18 19:32:16 +01:00
Unify TTL handling of redeemed receipts
This commit is contained in:
@@ -138,7 +138,6 @@ dynamoDbTables:
|
||||
tableName: Example_PushNotificationExperimentSamples
|
||||
redeemedReceipts:
|
||||
tableName: Example_RedeemedReceipts
|
||||
expiration: P30D # Duration of time until rows expire
|
||||
registrationRecovery:
|
||||
tableName: Example_RegistrationRecovery
|
||||
expiration: P300D # Duration of time until rows expire
|
||||
|
||||
@@ -545,8 +545,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
RedeemedReceiptsManager redeemedReceiptsManager = new RedeemedReceiptsManager(clock,
|
||||
config.getDynamoDbTables().getRedeemedReceipts().getTableName(),
|
||||
dynamoDbClient,
|
||||
config.getDynamoDbTables().getRedeemedReceipts().getExpiration());
|
||||
dynamoDbClient);
|
||||
|
||||
Accounts accounts = new Accounts(
|
||||
clock,
|
||||
|
||||
+1
-1
@@ -266,7 +266,7 @@ public class BackupAuthManager {
|
||||
}
|
||||
|
||||
boolean receiptAllowed = redeemedReceiptsManager
|
||||
.put(receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, account.getAccountIdentifier());
|
||||
.put(receiptSerial, receiptExpiration, receiptLevel, account.getAccountIdentifier());
|
||||
if (!receiptAllowed) {
|
||||
throw new BackupBadReceiptException("receipt serial is already redeemed");
|
||||
}
|
||||
|
||||
+3
-3
@@ -69,7 +69,7 @@ public class DynamoDbTables {
|
||||
private final Table profilesV2;
|
||||
private final Table pushChallenge;
|
||||
private final Table pushNotificationExperimentSamples;
|
||||
private final TableWithExpiration redeemedReceipts;
|
||||
private final Table redeemedReceipts;
|
||||
private final TableWithExpiration registrationRecovery;
|
||||
private final Table remoteConfig;
|
||||
private final Table reportMessage;
|
||||
@@ -100,7 +100,7 @@ public class DynamoDbTables {
|
||||
@JsonProperty("profilesV2") final Table profilesV2,
|
||||
@JsonProperty("pushChallenge") final Table pushChallenge,
|
||||
@JsonProperty("pushNotificationExperimentSamples") final Table pushNotificationExperimentSamples,
|
||||
@JsonProperty("redeemedReceipts") final TableWithExpiration redeemedReceipts,
|
||||
@JsonProperty("redeemedReceipts") final Table redeemedReceipts,
|
||||
@JsonProperty("registrationRecovery") final TableWithExpiration registrationRecovery,
|
||||
@JsonProperty("remoteConfig") final Table remoteConfig,
|
||||
@JsonProperty("reportMessage") final Table reportMessage,
|
||||
@@ -273,7 +273,7 @@ public class DynamoDbTables {
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public TableWithExpiration getRedeemedReceipts() {
|
||||
public Table getRedeemedReceipts() {
|
||||
return redeemedReceipts;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ public class DonationController {
|
||||
}
|
||||
|
||||
final boolean receiptMatched = redeemedReceiptsManager.put(
|
||||
receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, auth.accountIdentifier());
|
||||
receiptSerial, receiptExpiration, receiptLevel, auth.accountIdentifier());
|
||||
if (!receiptMatched) {
|
||||
return Response.status(Status.BAD_REQUEST)
|
||||
.entity("receipt serial is already redeemed")
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ public class DonationsGrpcService extends SimpleDonationsGrpc.DonationsImplBase
|
||||
throw GrpcExceptions.unavailable("server does not recognize the requested receipt level");
|
||||
}
|
||||
final boolean receiptMatched = redeemedReceiptsManager.put(
|
||||
receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, authenticatedDevice.accountIdentifier());
|
||||
receiptSerial, receiptExpiration, receiptLevel, authenticatedDevice.accountIdentifier());
|
||||
if (!receiptMatched) {
|
||||
return RedeemReceiptResponse.newBuilder()
|
||||
.setAlreadyRedeemed(FailedPrecondition.newBuilder()
|
||||
|
||||
@@ -334,7 +334,7 @@ public class Accounts {
|
||||
|
||||
if (conditionalCheckFailed(receiptSerialConstraintCancellationReason)) {
|
||||
final UUID existingAccountUuid = UUIDUtil.fromByteBuffer(receiptSerialConstraintCancellationReason.item()
|
||||
.get(RedeemedReceiptsManager.KEY_ACCOUNT_UUID).b().asByteBuffer());
|
||||
.get(RedeemedReceiptsManager.ATTR_ACCOUNT_UUID).b().asByteBuffer());
|
||||
final Account existingAccount = getByAccountIdentifier(existingAccountUuid)
|
||||
// The account was already deleted, and we don't allow re-registering with the same receipt
|
||||
.orElseThrow(ReceiptAlreadyRedeemedException::new);
|
||||
|
||||
+33
-31
@@ -12,6 +12,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nonnull;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptSerial;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
@@ -27,26 +28,26 @@ import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;
|
||||
public class RedeemedReceiptsManager {
|
||||
|
||||
public static final String KEY_SERIAL = "S";
|
||||
public static final String KEY_TTL = "E";
|
||||
public static final String KEY_RECEIPT_EXPIRATION = "G";
|
||||
public static final String KEY_RECEIPT_LEVEL = "L";
|
||||
public static final String KEY_ACCOUNT_UUID = "U";
|
||||
public static final String KEY_REDEMPTION_TIME = "R";
|
||||
public static final String ATTR_TTL = "E";
|
||||
public static final String ATTR_RECEIPT_EXPIRATION = "G";
|
||||
public static final String ATTR_RECEIPT_LEVEL = "L";
|
||||
public static final String ATTR_ACCOUNT_UUID = "U";
|
||||
public static final String ATTR_REDEMPTION_TIME = "R";
|
||||
|
||||
private final Clock clock;
|
||||
private final String table;
|
||||
private final DynamoDbClient client;
|
||||
private final Duration expirationTime;
|
||||
|
||||
@VisibleForTesting
|
||||
static final Duration TTL_PADDING = Duration.ofDays(30);
|
||||
|
||||
public RedeemedReceiptsManager(
|
||||
@Nonnull final Clock clock,
|
||||
@Nonnull final String table,
|
||||
@Nonnull final DynamoDbClient client,
|
||||
@Nonnull final Duration expirationTime) {
|
||||
@Nonnull final DynamoDbClient client) {
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
this.table = Objects.requireNonNull(table);
|
||||
this.client = Objects.requireNonNull(client);
|
||||
this.expirationTime = Objects.requireNonNull(expirationTime);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +57,7 @@ public class RedeemedReceiptsManager {
|
||||
*/
|
||||
public boolean put(
|
||||
@Nonnull final ReceiptSerial receiptSerial,
|
||||
final long receiptExpiration,
|
||||
final Instant receiptExpiration,
|
||||
final long receiptLevel,
|
||||
@Nonnull final UUID accountUuid) {
|
||||
|
||||
@@ -65,7 +66,6 @@ public class RedeemedReceiptsManager {
|
||||
Objects.requireNonNull(accountUuid);
|
||||
|
||||
final Instant now = clock.instant();
|
||||
final Instant rowExpiration = now.plus(expirationTime);
|
||||
final AttributeValue serialAttributeValue = AttributeValues.b(receiptSerial.serialize());
|
||||
|
||||
final UpdateItemRequest updateItemRequest = UpdateItemRequest.builder()
|
||||
@@ -78,14 +78,14 @@ public class RedeemedReceiptsManager {
|
||||
+ "#account_uuid = if_not_exists(#account_uuid, :account_uuid), "
|
||||
+ "#redemption_time = if_not_exists(#redemption_time, :redemption_time)")
|
||||
.expressionAttributeNames(Map.of(
|
||||
"#ttl", KEY_TTL,
|
||||
"#receipt_expiration", KEY_RECEIPT_EXPIRATION,
|
||||
"#receipt_level", KEY_RECEIPT_LEVEL,
|
||||
"#account_uuid", KEY_ACCOUNT_UUID,
|
||||
"#redemption_time", KEY_REDEMPTION_TIME))
|
||||
"#ttl", ATTR_TTL,
|
||||
"#receipt_expiration", ATTR_RECEIPT_EXPIRATION,
|
||||
"#receipt_level", ATTR_RECEIPT_LEVEL,
|
||||
"#account_uuid", ATTR_ACCOUNT_UUID,
|
||||
"#redemption_time", ATTR_REDEMPTION_TIME))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":ttl", AttributeValues.n(rowExpiration.getEpochSecond()),
|
||||
":receipt_expiration", AttributeValues.n(receiptExpiration),
|
||||
":ttl", rowTtl(receiptExpiration),
|
||||
":receipt_expiration", AttributeValues.n(receiptExpiration.getEpochSecond()),
|
||||
":receipt_level", AttributeValues.n(receiptLevel),
|
||||
":account_uuid", AttributeValues.b(accountUuid),
|
||||
":redemption_time", AttributeValues.n(now.getEpochSecond())))
|
||||
@@ -93,10 +93,10 @@ public class RedeemedReceiptsManager {
|
||||
final UpdateItemResponse updateItemResponse = client.updateItem(updateItemRequest);
|
||||
|
||||
final Map<String, AttributeValue> attributes = updateItemResponse.attributes();
|
||||
final long ddbReceiptExpiration = Long.parseLong(attributes.get(KEY_RECEIPT_EXPIRATION).n());
|
||||
final long ddbReceiptLevel = Long.parseLong(attributes.get(KEY_RECEIPT_LEVEL).n());
|
||||
final UUID ddbAccountUuid = UUIDUtil.fromByteBuffer(attributes.get(KEY_ACCOUNT_UUID).b().asByteBuffer());
|
||||
return ddbReceiptExpiration == receiptExpiration && ddbReceiptLevel == receiptLevel &&
|
||||
final long ddbReceiptExpiration = Long.parseLong(attributes.get(ATTR_RECEIPT_EXPIRATION).n());
|
||||
final long ddbReceiptLevel = Long.parseLong(attributes.get(ATTR_RECEIPT_LEVEL).n());
|
||||
final UUID ddbAccountUuid = UUIDUtil.fromByteBuffer(attributes.get(ATTR_ACCOUNT_UUID).b().asByteBuffer());
|
||||
return ddbReceiptExpiration == receiptExpiration.getEpochSecond() && ddbReceiptLevel == receiptLevel &&
|
||||
Objects.equals(ddbAccountUuid, accountUuid);
|
||||
}
|
||||
|
||||
@@ -119,18 +119,16 @@ public class RedeemedReceiptsManager {
|
||||
.tableName(table)
|
||||
.item(Map.of(
|
||||
KEY_SERIAL, AttributeValues.b(receiptSerial.serialize()),
|
||||
// NOTE: this differs from the expiration for other receipt types (see RedeemedReceiptsManager#put) which
|
||||
// sets the expiration to now + a static expiration time. The TTL handling needs to be unified in the future.
|
||||
KEY_TTL, AttributeValues.n(receiptExpiration.getEpochSecond()),
|
||||
KEY_RECEIPT_EXPIRATION, AttributeValues.n(receiptExpiration.getEpochSecond()),
|
||||
KEY_RECEIPT_LEVEL, AttributeValues.n(receiptLevel),
|
||||
KEY_ACCOUNT_UUID, AttributeValues.b(accountUuid),
|
||||
KEY_REDEMPTION_TIME, AttributeValues.n(clock.instant().getEpochSecond())))
|
||||
ATTR_TTL, rowTtl(receiptExpiration),
|
||||
ATTR_RECEIPT_EXPIRATION, AttributeValues.n(receiptExpiration.getEpochSecond()),
|
||||
ATTR_RECEIPT_LEVEL, AttributeValues.n(receiptLevel),
|
||||
ATTR_ACCOUNT_UUID, AttributeValues.b(accountUuid),
|
||||
ATTR_REDEMPTION_TIME, AttributeValues.n(clock.instant().getEpochSecond())))
|
||||
.conditionExpression("attribute_not_exists(#serial) OR (#account_uuid = :account_uuid AND #receipt_level = :receipt_level)")
|
||||
.expressionAttributeNames(Map.of(
|
||||
"#serial", KEY_SERIAL,
|
||||
"#account_uuid", KEY_ACCOUNT_UUID,
|
||||
"#receipt_level", KEY_RECEIPT_LEVEL))
|
||||
"#account_uuid", ATTR_ACCOUNT_UUID,
|
||||
"#receipt_level", ATTR_RECEIPT_LEVEL))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":account_uuid", AttributeValues.b(accountUuid),
|
||||
":receipt_level", AttributeValues.n(receiptLevel)
|
||||
@@ -139,4 +137,8 @@ public class RedeemedReceiptsManager {
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static AttributeValue rowTtl(final Instant receiptExpiration) {
|
||||
return AttributeValues.n(receiptExpiration.plus(TTL_PADDING).getEpochSecond());
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -263,8 +263,7 @@ public record CommandDependencies(
|
||||
|
||||
RedeemedReceiptsManager redeemedReceiptsManager = new RedeemedReceiptsManager(clock,
|
||||
configuration.getDynamoDbTables().getRedeemedReceipts().getTableName(),
|
||||
dynamoDbClient,
|
||||
configuration.getDynamoDbTables().getRedeemedReceipts().getExpiration());
|
||||
dynamoDbClient);
|
||||
|
||||
Accounts accounts = new Accounts(
|
||||
clock,
|
||||
|
||||
+4
-4
@@ -340,7 +340,7 @@ public class BackupAuthManagerTest {
|
||||
.build();
|
||||
clock.pin(Instant.EPOCH.plus(Duration.ofDays(1)));
|
||||
when(accountsManager.update(any(Account.class), any())).thenReturn(account);
|
||||
when(redeemedReceiptsManager.put(any(), eq(expirationTime.getEpochSecond()), eq(201L), eq(aci)))
|
||||
when(redeemedReceiptsManager.put(any(), eq(expirationTime), eq(201L), eq(aci)))
|
||||
.thenReturn(true);
|
||||
authManager.redeemReceipt(account, receiptPresentation(201, expirationTime));
|
||||
verify(accountsManager, times(1)).update(any(Account.class), any());
|
||||
@@ -353,7 +353,7 @@ public class BackupAuthManagerTest {
|
||||
final Account account = new MockAccountBuilder().mediaCredential(Optional.empty()).build();
|
||||
|
||||
clock.pin(Instant.EPOCH.plus(Duration.ofDays(1)));
|
||||
when(redeemedReceiptsManager.put(any(), eq(expirationTime.getEpochSecond()), eq(201L), eq(aci)))
|
||||
when(redeemedReceiptsManager.put(any(), eq(expirationTime), eq(201L), eq(aci)))
|
||||
.thenReturn(true);
|
||||
assertThatExceptionOfType(BackupMissingIdCommitmentException.class)
|
||||
.isThrownBy(() -> authManager.redeemReceipt(account, receiptPresentation(201, expirationTime)));
|
||||
@@ -374,7 +374,7 @@ public class BackupAuthManagerTest {
|
||||
|
||||
clock.pin(Instant.EPOCH.plus(Duration.ofDays(1)));
|
||||
when(accountsManager.update(any(Account.class), any())).thenReturn(account);
|
||||
when(redeemedReceiptsManager.put(any(), eq(newExpirationTime.getEpochSecond()), eq(201L), eq(aci)))
|
||||
when(redeemedReceiptsManager.put(any(), eq(newExpirationTime), eq(201L), eq(aci)))
|
||||
.thenReturn(true);
|
||||
authManager.redeemReceipt(account, receiptPresentation(201, newExpirationTime));
|
||||
|
||||
@@ -429,7 +429,7 @@ public class BackupAuthManagerTest {
|
||||
|
||||
clock.pin(Instant.EPOCH.plus(Duration.ofDays(1)));
|
||||
when(accountsManager.update(any(Account.class), any())).thenReturn(account);
|
||||
when(redeemedReceiptsManager.put(any(), eq(expirationTime.getEpochSecond()), eq(201L), eq(aci)))
|
||||
when(redeemedReceiptsManager.put(any(), eq(expirationTime), eq(201L), eq(aci)))
|
||||
.thenReturn(false);
|
||||
|
||||
assertThatExceptionOfType(BackupBadReceiptException.class)
|
||||
|
||||
+2
-2
@@ -138,7 +138,7 @@ class DonationControllerTest {
|
||||
when(receiptCredentialPresentation.getReceiptLevel()).thenReturn(receiptLevel);
|
||||
final long receiptExpiration = nowEpochSeconds + 86400 * 30;
|
||||
when(receiptCredentialPresentation.getReceiptExpirationTime()).thenReturn(receiptExpiration);
|
||||
when(redeemedReceiptsManager.put(same(receiptSerial), eq(receiptExpiration), eq(receiptLevel), eq(AuthHelper.VALID_UUID))).thenReturn(true);
|
||||
when(redeemedReceiptsManager.put(same(receiptSerial), eq(Instant.ofEpochSecond(receiptExpiration)), eq(receiptLevel), eq(AuthHelper.VALID_UUID))).thenReturn(true);
|
||||
when(accountsManager.getByAccountIdentifier(eq(AuthHelper.VALID_UUID)))
|
||||
.thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
|
||||
|
||||
@@ -163,7 +163,7 @@ class DonationControllerTest {
|
||||
when(receiptCredentialPresentation.getReceiptLevel()).thenReturn(receiptLevel);
|
||||
final long receiptExpiration = nowEpochSeconds + 86400 * 30;
|
||||
when(receiptCredentialPresentation.getReceiptExpirationTime()).thenReturn(receiptExpiration);
|
||||
when(redeemedReceiptsManager.put(same(receiptSerial), eq(receiptExpiration), eq(receiptLevel), eq(AuthHelper.VALID_UUID))).thenReturn(false);
|
||||
when(redeemedReceiptsManager.put(same(receiptSerial), eq(Instant.ofEpochSecond(receiptExpiration)), eq(receiptLevel), eq(AuthHelper.VALID_UUID))).thenReturn(false);
|
||||
when(accountsManager.getByAccountIdentifier(eq(AuthHelper.VALID_UUID)))
|
||||
.thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
|
||||
|
||||
|
||||
+5
-5
@@ -84,7 +84,7 @@ class DonationsGrpcServiceTest extends SimpleBaseGrpcTest<DonationsGrpcService,
|
||||
|
||||
private final TestClock clock = TestClock.pinned(Instant.ofEpochSecond(100));
|
||||
|
||||
private static final long EXPIRATION_TIME_EPOCH_SECONDS = 200;
|
||||
private static final Instant EXPIRATION_TIME = Instant.ofEpochSecond(200);
|
||||
|
||||
@Override
|
||||
protected DonationsGrpcService createServiceBeforeEachTest() {
|
||||
@@ -98,7 +98,7 @@ class DonationsGrpcServiceTest extends SimpleBaseGrpcTest<DonationsGrpcService,
|
||||
}
|
||||
when(receiptCredentialPresentation.getReceiptLevel()).thenReturn(ReceiptLevel.ONE_TIME_DONATION.getValue());
|
||||
when(badgesConfiguration.getReceiptLevels()).thenReturn(Map.of(ReceiptLevel.ONE_TIME_DONATION, "testBadge"));
|
||||
when(receiptCredentialPresentation.getReceiptExpirationTime()).thenReturn(EXPIRATION_TIME_EPOCH_SECONDS);
|
||||
when(receiptCredentialPresentation.getReceiptExpirationTime()).thenReturn(EXPIRATION_TIME.getEpochSecond());
|
||||
when(receiptCredentialPresentation.getReceiptSerial()).thenReturn(receiptSerial);
|
||||
|
||||
donationPermitsManager = new DonationPermitsManager(mock(DonationPermits.class), donationsPermitSecretParams,
|
||||
@@ -123,7 +123,7 @@ class DonationsGrpcServiceTest extends SimpleBaseGrpcTest<DonationsGrpcService,
|
||||
void redeemReceipt(
|
||||
@CartesianTest.Values(booleans = {true, false}) final boolean isVisible,
|
||||
@CartesianTest.Values(booleans = {true, false}) final boolean isPrimary) {
|
||||
when(redeemedReceiptsManager.put(receiptSerial, EXPIRATION_TIME_EPOCH_SECONDS, 1, AUTHENTICATED_ACI)).thenReturn(
|
||||
when(redeemedReceiptsManager.put(receiptSerial, EXPIRATION_TIME, 1, AUTHENTICATED_ACI)).thenReturn(
|
||||
true);
|
||||
|
||||
final RedeemReceiptResponse response = authenticatedServiceStub().redeemReceipt(RedeemReceiptRequest.newBuilder()
|
||||
@@ -135,7 +135,7 @@ class DonationsGrpcServiceTest extends SimpleBaseGrpcTest<DonationsGrpcService,
|
||||
assertEquals(RedeemReceiptResponse.ResponseCase.SUCCESS, response.getResponseCase());
|
||||
|
||||
verify(account).addBadge(clock,
|
||||
new AccountBadge("testBadge", Instant.ofEpochSecond(EXPIRATION_TIME_EPOCH_SECONDS), isVisible));
|
||||
new AccountBadge("testBadge", EXPIRATION_TIME, isVisible));
|
||||
if (isPrimary) {
|
||||
verify(account).makeBadgePrimaryIfExists(clock, "testBadge");
|
||||
} else {
|
||||
@@ -145,7 +145,7 @@ class DonationsGrpcServiceTest extends SimpleBaseGrpcTest<DonationsGrpcService,
|
||||
|
||||
@Test
|
||||
void alreadyRedeemed() {
|
||||
when(redeemedReceiptsManager.put(receiptSerial, EXPIRATION_TIME_EPOCH_SECONDS, 1, AUTHENTICATED_ACI)).thenReturn(
|
||||
when(redeemedReceiptsManager.put(receiptSerial, EXPIRATION_TIME, 1, AUTHENTICATED_ACI)).thenReturn(
|
||||
false);
|
||||
|
||||
final RedeemReceiptResponse response = authenticatedServiceStub().redeemReceipt(RedeemReceiptRequest.newBuilder()
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ public class AccountCreationDeletionIntegrationTest {
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||
new RedeemedReceiptsManager(CLOCK, DynamoDbExtensionSchema.Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(), Duration.ofDays(30)),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient()),
|
||||
DynamoDbExtensionSchema.Tables.ACCOUNTS.tableName(),
|
||||
DynamoDbExtensionSchema.Tables.NUMBERS.tableName(),
|
||||
DynamoDbExtensionSchema.Tables.PNI_ASSIGNMENTS.tableName(),
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||
new RedeemedReceiptsManager(Clock.systemUTC(), Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(), Duration.ofDays(30)),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient()),
|
||||
Tables.ACCOUNTS.tableName(),
|
||||
Tables.NUMBERS.tableName(),
|
||||
Tables.PNI_ASSIGNMENTS.tableName(),
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||
new RedeemedReceiptsManager(Clock.systemUTC(), Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(), Duration.ofDays(30)),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient()),
|
||||
Tables.ACCOUNTS.tableName(),
|
||||
Tables.NUMBERS.tableName(),
|
||||
Tables.PNI_ASSIGNMENTS.tableName(),
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ public class AccountsManagerMigrateRecoveryPasswordIntegrationTest {
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||
new RedeemedReceiptsManager(Clock.systemUTC(), DynamoDbExtensionSchema.Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(), Duration.ofDays(30)),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient()),
|
||||
DynamoDbExtensionSchema.Tables.ACCOUNTS.tableName(),
|
||||
DynamoDbExtensionSchema.Tables.NUMBERS.tableName(),
|
||||
DynamoDbExtensionSchema.Tables.PNI_ASSIGNMENTS.tableName(),
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ class AccountsManagerUsernameIntegrationTest {
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||
new RedeemedReceiptsManager(Clock.systemUTC(), Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(), Duration.ofDays(30)),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient()),
|
||||
Tables.ACCOUNTS.tableName(),
|
||||
Tables.NUMBERS.tableName(),
|
||||
Tables.PNI_ASSIGNMENTS.tableName(),
|
||||
|
||||
@@ -147,7 +147,7 @@ class AccountsTest {
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||
new RedeemedReceiptsManager(clock, Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(), Duration.ofDays(30)),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient()),
|
||||
Tables.ACCOUNTS.tableName(),
|
||||
Tables.NUMBERS.tableName(),
|
||||
Tables.PNI_ASSIGNMENTS.tableName(),
|
||||
@@ -880,7 +880,7 @@ class AccountsTest {
|
||||
dynamoDbClient,
|
||||
mock(DynamoDbAsyncClient.class),
|
||||
new RedeemedReceiptsManager(clock, Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
dynamoDbClient, Duration.ofDays(30)),
|
||||
dynamoDbClient),
|
||||
Tables.ACCOUNTS.tableName(),
|
||||
Tables.NUMBERS.tableName(),
|
||||
Tables.PNI_ASSIGNMENTS.tableName(),
|
||||
@@ -986,7 +986,7 @@ class AccountsTest {
|
||||
dynamoDbClient,
|
||||
mock(DynamoDbAsyncClient.class),
|
||||
new RedeemedReceiptsManager(clock, Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
dynamoDbClient, Duration.ofDays(30)),
|
||||
dynamoDbClient),
|
||||
Tables.ACCOUNTS.tableName(),
|
||||
Tables.NUMBERS.tableName(),
|
||||
Tables.PNI_ASSIGNMENTS.tableName(),
|
||||
@@ -1444,7 +1444,7 @@ class AccountsTest {
|
||||
dynamoDbClient,
|
||||
mock(DynamoDbAsyncClient.class),
|
||||
new RedeemedReceiptsManager(clock, Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
dynamoDbClient, Duration.ofDays(30)),
|
||||
dynamoDbClient),
|
||||
Tables.ACCOUNTS.tableName(),
|
||||
Tables.NUMBERS.tableName(),
|
||||
Tables.PNI_ASSIGNMENTS.tableName(),
|
||||
@@ -1492,7 +1492,7 @@ class AccountsTest {
|
||||
dynamoDbClient,
|
||||
mock(DynamoDbAsyncClient.class),
|
||||
new RedeemedReceiptsManager(clock, Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
dynamoDbClient, Duration.ofDays(30)),
|
||||
dynamoDbClient),
|
||||
Tables.ACCOUNTS.tableName(),
|
||||
Tables.NUMBERS.tableName(),
|
||||
Tables.PNI_ASSIGNMENTS.tableName(),
|
||||
@@ -1598,7 +1598,7 @@ class AccountsTest {
|
||||
dynamoDbClient,
|
||||
mock(DynamoDbAsyncClient.class),
|
||||
new RedeemedReceiptsManager(clock, Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
dynamoDbClient, Duration.ofDays(30)),
|
||||
dynamoDbClient),
|
||||
Tables.ACCOUNTS.tableName(),
|
||||
Tables.NUMBERS.tableName(),
|
||||
Tables.PNI_ASSIGNMENTS.tableName(),
|
||||
@@ -2378,9 +2378,9 @@ class AccountsTest {
|
||||
.build()
|
||||
);
|
||||
assertThat(receiptConstraintResponse.hasItem()).isTrue();
|
||||
assertThat(AttributeValues.getUUID(receiptConstraintResponse.item(), RedeemedReceiptsManager.KEY_ACCOUNT_UUID, null)).isEqualTo(accountId);
|
||||
assertThat(AttributeValues.getLong(receiptConstraintResponse.item(), RedeemedReceiptsManager.KEY_RECEIPT_LEVEL, -1)).isEqualTo(receiptCredentialPresentation.getReceiptLevel());
|
||||
assertThat(AttributeValues.getLong(receiptConstraintResponse.item(), RedeemedReceiptsManager.KEY_RECEIPT_EXPIRATION, -1)).isEqualTo(receiptCredentialPresentation.getReceiptExpirationTime());
|
||||
assertThat(AttributeValues.getUUID(receiptConstraintResponse.item(), RedeemedReceiptsManager.ATTR_ACCOUNT_UUID, null)).isEqualTo(accountId);
|
||||
assertThat(AttributeValues.getLong(receiptConstraintResponse.item(), RedeemedReceiptsManager.ATTR_RECEIPT_LEVEL, -1)).isEqualTo(receiptCredentialPresentation.getReceiptLevel());
|
||||
assertThat(AttributeValues.getLong(receiptConstraintResponse.item(), RedeemedReceiptsManager.ATTR_RECEIPT_EXPIRATION, -1)).isEqualTo(receiptCredentialPresentation.getReceiptExpirationTime());
|
||||
}
|
||||
|
||||
private Map<String, AttributeValue> readAccount(final UUID uuid) {
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ public class AddRemoveDeviceIntegrationTest {
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||
new RedeemedReceiptsManager(clock, DynamoDbExtensionSchema.Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(), Duration.ofDays(30)),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient()),
|
||||
DynamoDbExtensionSchema.Tables.ACCOUNTS.tableName(),
|
||||
DynamoDbExtensionSchema.Tables.NUMBERS.tableName(),
|
||||
DynamoDbExtensionSchema.Tables.PNI_ASSIGNMENTS.tableName(),
|
||||
|
||||
+86
-12
@@ -6,20 +6,29 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptSerial;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import org.whispersystems.textsecuregcm.util.TestClock;
|
||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||
import software.amazon.awssdk.services.dynamodb.model.CancellationReason;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
|
||||
|
||||
class RedeemedReceiptsManagerTest {
|
||||
|
||||
@@ -38,30 +47,95 @@ class RedeemedReceiptsManagerTest {
|
||||
redeemedReceiptsManager = new RedeemedReceiptsManager(
|
||||
clock,
|
||||
Tables.REDEEMED_RECEIPTS.tableName(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient(),
|
||||
Duration.ofDays(90));
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPut() throws ExecutionException, InterruptedException {
|
||||
final long receiptExpiration = 42;
|
||||
void testPut() {
|
||||
final Instant receiptExpiration = Instant.ofEpochSecond(42);
|
||||
final long receiptLevel = 3;
|
||||
final UUID uuid1 = UUID.randomUUID();
|
||||
boolean put;
|
||||
|
||||
// initial insert should return true
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, AuthHelper.VALID_UUID);
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, uuid1);
|
||||
assertThat(put).isTrue();
|
||||
|
||||
// subsequent attempted inserts with modified parameters should return false
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration + 1, receiptLevel, AuthHelper.VALID_UUID);
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration.plusSeconds(1), receiptLevel, uuid1);
|
||||
assertThat(put).isFalse();
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel + 1, AuthHelper.VALID_UUID);
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel + 1, uuid1);
|
||||
assertThat(put).isFalse();
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, AuthHelper.VALID_UUID_TWO);
|
||||
|
||||
final UUID uuid2 = UUID.randomUUID();
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, uuid2);
|
||||
assertThat(put).isFalse();
|
||||
|
||||
// repeated insert attempt of the original parameters should return true
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, AuthHelper.VALID_UUID);
|
||||
put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, uuid1);
|
||||
assertThat(put).isTrue();
|
||||
|
||||
// verify that the TTL is receipt expiration + padding
|
||||
final GetItemResponse response = DYNAMO_DB_EXTENSION.getDynamoDbClient().getItem(GetItemRequest.builder()
|
||||
.tableName(Tables.REDEEMED_RECEIPTS.tableName())
|
||||
.key(Map.of(RedeemedReceiptsManager.KEY_SERIAL, AttributeValues.b(receiptSerial.serialize())))
|
||||
.build());
|
||||
assertThat(Long.parseLong(response.item().get(RedeemedReceiptsManager.ATTR_TTL).n())).isEqualTo(
|
||||
receiptExpiration.plus(RedeemedReceiptsManager.TTL_PADDING).getEpochSecond());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuildTransactWriteItemForReceipt() {
|
||||
final Instant receiptExpiration = Instant.ofEpochSecond(42);
|
||||
final long receiptLevel1 = 3;
|
||||
final UUID uuid1 = UUID.randomUUID();
|
||||
|
||||
final TransactWriteItem writeItem = redeemedReceiptsManager.buildTransactWriteItemForReceipt(receiptSerial,
|
||||
receiptExpiration, receiptLevel1, uuid1);
|
||||
|
||||
assertThatCode(() -> DYNAMO_DB_EXTENSION.getDynamoDbClient().transactWriteItems(TransactWriteItemsRequest.builder()
|
||||
.transactItems(writeItem)
|
||||
.build())).doesNotThrowAnyException();
|
||||
|
||||
// A subsequent write with the same parameters should be idempotent
|
||||
assertThatCode(() -> DYNAMO_DB_EXTENSION.getDynamoDbClient().transactWriteItems(TransactWriteItemsRequest.builder()
|
||||
.transactItems(writeItem)
|
||||
.build())).doesNotThrowAnyException();
|
||||
|
||||
// An attempt to insert the same receipt with a different UUID should fail
|
||||
final UUID uuid2 = UUID.randomUUID();
|
||||
final TransactWriteItem writeItem2 = redeemedReceiptsManager.buildTransactWriteItemForReceipt(receiptSerial,
|
||||
receiptExpiration, receiptLevel1, uuid2);
|
||||
assertThatThrownBy(
|
||||
() -> DYNAMO_DB_EXTENSION.getDynamoDbClient().transactWriteItems(TransactWriteItemsRequest.builder()
|
||||
.transactItems(writeItem2)
|
||||
.build()))
|
||||
.isInstanceOfSatisfying(TransactionCanceledException.class, e -> {
|
||||
final List<CancellationReason> cancellationReasons = e.cancellationReasons();
|
||||
assertThat(cancellationReasons).hasSize(1);
|
||||
assertThat(cancellationReasons.getFirst().code()).isEqualTo("ConditionalCheckFailed");
|
||||
});
|
||||
|
||||
// An attempt to insert the same receipt with the original UUID but a different receipt level should fail
|
||||
final long receiptLevel2 = 4;
|
||||
final TransactWriteItem writeItem3 = redeemedReceiptsManager.buildTransactWriteItemForReceipt(receiptSerial,
|
||||
receiptExpiration, receiptLevel2, uuid1);
|
||||
assertThatThrownBy(
|
||||
() -> DYNAMO_DB_EXTENSION.getDynamoDbClient().transactWriteItems(TransactWriteItemsRequest.builder()
|
||||
.transactItems(writeItem3)
|
||||
.build()))
|
||||
.isInstanceOfSatisfying(TransactionCanceledException.class, e -> {
|
||||
final List<CancellationReason> cancellationReasons = e.cancellationReasons();
|
||||
assertThat(cancellationReasons).hasSize(1);
|
||||
assertThat(cancellationReasons.getFirst().code()).isEqualTo("ConditionalCheckFailed");
|
||||
});
|
||||
|
||||
// verify that the TTL is receipt expiration + padding
|
||||
final GetItemResponse response = DYNAMO_DB_EXTENSION.getDynamoDbClient().getItem(GetItemRequest.builder()
|
||||
.tableName(Tables.REDEEMED_RECEIPTS.tableName())
|
||||
.key(Map.of(RedeemedReceiptsManager.KEY_SERIAL, AttributeValues.b(receiptSerial.serialize())))
|
||||
.build());
|
||||
assertThat(Long.parseLong(response.item().get(RedeemedReceiptsManager.ATTR_TTL).n())).isEqualTo(
|
||||
receiptExpiration.plus(RedeemedReceiptsManager.TTL_PADDING).getEpochSecond());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,6 @@ dynamoDbTables:
|
||||
tableName: Example_PushNotificationExperimentSamples
|
||||
redeemedReceipts:
|
||||
tableName: redeemed_receipts_test
|
||||
expiration: P30D # Duration of time until rows expire
|
||||
registrationRecovery:
|
||||
tableName: registration_recovery_passwords_test
|
||||
expiration: P300D # Duration of time until rows expire
|
||||
|
||||
Reference in New Issue
Block a user