Accept source ACI at `/v1/messages/report`

This commit is contained in:
Chris Eager 2022-03-08 17:34:20 -08:00 committed by Chris Eager
parent fa3a9570d6
commit 77fd01bd9f
14 changed files with 477 additions and 112 deletions

View File

@ -637,8 +637,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new DirectoryV2Controller(directoryV2CredentialsGenerator),
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
ReceiptCredentialPresentation::new, stripeExecutor, config.getDonationConfiguration(), config.getStripe()),
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, apnFallbackManager,
reportMessageManager, multiRecipientMessageExecutor),
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccountsManager,
messagesManager, apnFallbackManager, reportMessageManager, multiRecipientMessageExecutor),
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager, profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations),
new ProvisioningController(rateLimiters, provisioningManager),

View File

@ -88,6 +88,7 @@ import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.redis.RedisOperation;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
@ -105,14 +106,15 @@ public class MessageController {
private static final Logger logger = LoggerFactory.getLogger(MessageController.class);
private final RateLimiters rateLimiters;
private final MessageSender messageSender;
private final ReceiptSender receiptSender;
private final AccountsManager accountsManager;
private final MessagesManager messagesManager;
private final ApnFallbackManager apnFallbackManager;
private final ReportMessageManager reportMessageManager;
private final ExecutorService multiRecipientMessageExecutor;
private final RateLimiters rateLimiters;
private final MessageSender messageSender;
private final ReceiptSender receiptSender;
private final AccountsManager accountsManager;
private final DeletedAccountsManager deletedAccountsManager;
private final MessagesManager messagesManager;
private final ApnFallbackManager apnFallbackManager;
private final ReportMessageManager reportMessageManager;
private final ExecutorService multiRecipientMessageExecutor;
@VisibleForTesting
static final Semver FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE = new Semver("5.22.0");
@ -146,6 +148,7 @@ public class MessageController {
MessageSender messageSender,
ReceiptSender receiptSender,
AccountsManager accountsManager,
DeletedAccountsManager deletedAccountsManager,
MessagesManager messagesManager,
ApnFallbackManager apnFallbackManager,
ReportMessageManager reportMessageManager,
@ -154,6 +157,7 @@ public class MessageController {
this.messageSender = messageSender;
this.receiptSender = receiptSender;
this.accountsManager = accountsManager;
this.deletedAccountsManager = deletedAccountsManager;
this.messagesManager = messagesManager;
this.apnFallbackManager = apnFallbackManager;
this.reportMessageManager = reportMessageManager;
@ -556,11 +560,39 @@ public class MessageController {
@Timed
@POST
@Path("/report/{sourceNumber}/{messageGuid}")
public Response reportMessage(@Auth AuthenticatedAccount auth, @PathParam("sourceNumber") String sourceNumber,
@Path("/report/{source}/{messageGuid}")
public Response reportMessage(@Auth AuthenticatedAccount auth, @PathParam("source") String source,
@PathParam("messageGuid") UUID messageGuid) {
reportMessageManager.report(sourceNumber, messageGuid, auth.getAccount().getUuid());
final Optional<String> sourceNumber;
final Optional<UUID> sourceAci;
final Optional<UUID> sourcePni;
if (source.startsWith("+")) {
sourceNumber = Optional.of(source);
final Optional<Account> maybeAccount = accountsManager.getByE164(source);
if (maybeAccount.isPresent()) {
sourceAci = maybeAccount.map(Account::getUuid);
sourcePni = maybeAccount.map(Account::getPhoneNumberIdentifier);
} else {
sourceAci = deletedAccountsManager.findDeletedAccountAci(source);
sourcePni = Optional.ofNullable(accountsManager.getPhoneNumberIdentifier(source));
}
} else {
sourceAci = Optional.of(UUID.fromString(source));
final Optional<Account> sourceAccount = accountsManager.getByAccountIdentifier(sourceAci.get());
if (sourceAccount.isEmpty()) {
logger.warn("Could not find source: {}", sourceAci.get());
sourceNumber = deletedAccountsManager.findDeletedAccountE164(sourceAci.get());
sourcePni = sourceNumber.map(accountsManager::getPhoneNumberIdentifier);
} else {
sourceNumber = sourceAccount.map(Account::getNumber);
sourcePni = sourceAccount.map(Account::getPhoneNumberIdentifier);
}
}
reportMessageManager.report(sourceNumber, sourceAci, sourcePni, messageGuid, auth.getAccount().getUuid());
return Response.status(Status.ACCEPTED)
.build();

View File

@ -513,6 +513,14 @@ public class AccountsManager {
}
}
public Optional<String> getNumberForPhoneNumberIdentifier(UUID pni) {
return phoneNumberIdentifiers.getPhoneNumber(pni);
}
public UUID getPhoneNumberIdentifier(String e164) {
return phoneNumberIdentifiers.getPhoneNumberIdentifier(e164);
}
public AccountCrawlChunk getAllFromDynamo(int length) {
return accounts.getAllFromStart(length);
}

View File

@ -21,14 +21,16 @@ import java.util.stream.Collectors;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.Pair;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.KeysAndAttributes;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
@ -40,6 +42,8 @@ public class DeletedAccounts extends AbstractDynamoDbStore {
static final String ATTR_EXPIRES = "E";
static final String ATTR_NEEDS_CDS_RECONCILIATION = "R";
static final String UUID_TO_E164_INDEX_NAME = "u_to_p";
static final Duration TIME_TO_LIVE = Duration.ofDays(30);
// Note that this limit is imposed by DynamoDB itself; going above 100 will result in errors
@ -76,6 +80,28 @@ public class DeletedAccounts extends AbstractDynamoDbStore {
return Optional.ofNullable(AttributeValues.getUUID(response.item(), ATTR_ACCOUNT_UUID, null));
}
Optional<String> findE164(final UUID uuid) {
final QueryResponse response = db().query(QueryRequest.builder()
.tableName(tableName)
.indexName(UUID_TO_E164_INDEX_NAME)
.keyConditionExpression("#uuid = :uuid")
.projectionExpression("#e164")
.expressionAttributeNames(Map.of("#uuid", ATTR_ACCOUNT_UUID,
"#e164", KEY_ACCOUNT_E164))
.expressionAttributeValues(Map.of(":uuid", AttributeValues.fromUUID(uuid))).build());
if (response.count() == 0) {
return Optional.empty();
}
if (response.count() > 1) {
throw new RuntimeException(
"Impossible result: more than one phone number returned for UUID: " + uuid);
}
return Optional.ofNullable(response.items().get(0).get(KEY_ACCOUNT_E164).s());
}
void remove(final String e164) {
db().deleteItem(DeleteItemRequest.builder()
.tableName(tableName)

View File

@ -173,8 +173,7 @@ public class DeletedAccountsManager {
}
return lockAcquired;
})
.collect(Collectors.toList());
}).toList();
assert lockItems.size() == reconciliationCandidates.size();
@ -192,7 +191,16 @@ public class DeletedAccountsManager {
try {
deletedAccounts.markReconciled(consumer.reconcile(accountsToReconcile));
} finally {
lockItems.forEach(lockItem -> lockClient.releaseLock(ReleaseLockOptions.builder(lockItem).withBestEffort(true).build()));
lockItems.forEach(
lockItem -> lockClient.releaseLock(ReleaseLockOptions.builder(lockItem).withBestEffort(true).build()));
}
}
public Optional<UUID> findDeletedAccountAci(final String e164) {
return deletedAccounts.findUuid(e164);
}
public Optional<String> findDeletedAccountE164(final UUID uuid) {
return deletedAccounts.findE164(uuid);
}
}

View File

@ -14,6 +14,8 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
@ -23,6 +25,8 @@ import org.whispersystems.textsecuregcm.util.Constants;
public class MessagesManager {
private static final Logger logger = LoggerFactory.getLogger(MessagesManager.class);
private static final int RESULT_SET_CHUNK_SIZE = 100;
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
@ -53,7 +57,11 @@ public class MessagesManager {
messagesCache.insert(messageGuid, destinationUuid, destinationDevice, message);
if (message.hasSource() && !destinationUuid.toString().equals(message.getSourceUuid())) {
reportMessageManager.store(message.getSource(), messageGuid);
if (message.hasSourceUuid()) {
reportMessageManager.store(message.getSource(), message.getSourceUuid(), messageGuid);
} else {
logger.warn("Message missing source UUID");
}
}
}

View File

@ -5,20 +5,23 @@
package org.whispersystems.textsecuregcm.storage;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Timer;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;
import java.util.Map;
import java.util.UUID;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
/**
* Manages a global, persistent mapping of phone numbers to phone number identifiers regardless of whether those
@ -31,7 +34,10 @@ public class PhoneNumberIdentifiers {
@VisibleForTesting
static final String KEY_E164 = "P";
private static final String ATTR_PHONE_NUMBER_IDENTIFIER = "PNI";
@VisibleForTesting
static final String INDEX_NAME = "pni_to_p";
@VisibleForTesting
static final String ATTR_PHONE_NUMBER_IDENTIFIER = "PNI";
private static final Timer GET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "get"));
private static final Timer SET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "set"));
@ -69,6 +75,34 @@ public class PhoneNumberIdentifiers {
return phoneNumberIdentifier;
}
public Optional<String> getPhoneNumber(final UUID phoneNumberIdentifier) {
final QueryResponse response = dynamoDbClient.query(QueryRequest.builder()
.tableName(tableName)
.indexName(INDEX_NAME)
.keyConditionExpression("#pni = :pni")
.projectionExpression("#phone_number")
.expressionAttributeNames(Map.of(
"#phone_number", KEY_E164,
"#pni", ATTR_PHONE_NUMBER_IDENTIFIER
))
.expressionAttributeValues(Map.of(
":pni", AttributeValues.fromUUID(phoneNumberIdentifier)
))
.build());
if (response.count() == 0) {
return Optional.empty();
}
if (response.count() > 1) {
throw new RuntimeException(
"Impossible result: more than one phone number returned for PNI: " + phoneNumberIdentifier);
}
return Optional.ofNullable(response.items().get(0).get(KEY_E164).s());
}
@VisibleForTesting
UUID generatePhoneNumberIdentifierIfNotExists(final String phoneNumber) {
final UpdateItemResponse response = SET_PNI_TIMER.record(() -> dynamoDbClient.updateItem(UpdateItemRequest.builder()

View File

@ -1,6 +1,9 @@
package org.whispersystems.textsecuregcm.storage;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import io.lettuce.core.RedisException;
import io.micrometer.core.instrument.Metrics;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -8,6 +11,7 @@ import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -16,6 +20,8 @@ import org.whispersystems.textsecuregcm.util.UUIDUtil;
public class ReportMessageManager {
private static final String MIGRATION_COUNTER_NAME = name(ReportMessageManager.class, "migration");
private final ReportMessageDynamoDb reportMessageDynamoDb;
private final FaultTolerantRedisCluster rateLimitCluster;
@ -39,51 +45,91 @@ public class ReportMessageManager {
this.reportedMessageListeners.add(listener);
}
public void store(String sourceNumber, UUID messageGuid) {
// TODO sourceNumber can be removed after 2022-04-01
public void store(String sourceNumber, String sourceAci, UUID messageGuid) {
try {
Objects.requireNonNull(sourceNumber);
Objects.requireNonNull(sourceAci);
reportMessageDynamoDb.store(hash(messageGuid, sourceNumber));
reportMessageDynamoDb.store(hash(messageGuid, sourceAci));
} catch (final Exception e) {
logger.warn("Failed to store hash", e);
}
}
public void report(String sourceNumber, UUID messageGuid, UUID reporterUuid) {
public void report(Optional<String> sourceNumber, Optional<UUID> sourceAci, Optional<UUID> sourcePni,
UUID messageGuid, UUID reporterUuid) {
final boolean found = reportMessageDynamoDb.remove(hash(messageGuid, sourceNumber));
// TODO sourceNumber can be removed after 2022-04-15
final boolean foundByNumber = sourceNumber.map(number -> reportMessageDynamoDb.remove(hash(messageGuid, number)))
.orElse(false);
if (found) {
final boolean foundByAci = sourceAci.map(uuid -> reportMessageDynamoDb.remove(hash(messageGuid, uuid.toString()))).
orElse(false);
if (foundByNumber || foundByAci) {
rateLimitCluster.useCluster(connection -> {
final String reportedSenderKey = getReportedSenderKey(sourceNumber);
sourceNumber.ifPresent(number -> {
final String reportedSenderKey = getReportedSenderKey(number);
connection.sync().pfadd(reportedSenderKey, reporterUuid.toString());
connection.sync().expire(reportedSenderKey, counterTtl.toSeconds());
});
connection.sync().pfadd(reportedSenderKey, reporterUuid.toString());
connection.sync().expire(reportedSenderKey, counterTtl.toSeconds());
sourcePni.ifPresent(pni -> {
final String reportedSenderKey = getReportedSenderPniKey(pni);
connection.sync().pfadd(reportedSenderKey, reporterUuid.toString());
connection.sync().expire(reportedSenderKey, counterTtl.toSeconds());
});
sourceAci.ifPresent(aci -> {
final String reportedSenderKey = getReportedSenderAciKey(aci);
connection.sync().pfadd(reportedSenderKey, reporterUuid.toString());
connection.sync().expire(reportedSenderKey, counterTtl.toSeconds());
});
});
reportedMessageListeners.forEach(listener -> {
try {
listener.handleMessageReported(sourceNumber, messageGuid, reporterUuid);
} catch (final Exception e) {
logger.error("Failed to notify listener of reported message", e);
}
});
sourceNumber.ifPresent(number ->
reportedMessageListeners.forEach(listener -> {
try {
// TODO should listener take the source Aci?
listener.handleMessageReported(number, messageGuid, reporterUuid);
} catch (final Exception e) {
logger.error("Failed to notify listener of reported message", e);
}
}));
}
Metrics.counter(
MIGRATION_COUNTER_NAME,
"foundByNumber", String.valueOf(foundByNumber),
"foundByAci", String.valueOf(foundByAci),
"sourceAciPresent", String.valueOf(sourceAci.isPresent()),
"sourcePniPresent", String.valueOf(sourcePni.isPresent()),
"sourceNumberPresent", String.valueOf(sourceNumber.isPresent())
).increment();
}
/**
* Returns the number of times messages from the given number have been reported by recipients as abusive. Note that
* Returns the number of times messages from the given account have been reported by recipients as abusive. Note that
* this method makes a call to an external service, and callers should take care to memoize calls where possible and
* avoid unnecessary calls.
*
* @param number the number to check for recent reports
*
* @param account the account to check for recent reports
* @return the number of times the given number has been reported recently
*/
public int getRecentReportCount(final String number) {
public int getRecentReportCount(final Account account) {
try {
return rateLimitCluster.withCluster(connection -> connection.sync().pfcount(getReportedSenderKey(number)).intValue());
return rateLimitCluster.withCluster(
connection ->
Math.max(
Math.max(
// TODO number can be removed after 2022-04-15
connection.sync().pfcount(getReportedSenderKey(account.getNumber())).intValue(),
connection.sync().pfcount(getReportedSenderPniKey(account.getPhoneNumberIdentifier()))
.intValue()),
connection.sync().pfcount(getReportedSenderAciKey(account.getUuid())).intValue()));
} catch (final RedisException e) {
return 0;
}
@ -106,4 +152,12 @@ public class ReportMessageManager {
private static String getReportedSenderKey(final String senderNumber) {
return "reported_number::" + senderNumber;
}
private static String getReportedSenderAciKey(final UUID aci) {
return "reported_account::" + aci.toString();
}
private static String getReportedSenderPniKey(final UUID pni) {
return "reported_pni::" + pni.toString();
}
}

View File

@ -80,6 +80,7 @@ import org.whispersystems.textsecuregcm.push.MessageSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
@ -94,24 +95,25 @@ class MessageControllerTest {
private static final UUID SINGLE_DEVICE_UUID = UUID.randomUUID();
private static final UUID SINGLE_DEVICE_PNI = UUID.randomUUID();
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
private static final UUID MULTI_DEVICE_UUID = UUID.randomUUID();
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
private static final UUID MULTI_DEVICE_UUID = UUID.randomUUID();
private static final String INTERNATIONAL_RECIPIENT = "+61123456789";
private static final UUID INTERNATIONAL_UUID = UUID.randomUUID();
private static final UUID INTERNATIONAL_UUID = UUID.randomUUID();
private Account internationalAccount;
@SuppressWarnings("unchecked")
private static final RedisAdvancedClusterCommands<String, String> redisCommands = mock(RedisAdvancedClusterCommands.class);
private static final MessageSender messageSender = mock(MessageSender.class);
private static final ReceiptSender receiptSender = mock(ReceiptSender.class);
private static final AccountsManager accountsManager = mock(AccountsManager.class);
private static final MessagesManager messagesManager = mock(MessagesManager.class);
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
private static final RateLimiter rateLimiter = mock(RateLimiter.class);
private static final ApnFallbackManager apnFallbackManager = mock(ApnFallbackManager.class);
private static final MessageSender messageSender = mock(MessageSender.class);
private static final ReceiptSender receiptSender = mock(ReceiptSender.class);
private static final AccountsManager accountsManager = mock(AccountsManager.class);
private static final DeletedAccountsManager deletedAccountsManager = mock(DeletedAccountsManager.class);
private static final MessagesManager messagesManager = mock(MessagesManager.class);
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
private static final RateLimiter rateLimiter = mock(RateLimiter.class);
private static final ApnFallbackManager apnFallbackManager = mock(ApnFallbackManager.class);
private static final ReportMessageManager reportMessageManager = mock(ReportMessageManager.class);
private static final ExecutorService multiRecipientMessageExecutor = mock(ExecutorService.class);
@ -122,8 +124,9 @@ class MessageControllerTest {
.addProvider(RateLimitExceededExceptionMapper.class)
.addProvider(MultiDeviceMessageListProvider.class)
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new MessageController(rateLimiters, messageSender, receiptSender, accountsManager,
messagesManager, apnFallbackManager, reportMessageManager, multiRecipientMessageExecutor))
.addResource(
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccountsManager,
messagesManager, apnFallbackManager, reportMessageManager, multiRecipientMessageExecutor))
.build();
@BeforeEach
@ -486,13 +489,13 @@ class MessageControllerTest {
}
@Test
void testGetMessages() throws Exception {
void testGetMessages() {
final long timestampOne = 313377;
final long timestampTwo = 313388;
final UUID messageGuidOne = UUID.randomUUID();
final UUID sourceUuid = UUID.randomUUID();
final UUID sourceUuid = UUID.randomUUID();
List<OutgoingMessageEntity> messages = new LinkedList<>() {{
add(new OutgoingMessageEntity(messageGuidOne, Envelope.Type.CIPHERTEXT_VALUE, timestampOne, "+14152222222", sourceUuid, 2, AuthHelper.VALID_UUID, "hi there".getBytes(), 0));
@ -595,12 +598,23 @@ class MessageControllerTest {
}
@Test
void testReportMessage() {
void testReportMessageByE164() {
final String senderNumber = "+12125550001";
final UUID messageGuid = UUID.randomUUID();
final UUID senderAci = UUID.randomUUID();
final UUID senderPni = UUID.randomUUID();
UUID messageGuid = UUID.randomUUID();
final Response response =
final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(senderAci);
when(account.getNumber()).thenReturn(senderNumber);
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
when(accountsManager.getByE164(senderNumber)).thenReturn(Optional.of(account));
when(deletedAccountsManager.findDeletedAccountAci(senderNumber)).thenReturn(Optional.of(senderAci));
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
Response response =
resources.getJerseyTest()
.target(String.format("/v1/messages/report/%s/%s", senderNumber, messageGuid))
.request()
@ -609,7 +623,73 @@ class MessageControllerTest {
assertThat(response.getStatus(), is(equalTo(202)));
verify(reportMessageManager).report(senderNumber, messageGuid, AuthHelper.VALID_UUID);
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
messageGuid, AuthHelper.VALID_UUID);
verify(deletedAccountsManager, never()).findDeletedAccountE164(any(UUID.class));
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
when(accountsManager.getByE164(senderNumber)).thenReturn(Optional.empty());
messageGuid = UUID.randomUUID();
response =
resources.getJerseyTest()
.target(String.format("/v1/messages/report/%s/%s", senderNumber, messageGuid))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.post(null);
assertThat(response.getStatus(), is(equalTo(202)));
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
messageGuid, AuthHelper.VALID_UUID);
}
@Test
void testReportMessageByAci() {
final String senderNumber = "+12125550001";
final UUID senderAci = UUID.randomUUID();
final UUID senderPni = UUID.randomUUID();
UUID messageGuid = UUID.randomUUID();
final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(senderAci);
when(account.getNumber()).thenReturn(senderNumber);
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account));
when(deletedAccountsManager.findDeletedAccountE164(senderAci)).thenReturn(Optional.of(senderNumber));
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
Response response =
resources.getJerseyTest()
.target(String.format("/v1/messages/report/%s/%s", senderAci, messageGuid))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.post(null);
assertThat(response.getStatus(), is(equalTo(202)));
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
messageGuid, AuthHelper.VALID_UUID);
verify(deletedAccountsManager, never()).findDeletedAccountE164(any(UUID.class));
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.empty());
messageGuid = UUID.randomUUID();
response =
resources.getJerseyTest()
.target(String.format("/v1/messages/report/%s/%s", senderAci, messageGuid))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.post(null);
assertThat(response.getStatus(), is(equalTo(202)));
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
messageGuid, AuthHelper.VALID_UUID);
}
@ParameterizedTest

View File

@ -5,6 +5,16 @@
package org.whispersystems.textsecuregcm.storage;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.lang.Thread.State;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@ -17,16 +27,6 @@ import software.amazon.awssdk.services.dynamodb.model.Projection;
import software.amazon.awssdk.services.dynamodb.model.ProjectionType;
import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput;
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
import java.lang.Thread.State;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class DeletedAccountsManagerTest {
@ -43,11 +43,26 @@ class DeletedAccountsManagerTest {
.attributeName(DeletedAccounts.ATTR_NEEDS_CDS_RECONCILIATION)
.attributeType(ScalarAttributeType.N)
.build())
.attributeDefinition(AttributeDefinition.builder()
.attributeName(DeletedAccounts.ATTR_ACCOUNT_UUID)
.attributeType(ScalarAttributeType.B)
.build())
.globalSecondaryIndex(GlobalSecondaryIndex.builder()
.indexName(NEEDS_RECONCILIATION_INDEX_NAME)
.keySchema(KeySchemaElement.builder().attributeName(DeletedAccounts.KEY_ACCOUNT_E164).keyType(KeyType.HASH).build(),
KeySchemaElement.builder().attributeName(DeletedAccounts.ATTR_NEEDS_CDS_RECONCILIATION).keyType(KeyType.RANGE).build())
.projection(Projection.builder().projectionType(ProjectionType.INCLUDE).nonKeyAttributes(DeletedAccounts.ATTR_ACCOUNT_UUID).build())
.keySchema(
KeySchemaElement.builder().attributeName(DeletedAccounts.KEY_ACCOUNT_E164).keyType(KeyType.HASH).build(),
KeySchemaElement.builder().attributeName(DeletedAccounts.ATTR_NEEDS_CDS_RECONCILIATION)
.keyType(KeyType.RANGE).build())
.projection(Projection.builder().projectionType(ProjectionType.INCLUDE)
.nonKeyAttributes(DeletedAccounts.ATTR_ACCOUNT_UUID).build())
.provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build())
.build())
.globalSecondaryIndex(GlobalSecondaryIndex.builder()
.indexName(DeletedAccounts.UUID_TO_E164_INDEX_NAME)
.keySchema(
KeySchemaElement.builder().attributeName(DeletedAccounts.ATTR_ACCOUNT_UUID).keyType(KeyType.HASH).build()
)
.projection(Projection.builder().projectionType(ProjectionType.KEYS_ONLY).build())
.provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build())
.build())
.build();
@ -167,4 +182,5 @@ class DeletedAccountsManagerTest {
return List.of(deletedAccounts.get(0).second());
});
}
}

View File

@ -41,11 +41,26 @@ class DeletedAccountsTest {
.attributeName(DeletedAccounts.ATTR_NEEDS_CDS_RECONCILIATION)
.attributeType(ScalarAttributeType.N)
.build())
.attributeDefinition(AttributeDefinition.builder()
.attributeName(DeletedAccounts.ATTR_ACCOUNT_UUID)
.attributeType(ScalarAttributeType.B)
.build())
.globalSecondaryIndex(GlobalSecondaryIndex.builder()
.indexName(NEEDS_RECONCILIATION_INDEX_NAME)
.keySchema(KeySchemaElement.builder().attributeName(DeletedAccounts.KEY_ACCOUNT_E164).keyType(KeyType.HASH).build(),
KeySchemaElement.builder().attributeName(DeletedAccounts.ATTR_NEEDS_CDS_RECONCILIATION).keyType(KeyType.RANGE).build())
.projection(Projection.builder().projectionType(ProjectionType.INCLUDE).nonKeyAttributes(DeletedAccounts.ATTR_ACCOUNT_UUID).build())
.keySchema(
KeySchemaElement.builder().attributeName(DeletedAccounts.KEY_ACCOUNT_E164).keyType(KeyType.HASH).build(),
KeySchemaElement.builder().attributeName(DeletedAccounts.ATTR_NEEDS_CDS_RECONCILIATION)
.keyType(KeyType.RANGE).build())
.projection(Projection.builder().projectionType(ProjectionType.INCLUDE)
.nonKeyAttributes(DeletedAccounts.ATTR_ACCOUNT_UUID).build())
.provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build())
.build())
.globalSecondaryIndex(GlobalSecondaryIndex.builder()
.indexName(DeletedAccounts.UUID_TO_E164_INDEX_NAME)
.keySchema(
KeySchemaElement.builder().attributeName(DeletedAccounts.ATTR_ACCOUNT_UUID).keyType(KeyType.HASH).build()
)
.projection(Projection.builder().projectionType(ProjectionType.KEYS_ONLY).build())
.provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build())
.build())
.build();
@ -156,4 +171,30 @@ class DeletedAccountsTest {
assertEquals(expectedAccountsNeedingReconciliation, accountsNeedingReconciliation);
}
@Test
void testFindE164() {
assertEquals(Optional.empty(), deletedAccounts.findE164(UUID.randomUUID()));
final UUID uuid = UUID.randomUUID();
final String e164 = "+18005551234";
deletedAccounts.put(uuid, e164, true);
assertEquals(Optional.of(e164), deletedAccounts.findE164(uuid));
}
@Test
void testFindUUID() {
final String e164 = "+18005551234";
assertEquals(Optional.empty(), deletedAccounts.findUuid(e164));
final UUID uuid = UUID.randomUUID();
deletedAccounts.put(uuid, e164, true);
assertEquals(Optional.of(uuid), deletedAccounts.findUuid(e164));
}
}

