Refactor: separate the various types of profile responses

This commit is contained in:
Jon Chambers 2021-12-07 11:35:32 -05:00 committed by Jon Chambers
parent 4ea7278c6f
commit 66845d7080
9 changed files with 463 additions and 351 deletions

View File

@ -8,6 +8,8 @@ package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.security.MessageDigest;
@ -36,9 +38,9 @@ public class OptionalAccess {
}
if (requestAccount.isPresent()) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
throw new NotFoundException();
} else {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
throw new NotAuthorizedException(Response.Status.UNAUTHORIZED);
}
}
} catch (NumberFormatException e) {
@ -56,7 +58,7 @@ public class OptionalAccess {
//noinspection ConstantConditions
if (requestAccount.isPresent() && (targetAccount.isEmpty() || (targetAccount.isPresent() && !targetAccount.get().isEnabled()))) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
throw new NotFoundException();
}
if (accessKey.isPresent() && targetAccount.isPresent() && targetAccount.get().isEnabled() && targetAccount.get().isUnrestrictedUnidentifiedAccess()) {
@ -72,7 +74,7 @@ public class OptionalAccess {
return;
}
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
throw new NotAuthorizedException(Response.Status.UNAUTHORIZED);
}
}

View File

@ -29,6 +29,8 @@ import javax.ws.rs.DefaultValue;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@ -63,9 +65,13 @@ import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.entities.CreateProfileRequest;
import org.whispersystems.textsecuregcm.entities.Profile;
import org.whispersystems.textsecuregcm.entities.CredentialProfileResponse;
import org.whispersystems.textsecuregcm.entities.PniCredentialProfileResponse;
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
import org.whispersystems.textsecuregcm.entities.ProfileKeyCredentialProfileResponse;
import org.whispersystems.textsecuregcm.entities.BaseProfileResponse;
import org.whispersystems.textsecuregcm.entities.UserCapabilities;
import org.whispersystems.textsecuregcm.entities.VersionedProfileResponse;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.s3.PolicySigner;
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
@ -196,23 +202,28 @@ public class ProfileController {
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{uuid}/{version}")
public Profile getProfile(
public VersionedProfileResponse getProfile(
@Auth Optional<AuthenticatedAccount> auth,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@Context ContainerRequestContext containerRequestContext,
@PathParam("uuid") UUID uuid,
@PathParam("version") String version)
throws RateLimitExceededException {
return getVersionedProfile(auth.map(AuthenticatedAccount::getAccount), accessKey,
getAcceptableLanguagesForRequest(containerRequestContext), uuid,
version, Optional.empty(), Optional.empty());
final Optional<Account> maybeRequester = auth.map(AuthenticatedAccount::getAccount);
final Account targetAccount = verifyPermissionToReceiveProfile(maybeRequester, accessKey, uuid);
return buildVersionedProfileResponse(targetAccount,
version,
isSelfProfileRequest(maybeRequester, uuid),
containerRequestContext);
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{uuid}/{version}/{credentialRequest}")
public Profile getProfile(
public CredentialProfileResponse getProfile(
@Auth Optional<AuthenticatedAccount> auth,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@Context ContainerRequestContext containerRequestContext,
@ -221,134 +232,140 @@ public class ProfileController {
@PathParam("credentialRequest") String credentialRequest,
@QueryParam("credentialType") @DefaultValue(PROFILE_KEY_CREDENTIAL_TYPE) String credentialType)
throws RateLimitExceededException {
return getVersionedProfile(auth.map(AuthenticatedAccount::getAccount), accessKey,
getAcceptableLanguagesForRequest(containerRequestContext), uuid,
version, Optional.of(credentialRequest), Optional.of(credentialType));
}
private Profile getVersionedProfile(
Optional<Account> requestAccount,
Optional<Anonymous> accessKey,
List<Locale> acceptableLanguages,
UUID uuid,
String version,
Optional<String> credentialRequest,
Optional<String> credentialType)
throws RateLimitExceededException {
if (requestAccount.isEmpty() && accessKey.isEmpty()) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
final Optional<Account> maybeRequester = auth.map(AuthenticatedAccount::getAccount);
final Account targetAccount = verifyPermissionToReceiveProfile(maybeRequester, accessKey, uuid);
final boolean isSelf = isSelfProfileRequest(maybeRequester, uuid);
boolean isSelf = false;
if (requestAccount.isPresent()) {
UUID authedUuid = requestAccount.get().getUuid();
rateLimiters.getProfileLimiter().validate(authedUuid);
isSelf = uuid.equals(authedUuid);
}
switch (credentialType) {
case PROFILE_KEY_CREDENTIAL_TYPE -> {
return buildProfileKeyCredentialProfileResponse(targetAccount,
version,
credentialRequest,
isSelf,
containerRequestContext);
}
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid);
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
assert accountProfile.isPresent();
Optional<String> username = accountProfile.flatMap(Account::getUsername);
Optional<VersionedProfile> profile = profilesManager.get(uuid, version);
String name = profile.map(VersionedProfile::getName).orElse(null);
String about = profile.map(VersionedProfile::getAbout).orElse(null);
String aboutEmoji = profile.map(VersionedProfile::getAboutEmoji).orElse(null);
String avatar = profile.map(VersionedProfile::getAvatar).orElse(null);
Optional<String> currentProfileVersion = accountProfile.get().getCurrentProfileVersion();
// Allow requests where either the version matches the latest version on Account or the latest version on Account
// is empty to read the payment address.
final String paymentAddress = profile
.filter(p -> currentProfileVersion.map(v -> v.equals(version)).orElse(true))
.map(VersionedProfile::getPaymentAddress)
.orElse(null);
final ProfileKeyCredentialResponse profileKeyCredentialResponse;
final PniCredentialResponse pniCredentialResponse;
if (credentialRequest.isPresent() && credentialType.isPresent() && profile.isPresent()) {
if (PNI_CREDENTIAL_TYPE.equals(credentialType.get())) {
case PNI_CREDENTIAL_TYPE -> {
if (!isSelf) {
throw new ForbiddenException();
}
profileKeyCredentialResponse = null;
pniCredentialResponse = getPniCredential(credentialRequest.get(),
profile.get(),
requestAccount.get().getUuid(),
requestAccount.get().getPhoneNumberIdentifier());
} else if (PROFILE_KEY_CREDENTIAL_TYPE.equals(credentialType.get())) {
profileKeyCredentialResponse = getProfileCredential(credentialRequest.get(),
profile.get(),
uuid);
pniCredentialResponse = null;
} else {
throw new BadRequestException();
return buildPniCredentialProfileResponse(targetAccount,
version,
credentialRequest,
containerRequestContext);
}
} else {
profileKeyCredentialResponse = null;
pniCredentialResponse = null;
}
return new Profile(
name,
about,
aboutEmoji,
avatar,
paymentAddress,
accountProfile.get().getIdentityKey(),
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
UserCapabilities.createForAccount(accountProfile.get()),
username.orElse(null),
null,
profileBadgeConverter.convert(acceptableLanguages, accountProfile.get().getBadges(), isSelf),
profileKeyCredentialResponse,
pniCredentialResponse);
default -> throw new BadRequestException();
}
}
// Although clients should generally be using versioned profiles wherever possible, there are still a few lingering
// use cases for getting profiles without a version (e.g. getting a contact's unidentified access key checksum).
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{identifier}")
public BaseProfileResponse getUnversionedProfile(
@Auth Optional<AuthenticatedAccount> auth,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@Context ContainerRequestContext containerRequestContext,
@HeaderParam("User-Agent") String userAgent,
@PathParam("identifier") UUID identifier,
@QueryParam("ca") boolean useCaCertificate)
throws RateLimitExceededException {
final Optional<Account> maybeRequester = auth.map(AuthenticatedAccount::getAccount);
final Account targetAccount = verifyPermissionToReceiveProfile(maybeRequester, accessKey, identifier);
return buildBaseProfileResponse(targetAccount,
isSelfProfileRequest(maybeRequester, identifier),
containerRequestContext);
}
private ProfileKeyCredentialProfileResponse buildProfileKeyCredentialProfileResponse(final Account account,
final String version,
final String encodedCredentialRequest,
final boolean isSelf,
final ContainerRequestContext containerRequestContext) {
final VersionedProfile profile =
profilesManager.get(account.getUuid(), version).orElseThrow(NotFoundException::new);
return new ProfileKeyCredentialProfileResponse(
buildVersionedProfileResponse(account, version, isSelf, containerRequestContext),
getProfileCredential(encodedCredentialRequest, profile, account.getUuid()));
}
private PniCredentialProfileResponse buildPniCredentialProfileResponse(final Account account,
final String version,
final String encodedCredentialRequest,
final ContainerRequestContext containerRequestContext) {
final VersionedProfile profile =
profilesManager.get(account.getUuid(), version).orElseThrow(NotFoundException::new);
return new PniCredentialProfileResponse(
buildVersionedProfileResponse(account, version, true, containerRequestContext),
getPniCredential(encodedCredentialRequest, profile, account.getUuid(), account.getPhoneNumberIdentifier()));
}
private VersionedProfileResponse buildVersionedProfileResponse(final Account account,
final String version,
final boolean isSelf,
final ContainerRequestContext containerRequestContext) {
final Optional<VersionedProfile> maybeProfile = profilesManager.get(account.getUuid(), version);
final String name = maybeProfile.map(VersionedProfile::getName).orElse(null);
final String about = maybeProfile.map(VersionedProfile::getAbout).orElse(null);
final String aboutEmoji = maybeProfile.map(VersionedProfile::getAboutEmoji).orElse(null);
final String avatar = maybeProfile.map(VersionedProfile::getAvatar).orElse(null);
// Allow requests where either the version matches the latest version on Account or the latest version on Account
// is empty to read the payment address.
final String paymentAddress = maybeProfile
.filter(p -> account.getCurrentProfileVersion().map(v -> v.equals(version)).orElse(true))
.map(VersionedProfile::getPaymentAddress)
.orElse(null);
return new VersionedProfileResponse(buildBaseProfileResponse(account, isSelf, containerRequestContext),
name, about, aboutEmoji, avatar, paymentAddress);
}
private BaseProfileResponse buildBaseProfileResponse(final Account account,
final boolean isSelf,
final ContainerRequestContext containerRequestContext) {
return new BaseProfileResponse(account.getIdentityKey(),
UnidentifiedAccessChecksum.generateFor(account.getUnidentifiedAccessKey()),
account.isUnrestrictedUnidentifiedAccess(),
UserCapabilities.createForAccount(account),
profileBadgeConverter.convert(
getAcceptableLanguagesForRequest(containerRequestContext),
account.getBadges(),
isSelf),
isSelf ? account.getUsername().orElse(null) : null,
account.getUuid());
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/username/{username}")
public Profile getProfileByUsername(
public BaseProfileResponse getProfileByUsername(
@Auth AuthenticatedAccount auth,
@Context ContainerRequestContext containerRequestContext,
@PathParam("username") String username)
throws RateLimitExceededException {
rateLimiters.getUsernameLookupLimiter().validate(auth.getAccount().getUuid());
username = username.toLowerCase();
final Account targetAccount = accountsManager.getByUsername(username).orElseThrow(NotFoundException::new);
final boolean isSelf = auth.getAccount().getUuid().equals(targetAccount.getUuid());
final Account accountProfile = accountsManager.getByUsername(username)
.orElseThrow(() -> new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build()));
final boolean isSelf = auth.getAccount().getUuid().equals(accountProfile.getUuid());
return new Profile(
null,
null,
null,
null,
null,
accountProfile.getIdentityKey(),
UnidentifiedAccessChecksum.generateFor(accountProfile.getUnidentifiedAccessKey()),
accountProfile.isUnrestrictedUnidentifiedAccess(),
UserCapabilities.createForAccount(accountProfile),
username,
accountProfile.getUuid(),
profileBadgeConverter.convert(
getAcceptableLanguagesForRequest(containerRequestContext),
accountProfile.getBadges(),
isSelf),
null,
null);
return buildBaseProfileResponse(targetAccount, isSelf, containerRequestContext);
}
private ProfileKeyCredentialResponse getProfileCredential(final String encodedProfileCredentialRequest,
@ -379,57 +396,6 @@ public class ProfileController {
}
}
// Although clients should generally be using versioned profiles wherever possible, there are still a few lingering
// use cases for getting profiles without a version (e.g. getting a contact's unidentified access key checksum).
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{identifier}")
public Profile getUnversionedProfile(
@Auth Optional<AuthenticatedAccount> auth,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@Context ContainerRequestContext containerRequestContext,
@HeaderParam("User-Agent") String userAgent,
@PathParam("identifier") UUID identifier,
@QueryParam("ca") boolean useCaCertificate)
throws RateLimitExceededException {
if (auth.isEmpty() && accessKey.isEmpty()) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
boolean isSelf = false;
if (auth.isPresent()) {
UUID authedUuid = auth.get().getAccount().getUuid();
rateLimiters.getProfileLimiter().validate(authedUuid);
isSelf = authedUuid.equals(identifier);
}
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(identifier);
OptionalAccess.verify(auth.map(AuthenticatedAccount::getAccount), accessKey, accountProfile);
Optional<String> username = accountProfile.flatMap(Account::getUsername);
return new Profile(
null,
null,
null,
null,
null,
accountProfile.get().getIdentityKey(),
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
UserCapabilities.createForAccount(accountProfile.get()),
username.orElse(null),
null,
profileBadgeConverter.convert(
getAcceptableLanguagesForRequest(containerRequestContext),
accountProfile.get().getBadges(),
isSelf),
null,
null);
}
private ProfileAvatarUploadAttributes generateAvatarUploadForm(String objectName) {
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
Pair<String, String> policy = policyGenerator.createFor(now, objectName, 10 * 1024 * 1024);
@ -497,4 +463,42 @@ public class ProfileController {
return new ArrayList<>(result.values());
}
/**
* Verifies that the requester has permission to view the profile of the account identified by the given ACI.
*
* @param maybeRequester the authenticated account requesting the profile, if any
* @param maybeAccessKey an anonymous access key for the target account
* @param targetUuid the ACI of the target account
*
* @return the target account
*
* @throws RateLimitExceededException if the requester must wait before requesting the target account's profile
* @throws NotFoundException if no account was found for the target ACI
* @throws NotAuthorizedException if the requester is not authorized to receive the target account's profile or if the
* requester was not authenticated and did not present an anonymous access key
*/
private Account verifyPermissionToReceiveProfile(final Optional<Account> maybeRequester,
final Optional<Anonymous> maybeAccessKey,
final UUID targetUuid) throws RateLimitExceededException {
if (maybeRequester.isEmpty() && maybeAccessKey.isEmpty()) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
if (maybeRequester.isPresent()) {
rateLimiters.getProfileLimiter().validate(maybeRequester.get().getUuid());
}
final Optional<Account> maybeTargetAccount = accountsManager.getByAccountIdentifier(targetUuid);
OptionalAccess.verify(maybeRequester, maybeAccessKey, maybeTargetAccount);
assert maybeTargetAccount.isPresent();
return maybeTargetAccount.get();
}
private boolean isSelfProfileRequest(final Optional<Account> maybeRequester, final UUID targetUuid) {
return maybeRequester.map(requester -> requester.getUuid().equals(targetUuid)).orElse(false);
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.annotation.Nullable;
import java.util.List;
import java.util.UUID;
public class BaseProfileResponse {
@JsonProperty
private String identityKey;
@JsonProperty
private String unidentifiedAccess;
@JsonProperty
private boolean unrestrictedUnidentifiedAccess;
@JsonProperty
private UserCapabilities capabilities;
@JsonProperty
private List<Badge> badges;
@JsonProperty
private String username;
@JsonProperty
private UUID uuid;
public BaseProfileResponse() {
}
public BaseProfileResponse(final String identityKey,
final String unidentifiedAccess,
final boolean unrestrictedUnidentifiedAccess,
final UserCapabilities capabilities,
final List<Badge> badges,
@Nullable final String username,
final UUID uuid) {
this.identityKey = identityKey;
this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
this.capabilities = capabilities;
this.badges = badges;
this.username = username;
this.uuid = uuid;
}
public String getIdentityKey() {
return identityKey;
}
public String getUnidentifiedAccess() {
return unidentifiedAccess;
}
public boolean isUnrestrictedUnidentifiedAccess() {
return unrestrictedUnidentifiedAccess;
}
public UserCapabilities getCapabilities() {
return capabilities;
}
public List<Badge> getBadges() {
return badges;
}
public String getUsername() {
return username;
}
public UUID getUuid() {
return uuid;
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
public abstract class CredentialProfileResponse {
@JsonUnwrapped
private VersionedProfileResponse versionedProfileResponse;
protected CredentialProfileResponse() {
}
protected CredentialProfileResponse(final VersionedProfileResponse versionedProfileResponse) {
this.versionedProfileResponse = versionedProfileResponse;
}
public VersionedProfileResponse getVersionedProfileResponse() {
return versionedProfileResponse;
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.signal.zkgroup.profiles.PniCredentialResponse;
import javax.annotation.Nullable;
import java.util.List;
import java.util.UUID;
public class PniCredentialProfileResponse extends CredentialProfileResponse {
@JsonProperty
@JsonSerialize(using = PniCredentialResponseAdapter.Serializing.class)
@JsonDeserialize(using = PniCredentialResponseAdapter.Deserializing.class)
private PniCredentialResponse pniCredential;
public PniCredentialProfileResponse() {
}
public PniCredentialProfileResponse(final VersionedProfileResponse versionedProfileResponse,
final PniCredentialResponse pniCredential) {
super(versionedProfileResponse);
this.pniCredential = pniCredential;
}
public PniCredentialResponse getPniCredential() {
return pniCredential;
}
}

View File

@ -1,151 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
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 java.util.List;
import java.util.UUID;
import org.signal.zkgroup.profiles.PniCredentialResponse;
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
public class Profile {
@JsonProperty
private String identityKey;
@JsonProperty
private String name;
@JsonProperty
private String about;
@JsonProperty
private String aboutEmoji;
@JsonProperty
private String avatar;
@JsonProperty
private String paymentAddress;
@JsonProperty
private String unidentifiedAccess;
@JsonProperty
private boolean unrestrictedUnidentifiedAccess;
@JsonProperty
private UserCapabilities capabilities;
@JsonProperty
private String username;
@JsonProperty
private UUID uuid;
@JsonProperty
private List<Badge> badges;
@JsonProperty
@JsonSerialize(using = ProfileKeyCredentialResponseAdapter.Serializing.class)
@JsonDeserialize(using = ProfileKeyCredentialResponseAdapter.Deserializing.class)
private ProfileKeyCredentialResponse credential;
@JsonProperty
@JsonSerialize(using = PniCredentialResponseAdapter.Serializing.class)
@JsonDeserialize(using = PniCredentialResponseAdapter.Deserializing.class)
private PniCredentialResponse pniCredential;
public Profile() {}
public Profile(
String name, String about, String aboutEmoji, String avatar, String paymentAddress, String identityKey,
String unidentifiedAccess, boolean unrestrictedUnidentifiedAccess, UserCapabilities capabilities, String username,
UUID uuid, List<Badge> badges, ProfileKeyCredentialResponse credential, PniCredentialResponse pniCredential)
{
this.name = name;
this.about = about;
this.aboutEmoji = aboutEmoji;
this.avatar = avatar;
this.paymentAddress = paymentAddress;
this.identityKey = identityKey;
this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
this.capabilities = capabilities;
this.username = username;
this.uuid = uuid;
this.badges = badges;
this.credential = credential;
this.pniCredential = pniCredential;
}
@VisibleForTesting
public String getIdentityKey() {
return identityKey;
}
@VisibleForTesting
public String getName() {
return name;
}
public String getAbout() {
return about;
}
public String getAboutEmoji() {
return aboutEmoji;
}
@VisibleForTesting
public String getAvatar() {
return avatar;
}
public String getPaymentAddress() {
return paymentAddress;
}
@VisibleForTesting
public String getUnidentifiedAccess() {
return unidentifiedAccess;
}
@VisibleForTesting
public boolean isUnrestrictedUnidentifiedAccess() {
return unrestrictedUnidentifiedAccess;
}
@VisibleForTesting
public UserCapabilities getCapabilities() {
return capabilities;
}
@VisibleForTesting
public String getUsername() {
return username;
}
@VisibleForTesting
public UUID getUuid() {
return uuid;
}
public List<Badge> getBadges() {
return badges;
}
public ProfileKeyCredentialResponse getCredential() {
return credential;
}
public PniCredentialResponse getPniCredential() {
return pniCredential;
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.signal.zkgroup.profiles.PniCredentialResponse;
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
import javax.annotation.Nullable;
import java.util.List;
import java.util.UUID;
public class ProfileKeyCredentialProfileResponse extends CredentialProfileResponse {
@JsonProperty
@JsonSerialize(using = ProfileKeyCredentialResponseAdapter.Serializing.class)
@JsonDeserialize(using = ProfileKeyCredentialResponseAdapter.Deserializing.class)
private ProfileKeyCredentialResponse credential;
public ProfileKeyCredentialProfileResponse() {
}
public ProfileKeyCredentialProfileResponse(final VersionedProfileResponse versionedProfileResponse,
final ProfileKeyCredentialResponse credential) {
super(versionedProfileResponse);
this.credential = credential;
}
public ProfileKeyCredentialResponse getCredential() {
return credential;
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import javax.annotation.Nullable;
import java.util.List;
import java.util.UUID;
public class VersionedProfileResponse {
@JsonUnwrapped
private BaseProfileResponse baseProfileResponse;
@JsonProperty
private String name;
@JsonProperty
private String about;
@JsonProperty
private String aboutEmoji;
@JsonProperty
private String avatar;
@JsonProperty
private String paymentAddress;
public VersionedProfileResponse() {
}
public VersionedProfileResponse(final BaseProfileResponse baseProfileResponse,
final String name,
final String about,
final String aboutEmoji,
final String avatar,
final String paymentAddress) {
this.baseProfileResponse = baseProfileResponse;
this.name = name;
this.about = about;
this.aboutEmoji = aboutEmoji;
this.avatar = avatar;
this.paymentAddress = paymentAddress;
}
public BaseProfileResponse getBaseProfileResponse() {
return baseProfileResponse;
}
public String getName() {
return name;
}
public String getAbout() {
return about;
}
public String getAboutEmoji() {
return aboutEmoji;
}
public String getAvatar() {
return avatar;
}
public String getPaymentAddress() {
return paymentAddress;
}
}

View File

@ -73,9 +73,12 @@ import org.whispersystems.textsecuregcm.controllers.ProfileController;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.Badge;
import org.whispersystems.textsecuregcm.entities.BadgeSvg;
import org.whispersystems.textsecuregcm.entities.BaseProfileResponse;
import org.whispersystems.textsecuregcm.entities.CreateProfileRequest;
import org.whispersystems.textsecuregcm.entities.Profile;
import org.whispersystems.textsecuregcm.entities.PniCredentialProfileResponse;
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
import org.whispersystems.textsecuregcm.entities.ProfileKeyCredentialProfileResponse;
import org.whispersystems.textsecuregcm.entities.VersionedProfileResponse;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.s3.PolicySigner;
@ -209,16 +212,14 @@ class ProfileControllerTest {
@Test
void testProfileGetByUuid() throws RateLimitExceededException {
Profile profile= resources.getJerseyTest()
BaseProfileResponse profile = resources.getJerseyTest()
.target("/v1/profile/" + AuthHelper.VALID_UUID_TWO)
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(Profile.class);
.get(BaseProfileResponse.class);
assertThat(profile.getIdentityKey()).isEqualTo("bar");
assertThat(profile.getName()).isNull();
assertThat(profile.getAvatar()).isNull();
assertThat(profile.getUsername()).isEqualTo("n00bkiller");
assertThat(profile.getUsername()).isNull();
assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>(
badge -> "Test Badge".equals(badge.getName()), "has badge with expected name"));
@ -228,16 +229,14 @@ class ProfileControllerTest {
@Test
void testProfileGetByUsername() throws RateLimitExceededException {
Profile profile= resources.getJerseyTest()
BaseProfileResponse profile = resources.getJerseyTest()
.target("/v1/profile/username/n00bkiller")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(Profile.class);
.get(BaseProfileResponse.class);
assertThat(profile.getIdentityKey()).isEqualTo("bar");
assertThat(profile.getName()).isNull();
assertThat(profile.getAvatar()).isNull();
assertThat(profile.getUsername()).isEqualTo("n00bkiller");
assertThat(profile.getUsername()).isNull();
assertThat(profile.getUuid()).isEqualTo(AuthHelper.VALID_UUID_TWO);
assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>(
badge -> "Test Badge".equals(badge.getName()), "has badge with expected name"));
@ -295,11 +294,11 @@ class ProfileControllerTest {
@Test
void testProfileCapabilities() {
Profile profile= resources.getJerseyTest()
BaseProfileResponse profile = resources.getJerseyTest()
.target("/v1/profile/" + AuthHelper.VALID_UUID)
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(Profile.class);
.get(BaseProfileResponse.class);
assertThat(profile.getCapabilities().isGv2()).isTrue();
assertThat(profile.getCapabilities().isGv1Migration()).isTrue();
@ -311,7 +310,7 @@ class ProfileControllerTest {
.target("/v1/profile/" + AuthHelper.VALID_UUID_TWO)
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO))
.get(Profile.class);
.get(BaseProfileResponse.class);
assertThat(profile.getCapabilities().isGv2()).isFalse();
assertThat(profile.getCapabilities().isGv1Migration()).isFalse();
@ -538,22 +537,22 @@ class ProfileControllerTest {
@Test
void testGetProfileByVersion() throws RateLimitExceededException {
Profile profile = resources.getJerseyTest()
VersionedProfileResponse profile = resources.getJerseyTest()
.target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(Profile.class);
.get(VersionedProfileResponse.class);
assertThat(profile.getIdentityKey()).isEqualTo("bar");
assertThat(profile.getBaseProfileResponse().getIdentityKey()).isEqualTo("bar");
assertThat(profile.getName()).isEqualTo("validname");
assertThat(profile.getAbout()).isEqualTo("about");
assertThat(profile.getAboutEmoji()).isEqualTo("emoji");
assertThat(profile.getAvatar()).isEqualTo("profiles/validavatar");
assertThat(profile.getCapabilities().isGv2()).isFalse();
assertThat(profile.getCapabilities().isGv1Migration()).isFalse();
assertThat(profile.getUsername()).isEqualTo("n00bkiller");
assertThat(profile.getUuid()).isNull();
assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>(
assertThat(profile.getBaseProfileResponse().getCapabilities().isGv2()).isFalse();
assertThat(profile.getBaseProfileResponse().getCapabilities().isGv1Migration()).isFalse();
assertThat(profile.getBaseProfileResponse().getUsername()).isNull();
assertThat(profile.getBaseProfileResponse().getUuid()).isEqualTo(AuthHelper.VALID_UUID_TWO);
assertThat(profile.getBaseProfileResponse().getBadges()).hasSize(1).element(0).has(new Condition<>(
badge -> "Test Badge".equals(badge.getName()), "has badge with expected name"));
verify(accountsManager, times(1)).getByAccountIdentifier(eq(AuthHelper.VALID_UUID_TWO));
@ -587,11 +586,11 @@ class ProfileControllerTest {
void testGetProfileReturnsNoPaymentAddressIfCurrentVersionMismatch() {
when(profilesManager.get(AuthHelper.VALID_UUID_TWO, "validversion")).thenReturn(
Optional.of(new VersionedProfile(null, null, null, null, null, "paymentaddress", null)));
Profile profile = resources.getJerseyTest()
VersionedProfileResponse profile = resources.getJerseyTest()
.target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(Profile.class);
.get(VersionedProfileResponse.class);
assertThat(profile.getPaymentAddress()).isEqualTo("paymentaddress");
when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.of("validversion"));
@ -599,7 +598,7 @@ class ProfileControllerTest {
.target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(Profile.class);
.get(VersionedProfileResponse.class);
assertThat(profile.getPaymentAddress()).isEqualTo("paymentaddress");
when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.of("someotherversion"));
@ -607,7 +606,7 @@ class ProfileControllerTest {
.target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(Profile.class);
.get(VersionedProfileResponse.class);
assertThat(profile.getPaymentAddress()).isNull();
}
@ -747,15 +746,14 @@ class ProfileControllerTest {
when(zkProfileOperations.issueProfileKeyCredential(credentialRequest, AuthHelper.VALID_UUID, profileKeyCommitment))
.thenReturn(credentialResponse);
final Profile profile = resources.getJerseyTest()
final ProfileKeyCredentialProfileResponse profile = resources.getJerseyTest()
.target(String.format("/v1/profile/%s/%s/%s", AuthHelper.VALID_UUID, version, Hex.encodeHexString(credentialRequest.serialize())))
.request()
.headers(authHeaders)
.get(Profile.class);
.get(ProfileKeyCredentialProfileResponse.class);
assertThat(profile.getUuid()).isNull();
assertThat(profile.getVersionedProfileResponse().getBaseProfileResponse().getUuid()).isEqualTo(AuthHelper.VALID_UUID);
assertThat(profile.getCredential()).isEqualTo(credentialResponse);
assertThat(profile.getPniCredential()).isNull();
verify(zkProfileOperations).issueProfileKeyCredential(credentialRequest, AuthHelper.VALID_UUID, profileKeyCommitment);
verify(zkProfileOperations, never()).issuePniCredential(any(), any(), any(), any());
@ -809,15 +807,14 @@ class ProfileControllerTest {
when(zkProfileOperations.issuePniCredential(credentialRequest, AuthHelper.VALID_UUID, AuthHelper.VALID_PNI, profileKeyCommitment))
.thenReturn(credentialResponse);
final Profile profile = resources.getJerseyTest()
final PniCredentialProfileResponse profile = resources.getJerseyTest()
.target(String.format("/v1/profile/%s/%s/%s", AuthHelper.VALID_UUID, version, Hex.encodeHexString(credentialRequest.serialize())))
.queryParam("credentialType", "pni")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(Profile.class);
.get(PniCredentialProfileResponse.class);
assertThat(profile.getUuid()).isNull();
assertThat(profile.getCredential()).isNull();
assertThat(profile.getVersionedProfileResponse().getBaseProfileResponse().getUuid()).isEqualTo(AuthHelper.VALID_UUID);
assertThat(profile.getPniCredential()).isEqualTo(credentialResponse);
verify(zkProfileOperations, never()).issueProfileKeyCredential(any(), any(), any());