Support for versioned profiles

Includes support for issuing zkgroup auth credentials
This commit is contained in:
Moxie Marlinspike 2019-10-09 11:30:01 -07:00
parent a94fc22659
commit ba3102d667
23 changed files with 1315 additions and 98 deletions

View File

@ -38,6 +38,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.signal</groupId>
<artifactId>zkgroup-java</artifactId>
<version>0.1</version>
</dependency>
<dependency>

View File

@ -166,6 +166,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private SecureBackupServiceConfiguration backupService;
@Valid
@NotNull
@JsonProperty
private ZkConfig zkConfig;
private Map<String, String> transparentDataIndex = new HashMap<>();
public RecaptchaConfiguration getRecaptchaConfiguration() {
@ -289,4 +294,9 @@ public class WhisperServerConfiguration extends Configuration {
public SecureBackupServiceConfiguration getSecureBackupServiceConfiguration() {
return backupService;
}
public ZkConfig getZkConfig() {
return zkConfig;
}
}

View File

@ -16,6 +16,12 @@
*/
package org.whispersystems.textsecuregcm;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.jdbi3.strategies.DefaultNameStrategy;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@ -26,6 +32,9 @@ import com.google.common.collect.ImmutableSet;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.jdbi.v3.core.Jdbi;
import org.signal.zkgroup.ServerSecretParams;
import org.signal.zkgroup.auth.ServerZkAuthOperations;
import org.signal.zkgroup.profiles.ServerZkProfileOperations;
import org.whispersystems.dispatch.DispatchManager;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
@ -55,6 +64,8 @@ import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.push.WebsocketSender;
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import org.whispersystems.textsecuregcm.s3.PolicySigner;
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
@ -67,6 +78,7 @@ import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
import org.whispersystems.textsecuregcm.workers.CertificateCommand;
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
import org.whispersystems.textsecuregcm.workers.VacuumCommand;
import org.whispersystems.textsecuregcm.workers.ZkParamsCommand;
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
import org.whispersystems.websocket.setup.WebSocketEnvironment;
@ -102,6 +114,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
bootstrap.addCommand(new VacuumCommand());
bootstrap.addCommand(new DeleteUserCommand());
bootstrap.addCommand(new CertificateCommand());
bootstrap.addCommand(new ZkParamsCommand());
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("keysdb", "keysdb.xml") {
@Override
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
@ -162,6 +175,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
PendingDevices pendingDevices = new PendingDevices (accountDatabase);
Usernames usernames = new Usernames(accountDatabase);
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
Profiles profiles = new Profiles(accountDatabase);
Keys keys = new Keys(keysDatabase);
Messages messages = new Messages(messageDatabase);
AbusiveHostRules abusiveHostRules = new AbusiveHostRules(abuseDatabase);
@ -182,6 +196,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, cacheClient );
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheClient);
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheClient);
MessagesCache messagesCache = new MessagesCache(messagesClient, messages, accountsManager, config.getMessageCacheConfiguration().getPersistDelayMinutes());
MessagesManager messagesManager = new MessagesManager(messages, messagesCache);
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager);
@ -231,11 +246,21 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.lifecycle().manage(messagesCache);
environment.lifecycle().manage(accountDatabaseCrawler);
AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret());
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
AmazonS3 cdnS3Client = AmazonS3Client.builder().withCredentials(credentialsProvider).withRegion(config.getCdnConfiguration().getRegion()).build();
PostPolicyGenerator cdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket(), config.getCdnConfiguration().getAccessKey());
PolicySigner cdnPolicySigner = new PolicySigner(config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion());
ServerSecretParams zkSecretParams = new ServerSecretParams(config.getZkConfig().getServerSecret());
ServerZkProfileOperations zkProfileOperations = new ServerZkProfileOperations(zkSecretParams);
ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams);
AttachmentControllerV1 attachmentControllerV1 = new AttachmentControllerV1(rateLimiters, config.getAttachmentsConfiguration().getAccessKey(), config.getAttachmentsConfiguration().getAccessSecret(), config.getAttachmentsConfiguration().getBucket() );
AttachmentControllerV2 attachmentControllerV2 = new AttachmentControllerV2(rateLimiters, config.getAttachmentsConfiguration().getAccessKey(), config.getAttachmentsConfiguration().getAccessSecret(), config.getAttachmentsConfiguration().getRegion(), config.getAttachmentsConfiguration().getBucket());
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager, directoryQueue);
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, apnFallbackManager);
ProfileController profileController = new ProfileController(rateLimiters, accountsManager, usernamesManager, config.getCdnConfiguration());
ProfileController profileController = new ProfileController(rateLimiters, accountsManager, profilesManager, usernamesManager, cdnS3Client, cdnPolicyGenerator, cdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations);
StickerController stickerController = new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket());
AuthFilter<BasicCredentials, Account> accountAuthFilter = new BasicCredentialAuthFilter.Builder<Account>().setAuthenticator(accountAuthenticator).buildAuthFilter ();
@ -249,7 +274,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices()));
environment.jersey().register(new DirectoryController(rateLimiters, directory, directoryCredentialsGenerator));
environment.jersey().register(new ProvisioningController(rateLimiters, pushSender));
environment.jersey().register(new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays())));
environment.jersey().register(new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()), zkAuthOperations));
environment.jersey().register(new VoiceVerificationController(config.getVoiceVerificationConfiguration().getUrl(), config.getVoiceVerificationConfiguration().getLocales()));
environment.jersey().register(new SecureStorageController(storageCredentialsGenerator));
environment.jersey().register(new SecureBackupController(backupCredentialsGenerator));

View File

@ -0,0 +1,31 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import javax.validation.constraints.NotNull;
public class ZkConfig {
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotNull
private byte[] serverSecret;
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotNull
private byte[] serverPublic;
public byte[] getServerSecret() {
return serverSecret;
}
public byte[] getServerPublic() {
return serverPublic;
}
}

View File

@ -1,15 +1,18 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import org.signal.zkgroup.auth.ServerZkAuthOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
import org.whispersystems.textsecuregcm.entities.GroupCredentials;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Util;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
@ -17,6 +20,8 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import io.dropwizard.auth.Auth;
@ -27,10 +32,12 @@ public class CertificateController {
private final Logger logger = LoggerFactory.getLogger(CertificateController.class);
private final CertificateGenerator certificateGenerator;
private final CertificateGenerator certificateGenerator;
private final ServerZkAuthOperations serverZkAuthOperations;
public CertificateController(CertificateGenerator certificateGenerator) {
this.certificateGenerator = certificateGenerator;
public CertificateController(CertificateGenerator certificateGenerator, ServerZkAuthOperations serverZkAuthOperations) {
this.certificateGenerator = certificateGenerator;
this.serverZkAuthOperations = serverZkAuthOperations;
}
@Timed
@ -50,4 +57,27 @@ public class CertificateController {
return new DeliveryCertificate(certificateGenerator.createFor(account, account.getAuthenticatedDevice().get(), includeUuid.orElse(false)));
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/group/{startRedemptionTime}/{endRedemptionTime}")
public GroupCredentials getAuthenticationCredentials(@Auth Account account,
@PathParam("startRedemptionTime") int startRedemptionTime,
@PathParam("endRedemptionTime") int endRedemptionTime)
{
if (startRedemptionTime > endRedemptionTime) throw new WebApplicationException(Response.Status.BAD_REQUEST);
if (endRedemptionTime > Util.currentDaysSinceEpoch() + 7) throw new WebApplicationException(Response.Status.BAD_REQUEST);
if (startRedemptionTime < Util.currentDaysSinceEpoch()) throw new WebApplicationException(Response.Status.BAD_REQUEST);
List<GroupCredentials.GroupCredential> credentials = new LinkedList<>();
for (int i=startRedemptionTime;i<=endRedemptionTime;i++) {
credentials.add(new GroupCredentials.GroupCredential(serverZkAuthOperations.issueAuthCredential(account.getUuid(), i)
.serialize(),
i));
}
return new GroupCredentials(credentials);
}
}

View File

@ -1,20 +1,25 @@
package org.whispersystems.textsecuregcm.controllers;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.codahale.metrics.annotation.Timed;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.valuehandling.UnwrapValidatedValue;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
import org.signal.zkgroup.profiles.ServerZkProfileOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
import org.whispersystems.textsecuregcm.auth.Anonymous;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.entities.CreateProfileRequest;
import org.whispersystems.textsecuregcm.entities.Profile;
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
import org.whispersystems.textsecuregcm.entities.UserCapabilities;
@ -23,10 +28,14 @@ import org.whispersystems.textsecuregcm.s3.PolicySigner;
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.util.ExactlySize;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.util.Pair;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
@ -49,41 +58,212 @@ import io.dropwizard.auth.Auth;
@Path("/v1/profile")
public class ProfileController {
private final Logger logger = LoggerFactory.getLogger(ProfileController.class);
private final RateLimiters rateLimiters;
private final ProfilesManager profilesManager;
private final AccountsManager accountsManager;
private final UsernamesManager usernamesManager;
private final PolicySigner policySigner;
private final PostPolicyGenerator policyGenerator;
private final PolicySigner policySigner;
private final PostPolicyGenerator policyGenerator;
private final ServerZkProfileOperations zkProfileOperations;
private final AmazonS3 s3client;
private final String bucket;
public ProfileController(RateLimiters rateLimiters,
AccountsManager accountsManager,
ProfilesManager profilesManager,
UsernamesManager usernamesManager,
CdnConfiguration profilesConfiguration)
AmazonS3 s3client,
PostPolicyGenerator policyGenerator,
PolicySigner policySigner,
String bucket,
ServerZkProfileOperations zkProfileOperations)
{
AWSCredentials credentials = new BasicAWSCredentials(profilesConfiguration.getAccessKey(), profilesConfiguration.getAccessSecret());
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
this.rateLimiters = rateLimiters;
this.accountsManager = accountsManager;
this.usernamesManager = usernamesManager;
this.bucket = profilesConfiguration.getBucket();
this.s3client = AmazonS3Client.builder()
.withCredentials(credentialsProvider)
.withRegion(profilesConfiguration.getRegion())
.build();
this.policyGenerator = new PostPolicyGenerator(profilesConfiguration.getRegion(),
profilesConfiguration.getBucket(),
profilesConfiguration.getAccessKey());
this.policySigner = new PolicySigner(profilesConfiguration.getAccessSecret(),
profilesConfiguration.getRegion());
this.rateLimiters = rateLimiters;
this.accountsManager = accountsManager;
this.profilesManager = profilesManager;
this.usernamesManager = usernamesManager;
this.zkProfileOperations = zkProfileOperations;
this.bucket = bucket;
this.s3client = s3client;
this.policyGenerator = policyGenerator;
this.policySigner = policySigner;
}
@Timed
@PUT
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response setProfile(@Auth Account account, @Valid CreateProfileRequest request) {
Optional<VersionedProfile> currentProfile = profilesManager.get(account.getUuid(), request.getVersion());
String avatar = request.isAvatar() ? generateAvatarObjectName() : null;
Optional<ProfileAvatarUploadAttributes> response = Optional.empty();
profilesManager.set(account.getUuid(), new VersionedProfile(request.getVersion(), request.getName(), avatar, request.getCommitment().serialize()));
if (request.isAvatar()) {
Optional<String> currentAvatar = Optional.empty();
if (currentProfile.isPresent() && currentProfile.get().getAvatar() != null && currentProfile.get().getAvatar().startsWith("profiles/")) {
currentAvatar = Optional.of(currentProfile.get().getAvatar());
}
if (currentAvatar.isEmpty() && account.getAvatar() != null && account.getAvatar().startsWith("profiles/")) {
currentAvatar = Optional.of(account.getAvatar());
}
currentAvatar.ifPresent(s -> s3client.deleteObject(bucket, s));
response = Optional.of(generateAvatarUploadForm(avatar));
}
account.setProfileName(request.getName());
if (avatar != null) account.setAvatar(avatar);
accountsManager.update(account);
if (response.isPresent()) return Response.ok(response).build();
else return Response.ok().build();
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{uuid}/{version}")
public Optional<Profile> getProfile(@Auth Optional<Account> requestAccount,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@PathParam("uuid") UUID uuid,
@PathParam("version") String version)
throws RateLimitExceededException
{
return getVersionedProfile(requestAccount, accessKey, uuid, version, Optional.empty());
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{uuid}/{version}/{credentialRequest}")
public Optional<Profile> getProfile(@Auth Optional<Account> requestAccount,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@PathParam("uuid") UUID uuid,
@PathParam("version") String version,
@PathParam("credentialRequest") String credentialRequest)
throws RateLimitExceededException
{
return getVersionedProfile(requestAccount, accessKey, uuid, version, Optional.of(credentialRequest));
}
@SuppressWarnings("OptionalIsPresent")
private Optional<Profile> getVersionedProfile(Optional<Account> requestAccount,
Optional<Anonymous> accessKey,
UUID uuid,
String version,
Optional<String> credentialRequest)
throws RateLimitExceededException
{
try {
if (!requestAccount.isPresent() && !accessKey.isPresent()) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
if (requestAccount.isPresent()) {
rateLimiters.getProfileLimiter().validate(requestAccount.get().getNumber());
}
Optional<Account> accountProfile = accountsManager.get(uuid);
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
assert(accountProfile.isPresent());
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());
Optional<VersionedProfile> profile = profilesManager.get(uuid, version);
String name = profile.map(VersionedProfile::getName).orElse(accountProfile.get().getProfileName());
String avatar = profile.map(VersionedProfile::getAvatar).orElse(accountProfile.get().getAvatar());
Optional<ProfileKeyCredentialResponse> credential = getProfileCredential(credentialRequest, profile, uuid);
return Optional.of(new Profile(name,
avatar,
accountProfile.get().getIdentityKey(),
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
new UserCapabilities(accountProfile.get().isUuidAddressingSupported()),
username.orElse(null),
null, credential.orElse(null)));
} catch (InvalidInputException e) {
logger.info("Bad profile request", e);
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/username/{username}")
public Profile getProfileByUsername(@Auth Account account, @PathParam("username") String username) throws RateLimitExceededException {
rateLimiters.getUsernameLookupLimiter().validate(account.getUuid().toString());
username = username.toLowerCase();
Optional<UUID> uuid = usernamesManager.get(username);
if (!uuid.isPresent()) {
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
}
Optional<Account> accountProfile = accountsManager.get(uuid.get());
if (!accountProfile.isPresent()) {
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
}
return new Profile(accountProfile.get().getProfileName(),
accountProfile.get().getAvatar(),
accountProfile.get().getIdentityKey(),
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
new UserCapabilities(accountProfile.get().isUuidAddressingSupported()),
username,
accountProfile.get().getUuid(), null);
}
private Optional<ProfileKeyCredentialResponse> getProfileCredential(Optional<String> encodedProfileCredentialRequest,
Optional<VersionedProfile> profile,
UUID uuid)
throws InvalidInputException
{
if (!encodedProfileCredentialRequest.isPresent()) return Optional.empty();
if (!profile.isPresent()) return Optional.empty();
try {
ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.get().getCommitment());
ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedProfileCredentialRequest.get()));
ProfileKeyCredentialResponse response = zkProfileOperations.issueProfileKeyCredential(request, uuid, commitment);
return Optional.of(response);
} catch (DecoderException | VerificationFailedException e) {
throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build());
}
}
// Old profile endpoints. Replaced by versioned profile endpoints (above)
@Deprecated
@Timed
@PUT
@Produces(MediaType.APPLICATION_JSON)
@Path("/name/{name}")
public void setProfile(@Auth Account account, @PathParam("name") @UnwrapValidatedValue(true) @ExactlySize({72, 108}) Optional<String> name) {
account.setProfileName(name.orElse(null));
accountsManager.update(account);
}
@Deprecated
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@ -119,60 +299,19 @@ public class ProfileController {
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
new UserCapabilities(accountProfile.get().isUuidAddressingSupported()),
username.orElse(null),
null);
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/username/{username}")
public Profile getProfileByUsername(@Auth Account account, @PathParam("username") String username) throws RateLimitExceededException {
rateLimiters.getUsernameLookupLimiter().validate(account.getUuid().toString());
username = username.toLowerCase();
Optional<UUID> uuid = usernamesManager.get(username);
if (!uuid.isPresent()) {
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
}
Optional<Account> accountProfile = accountsManager.get(uuid.get());
if (!accountProfile.isPresent()) {
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
}
return new Profile(accountProfile.get().getProfileName(),
accountProfile.get().getAvatar(),
accountProfile.get().getIdentityKey(),
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
new UserCapabilities(accountProfile.get().isUuidAddressingSupported()),
username,
accountProfile.get().getUuid());
}
@Timed
@PUT
@Produces(MediaType.APPLICATION_JSON)
@Path("/name/{name}")
public void setProfile(@Auth Account account, @PathParam("name") @UnwrapValidatedValue(true) @ExactlySize({72, 108}) Optional<String> name) {
account.setProfileName(name.orElse(null));
accountsManager.update(account);
null, null);
}
@Deprecated
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/form/avatar")
public ProfileAvatarUploadAttributes getAvatarUploadForm(@Auth Account account) {
String previousAvatar = account.getAvatar();
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
String objectName = generateAvatarObjectName();
Pair<String, String> policy = policyGenerator.createFor(now, objectName, 10 * 1024 * 1024);
String signature = policySigner.getSignature(now, policy.second());
String previousAvatar = account.getAvatar();
String objectName = generateAvatarObjectName();
ProfileAvatarUploadAttributes profileAvatarUploadAttributes = generateAvatarUploadForm(objectName);
if (previousAvatar != null && previousAvatar.startsWith("profiles/")) {
s3client.deleteObject(bucket, previousAvatar);
@ -181,8 +320,19 @@ public class ProfileController {
account.setAvatar(objectName);
accountsManager.update(account);
return profileAvatarUploadAttributes;
}
////
private ProfileAvatarUploadAttributes generateAvatarUploadForm(String objectName) {
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
Pair<String, String> policy = policyGenerator.createFor(now, objectName, 10 * 1024 * 1024);
String signature = policySigner.getSignature(now, policy.second());
return new ProfileAvatarUploadAttributes(objectName, policy.first(), "private", "AWS4-HMAC-SHA256",
now.format(PostPolicyGenerator.AWS_DATE_TIME), policy.second(), signature);
}
private String generateAvatarObjectName() {

View File

@ -0,0 +1,55 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.hibernate.validator.constraints.NotEmpty;
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
import org.whispersystems.textsecuregcm.util.ExactlySize;
import javax.validation.constraints.NotNull;
public class CreateProfileRequest {
@JsonProperty
@NotEmpty
private String version;
@JsonProperty
@ExactlySize({108})
private String name;
@JsonProperty
private boolean avatar;
@JsonProperty
@NotNull
@JsonDeserialize(using = ProfileKeyCommitmentAdapter.Deserializing.class)
@JsonSerialize(using = ProfileKeyCommitmentAdapter.Serializing.class)
private ProfileKeyCommitment commitment;
public CreateProfileRequest() {}
public CreateProfileRequest(ProfileKeyCommitment commitment, String version, String name, boolean wantsAvatar) {
this.commitment = commitment;
this.version = version;
this.name = name;
this.avatar = wantsAvatar;
}
public ProfileKeyCommitment getCommitment() {
return commitment;
}
public String getVersion() {
return version;
}
public String getName() {
return name;
}
public boolean isAvatar() {
return avatar;
}
}

View File

@ -0,0 +1,72 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.textsecuregcm.util.Base64;
import java.io.IOException;
import java.util.List;
public class GroupCredentials {
@JsonProperty
private List<GroupCredential> credentials;
public GroupCredentials() {}
public GroupCredentials(List<GroupCredential> credentials) {
this.credentials = credentials;
}
public List<GroupCredential> getCredentials() {
return credentials;
}
public static class GroupCredential {
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] credential;
@JsonProperty
private int redemptionTime;
public GroupCredential() {}
public GroupCredential(byte[] credential, int redemptionTime) {
this.credential = credential;
this.redemptionTime = redemptionTime;
}
public byte[] getCredential() {
return credential;
}
public int getRedemptionTime() {
return redemptionTime;
}
}
public static class ByteArraySerializer extends JsonSerializer<byte[]> {
@Override
public void serialize(byte[] bytes, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(Base64.encodeBytes(bytes));
}
}
public static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
@Override
public byte[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
return Base64.decode(jsonParser.getValueAsString());
}
}
}

View File

@ -1,8 +1,12 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.annotations.VisibleForTesting;
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
import java.util.UUID;
public class Profile {
@ -31,11 +35,17 @@ public class Profile {
@JsonProperty
private UUID uuid;
@JsonProperty
@JsonSerialize(using = ProfileKeyCredentialResponseAdapter.Serializing.class)
@JsonDeserialize(using = ProfileKeyCredentialResponseAdapter.Deserializing.class)
private ProfileKeyCredentialResponse credential;
public Profile() {}
public Profile(String name, String avatar, String identityKey,
String unidentifiedAccess, boolean unrestrictedUnidentifiedAccess,
UserCapabilities capabilities, String username, UUID uuid)
UserCapabilities capabilities, String username, UUID uuid,
ProfileKeyCredentialResponse credential)
{
this.name = name;
this.avatar = avatar;
@ -45,6 +55,7 @@ public class Profile {
this.capabilities = capabilities;
this.username = username;
this.uuid = uuid;
this.credential = credential;
}
@VisibleForTesting

View File

@ -41,4 +41,8 @@ public class ProfileAvatarUploadAttributes {
this.signature = signature;
}
public String getKey() {
return key;
}
}

View File

@ -0,0 +1,37 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
import org.whispersystems.textsecuregcm.util.Base64;
import java.io.IOException;
public class ProfileKeyCommitmentAdapter {
public static class Serializing extends JsonSerializer<ProfileKeyCommitment> {
@Override
public void serialize(ProfileKeyCommitment value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(Base64.encodeBytes(value.serialize()));
}
}
public static class Deserializing extends JsonDeserializer<ProfileKeyCommitment> {
@Override
public ProfileKeyCommitment deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
try {
return new ProfileKeyCommitment(Base64.decode(p.getValueAsString()));
} catch (InvalidInputException e) {
throw new IOException(e);
}
}
}
}

View File

@ -0,0 +1,40 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
import org.whispersystems.textsecuregcm.util.Base64;
import java.io.IOException;
public class ProfileKeyCredentialResponseAdapter {
public static class Serializing extends JsonSerializer<ProfileKeyCredentialResponse> {
@Override
public void serialize(ProfileKeyCredentialResponse response, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException, JsonProcessingException
{
if (response == null) jsonGenerator.writeNull();
else jsonGenerator.writeString(Base64.encodeBytes(response.serialize()));
}
}
public static class Deserializing extends JsonDeserializer<ProfileKeyCredentialResponse> {
@Override
public ProfileKeyCredentialResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JsonProcessingException
{
try {
return new ProfileKeyCredentialResponse(Base64.decode(jsonParser.getValueAsString()));
} catch (InvalidInputException e) {
throw new IOException(e);
}
}
}
}

View File

@ -0,0 +1,71 @@
package org.whispersystems.textsecuregcm.storage;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import org.whispersystems.textsecuregcm.storage.mappers.VersionedProfileMapper;
import org.whispersystems.textsecuregcm.util.Constants;
import java.util.Optional;
import java.util.UUID;
import static com.codahale.metrics.MetricRegistry.name;
public class Profiles {
public static final String ID = "id";
public static final String UID = "uuid";
public static final String VERSION = "version";
public static final String NAME = "name";
public static final String AVATAR = "avatar";
public static final String COMMITMENT = "commitment";
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Timer setTimer = metricRegistry.timer(name(Profiles.class, "set" ));
private final Timer getTimer = metricRegistry.timer(name(Profiles.class, "get" ));
private final Timer deleteTimer = metricRegistry.timer(name(Profiles.class, "delete"));
private final FaultTolerantDatabase database;
public Profiles(FaultTolerantDatabase database) {
this.database = database;
this.database.getDatabase().registerRowMapper(new VersionedProfileMapper());
}
public void set(UUID uuid, VersionedProfile profile) {
database.use(jdbi -> jdbi.useHandle(handle -> {
try (Timer.Context ignored = setTimer.time()) {
handle.createUpdate("INSERT INTO profiles (" + UID + ", " + VERSION + ", " + NAME + ", " + AVATAR + ", " + COMMITMENT + ") VALUES (:uuid, :version, :name, :avatar, :commitment) ON CONFLICT (" + UID + ", " + VERSION + ") DO UPDATE SET " + NAME + " = EXCLUDED." + NAME + ", " + AVATAR + " = EXCLUDED." + AVATAR)
.bind("uuid", uuid)
.bind("version", profile.getVersion())
.bind("name", profile.getName())
.bind("avatar", profile.getAvatar())
.bind("commitment", profile.getCommitment())
.execute();
}
}));
}
public Optional<VersionedProfile> get(UUID uuid, String version) {
return database.with(jdbi -> jdbi.withHandle(handle -> {
try (Timer.Context ignored = getTimer.time()) {
return handle.createQuery("SELECT * FROM profiles WHERE " + UID + " = :uuid AND " + VERSION + " = :version")
.bind("uuid", uuid)
.bind("version", version)
.mapTo(VersionedProfile.class)
.findFirst();
}
}));
}
public void deleteAll(UUID uuid) {
database.use(jdbi -> jdbi.useHandle(handle -> {
try (Timer.Context ignored = deleteTimer.time()) {
handle.createUpdate("DELETE FROM profiles WHERE " + UID + " = :uuid")
.bind("uuid", uuid)
.execute();
}
}));
}
}

View File

@ -0,0 +1,82 @@
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisException;
public class ProfilesManager {
private final Logger logger = LoggerFactory.getLogger(PendingAccountsManager.class);
private static final String CACHE_PREFIX = "profiles::";
private final Profiles profiles;
private final ReplicatedJedisPool cacheClient;
private final ObjectMapper mapper;
public ProfilesManager(Profiles profiles, ReplicatedJedisPool cacheClient) {
this.profiles = profiles;
this.cacheClient = cacheClient;
this.mapper = SystemMapper.getMapper();
}
public void set(UUID uuid, VersionedProfile versionedProfile) {
memcacheSet(uuid, versionedProfile);
profiles.set(uuid, versionedProfile);
}
public void deleteAll(UUID uuid) {
memcacheDelete(uuid);
profiles.deleteAll(uuid);
}
public Optional<VersionedProfile> get(UUID uuid, String version) {
Optional<VersionedProfile> profile = memcacheGet(uuid, version);
if (!profile.isPresent()) {
profile = profiles.get(uuid, version);
profile.ifPresent(versionedProfile -> memcacheSet(uuid, versionedProfile));
}
return profile;
}
private void memcacheSet(UUID uuid, VersionedProfile profile) {
try (Jedis jedis = cacheClient.getWriteResource()) {
jedis.hset(CACHE_PREFIX + uuid.toString(), profile.getVersion(), mapper.writeValueAsString(profile));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
private Optional<VersionedProfile> memcacheGet(UUID uuid, String version) {
try (Jedis jedis = cacheClient.getReadResource()) {
String json = jedis.hget(CACHE_PREFIX + uuid.toString(), version);
if (json == null) return Optional.empty();
else return Optional.of(mapper.readValue(json, VersionedProfile.class));
} catch (IOException e) {
logger.warn("Error deserializing value...", e);
return Optional.empty();
} catch (JedisException e) {
logger.warn("Redis exception", e);
return Optional.empty();
}
}
private void memcacheDelete(UUID uuid) {
try (Jedis jedis = cacheClient.getWriteResource()) {
jedis.del(CACHE_PREFIX + uuid.toString());
}
}
}

View File

@ -0,0 +1,49 @@
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
public class VersionedProfile {
@JsonProperty
private String version;
@JsonProperty
private String name;
@JsonProperty
private String avatar;
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
private byte[] commitment;
public VersionedProfile() {}
public VersionedProfile(String version, String name, String avatar, byte[] commitment) {
this.version = version;
this.name = name;
this.avatar = avatar;
this.commitment = commitment;
}
public String getVersion() {
return version;
}
public String getName() {
return name;
}
public String getAvatar() {
return avatar;
}
public byte[] getCommitment() {
return commitment;
}
}

View File

@ -0,0 +1,20 @@
package org.whispersystems.textsecuregcm.storage.mappers;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import java.sql.ResultSet;
import java.sql.SQLException;
public class VersionedProfileMapper implements RowMapper<VersionedProfile> {
@Override
public VersionedProfile map(ResultSet resultSet, StatementContext ctx) throws SQLException {
return new VersionedProfile(resultSet.getString(Profiles.VERSION),
resultSet.getString(Profiles.NAME),
resultSet.getString(Profiles.AVATAR),
resultSet.getBytes(Profiles.COMMITMENT));
}
}

View File

@ -144,6 +144,17 @@ public class Util {
return data;
}
public static int toIntExact(long value) {
if ((int)value != value) {
throw new ArithmeticException("integer overflow");
}
return (int)value;
}
public static int currentDaysSinceEpoch() {
return Util.toIntExact(System.currentTimeMillis() / 1000 / 60 / 60 / 24);
}
public static void sleep(long i) {
try {
Thread.sleep(i);

View File

@ -0,0 +1,32 @@
package org.whispersystems.textsecuregcm.workers;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.ServerSecretParams;
import org.whispersystems.textsecuregcm.util.Base64;
import io.dropwizard.cli.Command;
import io.dropwizard.setup.Bootstrap;
public class ZkParamsCommand extends Command {
public ZkParamsCommand() {
super("zkparams", "Generates server zkparams");
}
@Override
public void configure(Subparser subparser) {
}
@Override
public void run(Bootstrap<?> bootstrap, Namespace namespace) throws Exception {
ServerSecretParams serverSecretParams = ServerSecretParams.generate();
ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams();
System.out.println("Public: " + Base64.encodeBytesWithoutPadding(serverPublicParams.serialize()));
System.out.println("Private: " + Base64.encodeBytesWithoutPadding(serverSecretParams.serialize()));
}
}

View File

@ -239,4 +239,38 @@
</createTable>
</changeSet>
<changeSet id="11" author="moxie">
<createTable tableName="profiles">
<column name="id" type="bigint" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="uuid" type="uuid">
<constraints nullable="false"/>
</column>
<column name="version" type="text">
<constraints nullable="false"/>
</column>
<column name="name" type="text">
<constraints nullable="false"/>
</column>
<column name="avatar" type="text">
<constraints nullable="true"/>
</column>
<column name="commitment" type="bytea">
<constraints nullable="false"/>
</column>
</createTable>
<addUniqueConstraint tableName="profiles" columnNames="uuid, version" constraintName="uuid_and_version"/>
<createIndex tableName="profiles" indexName="profiles_uuid">
<column name="uuid"/>
</createIndex>
</changeSet>
</databaseChangeLog>

View File

@ -4,18 +4,27 @@ import com.google.common.collect.ImmutableSet;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.ClassRule;
import org.junit.Test;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.ServerSecretParams;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.auth.AuthCredential;
import org.signal.zkgroup.auth.AuthCredentialResponse;
import org.signal.zkgroup.auth.ClientZkAuthOperations;
import org.signal.zkgroup.auth.ServerZkAuthOperations;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.controllers.CertificateController;
import org.whispersystems.textsecuregcm.crypto.Curve;
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
import org.whispersystems.textsecuregcm.entities.GroupCredentials;
import org.whispersystems.textsecuregcm.entities.MessageProtos.SenderCertificate;
import org.whispersystems.textsecuregcm.entities.MessageProtos.ServerCertificate;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.Util;
import javax.ws.rs.core.Response;
import java.io.IOException;
@ -24,6 +33,7 @@ import java.util.Arrays;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static junit.framework.TestCase.assertTrue;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@ -35,11 +45,14 @@ public class CertificateControllerTest {
private static final String signingCertificate = "CiUIDBIhBbTz4h1My+tt+vw+TVscgUe/DeHS0W02tPWAWbTO2xc3EkD+go4bJnU0AcnFfbOLKoiBfCzouZtDYMOVi69rE7r4U9cXREEqOkUmU2WJBjykAxWPCcSTmVTYHDw7hkSp/puG";
private static final String signingKey = "ABOxG29xrfq4E7IrW11Eg7+HBbtba9iiS0500YoBjn4=";
private static CertificateGenerator certificateGenerator;
private static ServerSecretParams serverSecretParams = ServerSecretParams.generate();
private static CertificateGenerator certificateGenerator;
private static ServerZkAuthOperations serverZkAuthOperations;
static {
try {
certificateGenerator = new CertificateGenerator(Base64.decode(signingCertificate), Curve.decodePrivatePoint(Base64.decode(signingKey)), 1);
certificateGenerator = new CertificateGenerator(Base64.decode(signingCertificate), Curve.decodePrivatePoint(Base64.decode(signingKey)), 1);
serverZkAuthOperations = new ServerZkAuthOperations(serverSecretParams);
} catch (IOException e) {
throw new AssertionError(e);
}
@ -52,7 +65,7 @@ public class CertificateControllerTest {
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)))
.setMapper(SystemMapper.getMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new CertificateController(certificateGenerator))
.addResource(new CertificateController(certificateGenerator, serverZkAuthOperations))
.build();
@ -150,5 +163,73 @@ public class CertificateControllerTest {
assertEquals(response.getStatus(), 401);
}
@Test
public void testGetSingleAuthCredential() throws InvalidInputException, VerificationFailedException {
GroupCredentials credentials = resources.getJerseyTest()
.target("/v1/certificate/group/" + Util.currentDaysSinceEpoch() + "/" + Util.currentDaysSinceEpoch())
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(GroupCredentials.class);
assertThat(credentials.getCredentials().size()).isEqualTo(1);
assertThat(credentials.getCredentials().get(0).getRedemptionTime()).isEqualTo(Util.currentDaysSinceEpoch());
ClientZkAuthOperations clientZkAuthOperations = new ClientZkAuthOperations(serverSecretParams.getPublicParams());
AuthCredential credential = clientZkAuthOperations.receiveAuthCredential(AuthHelper.VALID_UUID, Util.currentDaysSinceEpoch(), new AuthCredentialResponse(credentials.getCredentials().get(0).getCredential()));
}
@Test
public void testGetWeekLongAuthCredentials() throws InvalidInputException, VerificationFailedException {
GroupCredentials credentials = resources.getJerseyTest()
.target("/v1/certificate/group/" + Util.currentDaysSinceEpoch() + "/" + (Util.currentDaysSinceEpoch() + 7))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(GroupCredentials.class);
assertThat(credentials.getCredentials().size()).isEqualTo(8);
for (int i=0;i<=7;i++) {
assertThat(credentials.getCredentials().get(i).getRedemptionTime()).isEqualTo(Util.currentDaysSinceEpoch() + i);
ClientZkAuthOperations clientZkAuthOperations = new ClientZkAuthOperations(serverSecretParams.getPublicParams());
AuthCredential credential = clientZkAuthOperations.receiveAuthCredential(AuthHelper.VALID_UUID, Util.currentDaysSinceEpoch() + i , new AuthCredentialResponse(credentials.getCredentials().get(i).getCredential()));
}
}
@Test
public void testTooManyDaysOut() throws InvalidInputException {
Response response = resources.getJerseyTest()
.target("/v1/certificate/group/" + Util.currentDaysSinceEpoch() + "/" + (Util.currentDaysSinceEpoch() + 8))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get();
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
public void testBackwardsInTime() throws InvalidInputException {
Response response = resources.getJerseyTest()
.target("/v1/certificate/group/" + (Util.currentDaysSinceEpoch() - 1) + "/" + (Util.currentDaysSinceEpoch() + 7))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get();
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
public void testBadAuth() throws InvalidInputException {
Response response = resources.getJerseyTest()
.target("/v1/certificate/group/" + Util.currentDaysSinceEpoch() + "/" + (Util.currentDaysSinceEpoch() + 7))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
.get();
assertThat(response.getStatus()).isEqualTo(401);
}
}

View File

@ -1,5 +1,7 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.google.common.collect.ImmutableSet;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
@ -8,21 +10,32 @@ import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Mockito;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
import org.signal.zkgroup.profiles.ServerZkProfileOperations;
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.controllers.ProfileController;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.CreateProfileRequest;
import org.whispersystems.textsecuregcm.entities.Profile;
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.s3.PolicySigner;
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Optional;
@ -34,18 +47,17 @@ import static org.mockito.Mockito.*;
public class ProfileControllerTest {
private static AccountsManager accountsManager = mock(AccountsManager.class );
private static ProfilesManager profilesManager = mock(ProfilesManager.class);
private static UsernamesManager usernamesManager = mock(UsernamesManager.class);
private static RateLimiters rateLimiters = mock(RateLimiters.class );
private static RateLimiter rateLimiter = mock(RateLimiter.class );
private static RateLimiter usernameRateLimiter = mock(RateLimiter.class );
private static CdnConfiguration configuration = mock(CdnConfiguration.class);
static {
when(configuration.getAccessKey()).thenReturn("accessKey");
when(configuration.getAccessSecret()).thenReturn("accessSecret");
when(configuration.getRegion()).thenReturn("us-east-1");
when(configuration.getBucket()).thenReturn("profile-bucket");
}
private static AmazonS3 s3client = mock(AmazonS3.class);
private static PostPolicyGenerator postPolicyGenerator = new PostPolicyGenerator("us-west-1", "profile-bucket", "accessKey");
private static PolicySigner policySigner = new PolicySigner("accessSecret", "us-west-1");
private static ServerZkProfileOperations zkProfileOperations = mock(ServerZkProfileOperations.class);
@ClassRule
public static final ResourceTestRule resources = ResourceTestRule.builder()
@ -55,8 +67,13 @@ public class ProfileControllerTest {
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new ProfileController(rateLimiters,
accountsManager,
profilesManager,
usernamesManager,
configuration))
s3client,
postPolicyGenerator,
policySigner,
"profilesBucket",
zkProfileOperations))
.build();
@Before
@ -93,7 +110,14 @@ public class ProfileControllerTest {
when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(capabilitiesAccount));
when(accountsManager.get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasNumber() && identifier.getNumber().equals(AuthHelper.VALID_NUMBER)))).thenReturn(Optional.of(capabilitiesAccount));
Mockito.clearInvocations(accountsManager);
when(profilesManager.get(eq(AuthHelper.VALID_UUID), eq("someversion"))).thenReturn(Optional.empty());
when(profilesManager.get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"))).thenReturn(Optional.of(new VersionedProfile("validversion", "validname", "profiles/validavatar", "validcommitmnet".getBytes())));
clearInvocations(rateLimiter);
clearInvocations(accountsManager);
clearInvocations(usernamesManager);
clearInvocations(usernameRateLimiter);
clearInvocations(profilesManager);
}
@Test
@ -111,8 +135,7 @@ public class ProfileControllerTest {
verify(accountsManager, times(1)).get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasUuid() && identifier.getUuid().equals(AuthHelper.VALID_UUID_TWO)));
verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));
verify(rateLimiter, times(2)).validate(eq(AuthHelper.VALID_NUMBER));
reset(rateLimiter);
verify(rateLimiter, times(1)).validate(eq(AuthHelper.VALID_NUMBER));
}
@Test
@ -133,7 +156,6 @@ public class ProfileControllerTest {
verify(accountsManager, times(1)).get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasNumber() && identifier.getNumber().equals(AuthHelper.VALID_NUMBER_TWO)));
verifyNoMoreInteractions(usernamesManager);
verify(rateLimiter, times(1)).validate(eq(AuthHelper.VALID_NUMBER));
reset(rateLimiter);
}
@Test
@ -188,7 +210,6 @@ public class ProfileControllerTest {
verify(usernamesManager, times(1)).get(eq("n00bkillerzzzzz"));
verify(usernameRateLimiter, times(1)).validate(eq(AuthHelper.VALID_UUID.toString()));
reset(usernameRateLimiter);
}
@ -215,7 +236,7 @@ public class ProfileControllerTest {
}
@Test
public void testSetProfileName() {
public void testSetProfileNameDeprecated() {
Response response = resources.getJerseyTest()
.target("/v1/profile/name/123456789012345678901234567890123456789012345678901234567890123456789012")
.request()
@ -228,7 +249,7 @@ public class ProfileControllerTest {
}
@Test
public void testSetProfileNameExtended() {
public void testSetProfileNameExtendedDeprecated() {
Response response = resources.getJerseyTest()
.target("/v1/profile/name/123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678")
.request()
@ -241,7 +262,7 @@ public class ProfileControllerTest {
}
@Test
public void testSetProfileNameWrongSize() {
public void testSetProfileNameWrongSizeDeprecated() {
Response response = resources.getJerseyTest()
.target("/v1/profile/name/1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890")
.request()
@ -252,4 +273,113 @@ public class ProfileControllerTest {
verifyNoMoreInteractions(accountsManager);
}
/////
@Test
public void testSetProfileWantAvatarUpload() throws InvalidInputException {
ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment();
ProfileAvatarUploadAttributes uploadAttributes = resources.getJerseyTest()
.target("/v1/profile/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new CreateProfileRequest(commitment, "someversion", "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678", true), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class);
ArgumentCaptor<VersionedProfile> profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class);
verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID), eq("someversion"));
verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID), profileArgumentCaptor.capture());
verifyNoMoreInteractions(s3client);
assertThat(profileArgumentCaptor.getValue().getCommitment()).isEqualTo(commitment.serialize());
assertThat(profileArgumentCaptor.getValue().getAvatar()).isEqualTo(uploadAttributes.getKey());
assertThat(profileArgumentCaptor.getValue().getVersion()).isEqualTo("someversion");
assertThat(profileArgumentCaptor.getValue().getName()).isEqualTo("123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678");
}
@Test
public void testSetProfileWantAvatarUploadWithBadProfileSize() throws InvalidInputException {
ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment();
Response response = resources.getJerseyTest()
.target("/v1/profile/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new CreateProfileRequest(commitment, "someversion", "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", true), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(422);
}
@Test
public void testSetProfileWithoutAvatarUpload() throws InvalidInputException {
ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment();
Response response = resources.getJerseyTest()
.target("/v1/profile/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO))
.put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678", false), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.hasEntity()).isFalse();
ArgumentCaptor<VersionedProfile> profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class);
verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("anotherversion"));
verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture());
verifyNoMoreInteractions(s3client);
assertThat(profileArgumentCaptor.getValue().getCommitment()).isEqualTo(commitment.serialize());
assertThat(profileArgumentCaptor.getValue().getAvatar()).isNull();
assertThat(profileArgumentCaptor.getValue().getVersion()).isEqualTo("anotherversion");
assertThat(profileArgumentCaptor.getValue().getName()).isEqualTo("123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678");
}
@Test
public void testSetProvfileWithAvatarUploadAndPreviousAvatar() throws InvalidInputException {
ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment();
ProfileAvatarUploadAttributes uploadAttributes= resources.getJerseyTest()
.target("/v1/profile/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO))
.put(Entity.entity(new CreateProfileRequest(commitment, "validversion", "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678", true), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class);
ArgumentCaptor<VersionedProfile> profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class);
verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"));
verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture());
verify(s3client, times(1)).deleteObject(eq("profilesBucket"), eq("profiles/validavatar"));
assertThat(profileArgumentCaptor.getValue().getCommitment()).isEqualTo(commitment.serialize());
assertThat(profileArgumentCaptor.getValue().getAvatar()).startsWith("profiles/");
assertThat(profileArgumentCaptor.getValue().getVersion()).isEqualTo("validversion");
assertThat(profileArgumentCaptor.getValue().getName()).isEqualTo("123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678");
}
@Test
public void testGetProfileByVersion() throws RateLimitExceededException {
Profile profile = resources.getJerseyTest()
.target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(Profile.class);
assertThat(profile.getIdentityKey()).isEqualTo("bar");
assertThat(profile.getName()).isEqualTo("validname");
assertThat(profile.getAvatar()).isEqualTo("profiles/validavatar");
assertThat(profile.getCapabilities().isUuid()).isFalse();
assertThat(profile.getUsername()).isEqualTo("n00bkiller");
assertThat(profile.getUuid()).isNull();;
verify(accountsManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));
verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));
verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"));
verify(rateLimiter, times(1)).validate(eq(AuthHelper.VALID_NUMBER));
}
}

View File

@ -0,0 +1,107 @@
package org.whispersystems.textsecuregcm.tests.storage;
import org.junit.Test;
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.util.Base64;
import java.util.Optional;
import java.util.UUID;
import static junit.framework.TestCase.assertSame;
import static junit.framework.TestCase.assertTrue;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisException;
public class ProfilesManagerTest {
@Test
public void testGetProfileInCache() {
ReplicatedJedisPool cacheClient = mock(ReplicatedJedisPool.class);
Jedis jedis = mock(Jedis.class );
Profiles profiles = mock(Profiles.class );
UUID uuid = UUID.randomUUID();
when(cacheClient.getReadResource()).thenReturn(jedis);
when(jedis.hget(eq("profiles::" + uuid.toString()), eq("someversion"))).thenReturn("{\"version\": \"someversion\", \"name\": \"somename\", \"avatar\": \"someavatar\", \"commitment\":\"" + Base64.encodeBytes("somecommitment".getBytes()) + "\"}");
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheClient);
Optional<VersionedProfile> profile = profilesManager.get(uuid, "someversion");
assertTrue(profile.isPresent());
assertEquals(profile.get().getName(), "somename");
assertEquals(profile.get().getAvatar(), "someavatar");
assertThat(profile.get().getCommitment()).isEqualTo("somecommitment".getBytes());
verify(jedis, times(1)).hget(eq("profiles::" + uuid.toString()), eq("someversion"));
verify(jedis, times(1)).close();
verifyNoMoreInteractions(jedis);
verifyNoMoreInteractions(profiles);
}
@Test
public void testGetProfileNotInCache() {
ReplicatedJedisPool cacheClient = mock(ReplicatedJedisPool.class);
Jedis jedis = mock(Jedis.class );
Profiles profiles = mock(Profiles.class );
UUID uuid = UUID.randomUUID();
VersionedProfile profile = new VersionedProfile("someversion", "somename", "someavatar", "somecommitment".getBytes());
when(cacheClient.getReadResource()).thenReturn(jedis);
when(cacheClient.getWriteResource()).thenReturn(jedis);
when(jedis.hget(eq("profiles::" + uuid.toString()), eq("someversion"))).thenReturn(null);
when(profiles.get(eq(uuid), eq("someversion"))).thenReturn(Optional.of(profile));
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheClient);
Optional<VersionedProfile> retrieved = profilesManager.get(uuid, "someversion");
assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), profile);
verify(jedis, times(1)).hget(eq("profiles::" + uuid.toString()), eq("someversion"));
verify(jedis, times(1)).hset(eq("profiles::" + uuid.toString()), eq("someversion"), anyString());
verify(jedis, times(2)).close();
verifyNoMoreInteractions(jedis);
verify(profiles, times(1)).get(eq(uuid), eq("someversion"));
verifyNoMoreInteractions(profiles);
}
@Test
public void testGetProfileBrokenCache() {
ReplicatedJedisPool cacheClient = mock(ReplicatedJedisPool.class);
Jedis jedis = mock(Jedis.class );
Profiles profiles = mock(Profiles.class );
UUID uuid = UUID.randomUUID();
VersionedProfile profile = new VersionedProfile("someversion", "somename", "someavatar", "somecommitment".getBytes());
when(cacheClient.getReadResource()).thenReturn(jedis);
when(cacheClient.getWriteResource()).thenReturn(jedis);
when(jedis.hget(eq("profiles::" + uuid.toString()), eq("someversion"))).thenThrow(new JedisException("Connection lost"));
when(profiles.get(eq(uuid), eq("someversion"))).thenReturn(Optional.of(profile));
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheClient);
Optional<VersionedProfile> retrieved = profilesManager.get(uuid, "someversion");
assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), profile);
verify(jedis, times(1)).hget(eq("profiles::" + uuid.toString()), eq("someversion"));
verify(jedis, times(1)).hset(eq("profiles::" + uuid.toString()), eq("someversion"), anyString());
verify(jedis, times(2)).close();
verifyNoMoreInteractions(jedis);
verify(profiles, times(1)).get(eq(uuid), eq("someversion"));
verifyNoMoreInteractions(profiles);
}
}

View File

@ -0,0 +1,130 @@
package org.whispersystems.textsecuregcm.tests.storage;
import com.opentable.db.postgres.embedded.LiquibasePreparer;
import com.opentable.db.postgres.junit.EmbeddedPostgresRules;
import com.opentable.db.postgres.junit.PreparedDbRule;
import org.jdbi.v3.core.Jdbi;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class ProfilesTest {
@Rule
public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase(LiquibasePreparer.forClasspathLocation("accountsdb.xml"));
private Profiles profiles;
@Before
public void setupProfilesDao() {
FaultTolerantDatabase faultTolerantDatabase = new FaultTolerantDatabase("profilesTest",
Jdbi.create(db.getTestDatabase()),
new CircuitBreakerConfiguration());
this.profiles = new Profiles(faultTolerantDatabase);
}
@Test
public void testSetGet() {
UUID uuid = UUID.randomUUID();
VersionedProfile profile = new VersionedProfile("123", "foo", "avatarLocation", "acommitment".getBytes());
profiles.set(uuid, profile);
Optional<VersionedProfile> retrieved = profiles.get(uuid, "123");
assertThat(retrieved.isPresent()).isTrue();
assertThat(retrieved.get().getName()).isEqualTo(profile.getName());
assertThat(retrieved.get().getAvatar()).isEqualTo(profile.getAvatar());
assertThat(retrieved.get().getCommitment()).isEqualTo(profile.getCommitment());
}
@Test
public void testSetReplace() {
UUID uuid = UUID.randomUUID();
VersionedProfile profile = new VersionedProfile("123", "foo", "avatarLocation", "acommitment".getBytes());
profiles.set(uuid, profile);
Optional<VersionedProfile> retrieved = profiles.get(uuid, "123");
assertThat(retrieved.isPresent()).isTrue();
assertThat(retrieved.get().getName()).isEqualTo(profile.getName());
assertThat(retrieved.get().getAvatar()).isEqualTo(profile.getAvatar());
assertThat(retrieved.get().getCommitment()).isEqualTo(profile.getCommitment());
VersionedProfile updated = new VersionedProfile("123", "bar", "baz", "boof".getBytes());
profiles.set(uuid, updated);
retrieved = profiles.get(uuid, "123");
assertThat(retrieved.isPresent()).isTrue();
assertThat(retrieved.get().getName()).isEqualTo(updated.getName());
assertThat(retrieved.get().getAvatar()).isEqualTo(updated.getAvatar());
assertThat(retrieved.get().getCommitment()).isEqualTo(profile.getCommitment());
}
@Test
public void testMultipleVersions() {
UUID uuid = UUID.randomUUID();
VersionedProfile profileOne = new VersionedProfile("123", "foo", "avatarLocation", "acommitmnet".getBytes());
VersionedProfile profileTwo = new VersionedProfile("345", "bar", "baz", "boof".getBytes());
profiles.set(uuid, profileOne);
profiles.set(uuid, profileTwo);
Optional<VersionedProfile> retrieved = profiles.get(uuid, "123");
assertThat(retrieved.isPresent()).isTrue();
assertThat(retrieved.get().getName()).isEqualTo(profileOne.getName());
assertThat(retrieved.get().getAvatar()).isEqualTo(profileOne.getAvatar());
assertThat(retrieved.get().getCommitment()).isEqualTo(profileOne.getCommitment());
retrieved = profiles.get(uuid, "345");
assertThat(retrieved.isPresent()).isTrue();
assertThat(retrieved.get().getName()).isEqualTo(profileTwo.getName());
assertThat(retrieved.get().getAvatar()).isEqualTo(profileTwo.getAvatar());
assertThat(retrieved.get().getCommitment()).isEqualTo(profileTwo.getCommitment());
}
@Test
public void testMissing() {
UUID uuid = UUID.randomUUID();
VersionedProfile profile = new VersionedProfile("123", "foo", "avatarLocation", "aDigest".getBytes());
profiles.set(uuid, profile);
Optional<VersionedProfile> retrieved = profiles.get(uuid, "888");
assertThat(retrieved.isPresent()).isFalse();
}
@Test
public void testDelete() {
UUID uuid = UUID.randomUUID();
VersionedProfile profileOne = new VersionedProfile("123", "foo", "avatarLocation", "aDigest".getBytes());
VersionedProfile profileTwo = new VersionedProfile("345", "bar", "baz", "boof".getBytes());
profiles.set(uuid, profileOne);
profiles.set(uuid, profileTwo);
profiles.deleteAll(uuid);
Optional<VersionedProfile> retrieved = profiles.get(uuid, "123");
assertThat(retrieved.isPresent()).isFalse();
retrieved = profiles.get(uuid, "345");
assertThat(retrieved.isPresent()).isFalse();
}
}