View File

@ -24,16 +24,17 @@ class MessagesManagerTest {
@Test
void insert() {
final String sourceNumber = "+12025551212";
final UUID sourceAci = UUID.randomUUID();
final Envelope message = Envelope.newBuilder()
.setSource(sourceNumber)
.setSourceUuid(UUID.randomUUID().toString())
.setSourceUuid(sourceAci.toString())
.build();
final UUID destinationUuid = UUID.randomUUID();
messagesManager.insert(destinationUuid, 1L, message);
verify(reportMessageManager).store(eq(sourceNumber), any(UUID.class));
verify(reportMessageManager).store(eq(sourceNumber), eq(sourceAci.toString()), any(UUID.class));
final Envelope syncMessage = Envelope.newBuilder(message)
.setSourceUuid(destinationUuid.toString())

View File

@ -6,13 +6,21 @@
package org.whispersystems.textsecuregcm.storage;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex;
import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
import software.amazon.awssdk.services.dynamodb.model.KeyType;
import software.amazon.awssdk.services.dynamodb.model.Projection;
import software.amazon.awssdk.services.dynamodb.model.ProjectionType;
import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput;
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
class PhoneNumberIdentifiersTest {
@ -27,6 +35,20 @@ class PhoneNumberIdentifiersTest {
.attributeName(PhoneNumberIdentifiers.KEY_E164)
.attributeType(ScalarAttributeType.S)
.build())
.attributeDefinition(AttributeDefinition.builder()
.attributeName(PhoneNumberIdentifiers.ATTR_PHONE_NUMBER_IDENTIFIER)
.attributeType(ScalarAttributeType.B)
.build())
.globalSecondaryIndex(GlobalSecondaryIndex.builder()
.indexName(PhoneNumberIdentifiers.INDEX_NAME)
.projection(Projection.builder()
.projectionType(ProjectionType.KEYS_ONLY)
.build())
.keySchema(KeySchemaElement.builder().keyType(KeyType.HASH)
.attributeName(PhoneNumberIdentifiers.ATTR_PHONE_NUMBER_IDENTIFIER)
.build())
.provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build())
.build())
.build();
private PhoneNumberIdentifiers phoneNumberIdentifiers;
@ -55,4 +77,14 @@ class PhoneNumberIdentifiersTest {
assertEquals(phoneNumberIdentifiers.generatePhoneNumberIdentifierIfNotExists(number),
phoneNumberIdentifiers.generatePhoneNumberIdentifierIfNotExists(number));
}
@Test
void getPhoneNumber() {
final String number = "+18005551234";
assertFalse(phoneNumberIdentifiers.getPhoneNumber(UUID.randomUUID()).isPresent());
final UUID pni = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
assertEquals(Optional.of(number), phoneNumberIdentifiers.getPhoneNumber(pni));
}
}

View File

@ -6,14 +6,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import java.time.Duration;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -23,8 +22,16 @@ import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
class ReportMessageManagerTest {
private ReportMessageDynamoDb reportMessageDynamoDb;
private ReportMessageManager reportMessageManager;
private String sourceNumber;
private UUID sourceAci;
private UUID sourcePni;
private Account sourceAccount;
private UUID messageGuid;
private UUID reporterUuid;
@RegisterExtension
static RedisClusterExtension RATE_LIMIT_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
@ -34,77 +41,95 @@ class ReportMessageManagerTest {
reportMessageManager = new ReportMessageManager(reportMessageDynamoDb,
RATE_LIMIT_CLUSTER_EXTENSION.getRedisCluster(), Duration.ofDays(1));
sourceNumber = "+15105551111";
sourceAci = UUID.randomUUID();
sourcePni = UUID.randomUUID();
messageGuid = UUID.randomUUID();
reporterUuid = UUID.randomUUID();
sourceAccount = mock(Account.class);
when(sourceAccount.getUuid()).thenReturn(sourceAci);
when(sourceAccount.getNumber()).thenReturn(sourceNumber);
when(sourceAccount.getPhoneNumberIdentifier()).thenReturn(sourcePni);
}
@Test
void testStore() {
final UUID messageGuid = UUID.randomUUID();
final String number = "+15105551111";
assertDoesNotThrow(() -> reportMessageManager.store(null, messageGuid));
assertDoesNotThrow(() -> reportMessageManager.store(null, null, messageGuid));
verifyNoInteractions(reportMessageDynamoDb);
reportMessageManager.store(number, messageGuid);
reportMessageManager.store(sourceNumber, sourceAci.toString(), messageGuid);
verify(reportMessageDynamoDb).store(any());
verify(reportMessageDynamoDb, times(2)).store(any());
doThrow(RuntimeException.class)
.when(reportMessageDynamoDb).store(any());
assertDoesNotThrow(() -> reportMessageManager.store(number, messageGuid));
assertDoesNotThrow(() -> reportMessageManager.store(sourceNumber, sourceAci.toString(), messageGuid));
}
@Test
void testReport() {
final String sourceNumber = "+15105551111";
final UUID messageGuid = UUID.randomUUID();
final UUID reporterUuid = UUID.randomUUID();
final ReportedMessageListener listener = mock(ReportedMessageListener.class);
reportMessageManager.addListener(listener);
when(reportMessageDynamoDb.remove(any())).thenReturn(false);
reportMessageManager.report(sourceNumber, messageGuid, reporterUuid);
reportMessageManager.report(Optional.of(sourceNumber), Optional.of(sourceAci), Optional.of(sourcePni), messageGuid,
reporterUuid);
assertEquals(0, reportMessageManager.getRecentReportCount(sourceNumber));
assertEquals(0, reportMessageManager.getRecentReportCount(sourceAccount));
when(reportMessageDynamoDb.remove(any())).thenReturn(true);
reportMessageManager.report(sourceNumber, messageGuid, reporterUuid);
reportMessageManager.report(Optional.of(sourceNumber), Optional.of(sourceAci), Optional.of(sourcePni), messageGuid,
reporterUuid);
assertEquals(1, reportMessageManager.getRecentReportCount(sourceNumber));
assertEquals(1, reportMessageManager.getRecentReportCount(sourceAccount));
verify(listener).handleMessageReported(sourceNumber, messageGuid, reporterUuid);
}
@Test
void testReportMultipleReporters() {
final String sourceNumber = "+15105551111";
final UUID messageGuid = UUID.randomUUID();
when(reportMessageDynamoDb.remove(any())).thenReturn(true);
assertEquals(0, reportMessageManager.getRecentReportCount(sourceNumber));
assertEquals(0, reportMessageManager.getRecentReportCount(sourceAccount));
for (int i = 0; i < 100; i++) {
reportMessageManager.report(sourceNumber, messageGuid, UUID.randomUUID());
reportMessageManager.report(Optional.of(sourceNumber), Optional.of(sourceAci), Optional.of(sourcePni),
messageGuid, UUID.randomUUID());
}
assertTrue(reportMessageManager.getRecentReportCount(sourceNumber) > 10);
assertTrue(reportMessageManager.getRecentReportCount(sourceAccount) > 10);
}
@Test
void testReportSingleReporter() {
final String sourceNumber = "+15105551111";
final UUID messageGuid = UUID.randomUUID();
final UUID reporterUuid = UUID.randomUUID();
when(reportMessageDynamoDb.remove(any())).thenReturn(true);
assertEquals(0, reportMessageManager.getRecentReportCount(sourceNumber));
assertEquals(0, reportMessageManager.getRecentReportCount(sourceAccount));
for (int i = 0; i < 100; i++) {
reportMessageManager.report(sourceNumber, messageGuid, reporterUuid);
reportMessageManager.report(Optional.of(sourceNumber), Optional.of(sourceAci), Optional.of(sourcePni),
messageGuid,
reporterUuid);
}
assertEquals(1, reportMessageManager.getRecentReportCount(sourceNumber));
assertEquals(1, reportMessageManager.getRecentReportCount(sourceAccount));
}
@Test
void testReportMultipleReportersByPni() {
when(reportMessageDynamoDb.remove(any())).thenReturn(true);
assertEquals(0, reportMessageManager.getRecentReportCount(sourceAccount));
for (int i = 0; i < 100; i++) {
reportMessageManager.report(Optional.of(sourceNumber), Optional.empty(), Optional.of(sourcePni),
messageGuid, UUID.randomUUID());
}
reportMessageManager.report(Optional.empty(), Optional.of(sourceAci), Optional.empty(),
messageGuid, UUID.randomUUID());
final int recentReportCount = reportMessageManager.getRecentReportCount(sourceAccount);
assertTrue(recentReportCount > 10);
}
}