Support for versioned profiles
Includes support for issuing zkgroup auth credentials
This commit is contained in:
parent
a94fc22659
commit
ba3102d667
|
@ -38,6 +38,11 @@
|
|||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>zkgroup-java</artifactId>
|
||||
<version>0.1</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -41,4 +41,8 @@ public class ProfileAvatarUploadAttributes {
|
|||
this.signature = signature;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue