Add a DynamoDB-backed stored verification code store.

This commit is contained in:
Jon Chambers 2021-06-18 12:07:34 -04:00 committed by Jon Chambers
parent 111f5ba024
commit d2d39baede
2 changed files with 154 additions and 0 deletions

View File

@ -0,0 +1,106 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.SystemMapper;
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.PutItemRequest;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import static com.codahale.metrics.MetricRegistry.name;
public class VerificationCodeStoreDynamoDb implements VerificationCodeStore {
private final DynamoDbClient dynamoDbClient;
private final String tableName;
private final Timer insertTimer;
private final Timer getTimer;
private final Timer removeTimer;
@VisibleForTesting
static final String KEY_E164 = "P";
private static final String ATTR_STORED_CODE = "C";
private static final String ATTR_TTL = "E";
private static final Logger log = LoggerFactory.getLogger(VerificationCodeStoreDynamoDb.class);
public VerificationCodeStoreDynamoDb(final DynamoDbClient dynamoDbClient, final String tableName) {
this.dynamoDbClient = dynamoDbClient;
this.tableName = tableName;
this.insertTimer = Metrics.timer(name(getClass(), "insert"), "table", tableName);
this.getTimer = Metrics.timer(name(getClass(), "get"), "table", tableName);
this.removeTimer = Metrics.timer(name(getClass(), "remove"), "table", tableName);
}
@Override
public void insert(final String number, final StoredVerificationCode verificationCode) {
insertTimer.record(() -> {
try {
dynamoDbClient.putItem(PutItemRequest.builder()
.tableName(tableName)
.item(Map.of(
KEY_E164, AttributeValues.fromString(number),
ATTR_STORED_CODE, AttributeValues.fromString(SystemMapper.getMapper().writeValueAsString(verificationCode)),
ATTR_TTL, AttributeValues.fromLong(getExpirationTimestamp(verificationCode))))
.build());
} catch (final JsonProcessingException e) {
// This should never happen when writing directly to a string except in cases of serious misconfiguration, which
// would be caught by tests.
throw new AssertionError(e);
}
});
}
private long getExpirationTimestamp(final StoredVerificationCode storedVerificationCode) {
return Instant.ofEpochMilli(storedVerificationCode.getTimestamp()).plus(StoredVerificationCode.EXPIRATION).getEpochSecond();
}
@Override
public Optional<StoredVerificationCode> findForNumber(final String number) {
return getTimer.record(() -> {
final GetItemResponse response = dynamoDbClient.getItem(GetItemRequest.builder()
.tableName(tableName)
.consistentRead(true)
.key(Map.of(KEY_E164, AttributeValues.fromString(number)))
.build());
try {
return response.hasItem()
? Optional.of(SystemMapper.getMapper().readValue(response.item().get(ATTR_STORED_CODE).s(), StoredVerificationCode.class))
: Optional.empty();
} catch (final JsonProcessingException e) {
log.error("Failed to parse stored verification code", e);
return Optional.empty();
}
});
}
@Override
public void remove(final String number) {
removeTimer.record(() -> {
dynamoDbClient.deleteItem(DeleteItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_E164, AttributeValues.fromString(number)))
.build());
});
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
class VerificationCodeStoreDynamoDbTest extends VerificationCodeStoreTest {
private VerificationCodeStoreDynamoDb verificationCodeStore;
private static final String TABLE_NAME = "verification_code_test";
@RegisterExtension
static final DynamoDbExtension DYNAMO_DB_EXTENSION = DynamoDbExtension.builder()
.tableName(TABLE_NAME)
.hashKey(VerificationCodeStoreDynamoDb.KEY_E164)
.attributeDefinition(AttributeDefinition.builder()
.attributeName(VerificationCodeStoreDynamoDb.KEY_E164)
.attributeType(ScalarAttributeType.S)
.build())
.build();
@BeforeEach
void setUp() {
verificationCodeStore = new VerificationCodeStoreDynamoDb(DYNAMO_DB_EXTENSION.getDynamoDbClient(), TABLE_NAME);
}
@Override
protected VerificationCodeStore getVerificationCodeStore() {
return verificationCodeStore;
}
@Override
protected boolean expectNullPushCode() {
return false;
}
@Override
protected boolean expectEmptyTwilioSid() {
return false;
}
}