From 5816f76bbe72db5114d347f4d30f8c0c23678496 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Wed, 13 Apr 2022 10:35:54 -0400 Subject: [PATCH] Add support for getting (limited) profiles by phone number identifier --- .../controllers/ProfileController.java | 49 ++++++++++--- .../controllers/ProfileControllerTest.java | 71 +++++++++++++++++-- .../textsecuregcm/tests/util/AuthHelper.java | 3 + 3 files changed, 108 insertions(+), 15 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java index a2a37924c..b16814e5f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java @@ -13,6 +13,7 @@ import java.time.Duration; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; @@ -228,7 +229,7 @@ public class ProfileController { throws RateLimitExceededException { final Optional maybeRequester = auth.map(AuthenticatedAccount::getAccount); - final Account targetAccount = verifyPermissionToReceiveProfile(maybeRequester, accessKey, uuid); + final Account targetAccount = verifyPermissionToReceiveAccountIdentityProfile(maybeRequester, accessKey, uuid); return buildVersionedProfileResponse(targetAccount, version, @@ -251,7 +252,7 @@ public class ProfileController { throws RateLimitExceededException { final Optional maybeRequester = auth.map(AuthenticatedAccount::getAccount); - final Account targetAccount = verifyPermissionToReceiveProfile(maybeRequester, accessKey, uuid); + final Account targetAccount = verifyPermissionToReceiveAccountIdentityProfile(maybeRequester, accessKey, uuid); final boolean isSelf = isSelfProfileRequest(maybeRequester, uuid); switch (credentialType) { @@ -293,12 +294,30 @@ public class ProfileController { @QueryParam("ca") boolean useCaCertificate) throws RateLimitExceededException { + final Optional maybeAccountByPni = accountsManager.getByPhoneNumberIdentifier(identifier); final Optional maybeRequester = auth.map(AuthenticatedAccount::getAccount); - final Account targetAccount = verifyPermissionToReceiveProfile(maybeRequester, accessKey, identifier); - return buildBaseProfileResponse(targetAccount, - isSelfProfileRequest(maybeRequester, identifier), - containerRequestContext); + final BaseProfileResponse profileResponse; + + if (maybeAccountByPni.isPresent()) { + if (maybeRequester.isEmpty()) { + throw new WebApplicationException(Response.Status.UNAUTHORIZED); + } else { + rateLimiters.getProfileLimiter().validate(maybeRequester.get().getUuid()); + } + + OptionalAccess.verify(maybeRequester, Optional.empty(), maybeAccountByPni); + + profileResponse = buildBaseProfileResponseForPhoneNumberIdentity(maybeAccountByPni.get()); + } else { + final Account targetAccount = verifyPermissionToReceiveAccountIdentityProfile(maybeRequester, accessKey, identifier); + + profileResponse = buildBaseProfileResponseForAccountIdentity(targetAccount, + isSelfProfileRequest(maybeRequester, identifier), + containerRequestContext); + } + + return profileResponse; } private ProfileKeyCredentialProfileResponse buildProfileKeyCredentialProfileResponse(final Account account, @@ -354,11 +373,12 @@ public class ProfileController { .map(VersionedProfile::getPaymentAddress) .orElse(null); - return new VersionedProfileResponse(buildBaseProfileResponse(account, isSelf, containerRequestContext), + return new VersionedProfileResponse( + buildBaseProfileResponseForAccountIdentity(account, isSelf, containerRequestContext), name, about, aboutEmoji, avatar, paymentAddress); } - private BaseProfileResponse buildBaseProfileResponse(final Account account, + private BaseProfileResponse buildBaseProfileResponseForAccountIdentity(final Account account, final boolean isSelf, final ContainerRequestContext containerRequestContext) { @@ -373,6 +393,15 @@ public class ProfileController { account.getUuid()); } + private BaseProfileResponse buildBaseProfileResponseForPhoneNumberIdentity(final Account account) { + return new BaseProfileResponse(account.getPhoneNumberIdentityKey(), + null, + false, + UserCapabilities.createForAccount(account), + Collections.emptyList(), + account.getPhoneNumberIdentifier()); + } + @Timed @GET @Produces(MediaType.APPLICATION_JSON) @@ -388,7 +417,7 @@ public class ProfileController { final Account targetAccount = accountsManager.getByUsername(username).orElseThrow(NotFoundException::new); final boolean isSelf = auth.getAccount().getUuid().equals(targetAccount.getUuid()); - return buildBaseProfileResponse(targetAccount, isSelf, containerRequestContext); + return buildBaseProfileResponseForAccountIdentity(targetAccount, isSelf, containerRequestContext); } private ProfileKeyCredentialResponse getProfileCredential(final String encodedProfileCredentialRequest, @@ -507,7 +536,7 @@ public class ProfileController { * @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 maybeRequester, + private Account verifyPermissionToReceiveAccountIdentityProfile(final Optional maybeRequester, final Optional maybeAccessKey, final UUID targetUuid) throws RateLimitExceededException { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java index 47fa2757b..2fa4543cc 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java @@ -172,7 +172,9 @@ class ProfileControllerTest { profileAccount = mock(Account.class); when(profileAccount.getIdentityKey()).thenReturn("bar"); + when(profileAccount.getPhoneNumberIdentityKey()).thenReturn("baz"); when(profileAccount.getUuid()).thenReturn(AuthHelper.VALID_UUID_TWO); + when(profileAccount.getPhoneNumberIdentifier()).thenReturn(AuthHelper.VALID_PNI_TWO); when(profileAccount.isEnabled()).thenReturn(true); when(profileAccount.isGroupsV2Supported()).thenReturn(false); when(profileAccount.isGv1MigrationSupported()).thenReturn(false); @@ -195,6 +197,7 @@ class ProfileControllerTest { when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(profileAccount)); when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID_TWO)).thenReturn(Optional.of(profileAccount)); + when(accountsManager.getByPhoneNumberIdentifier(AuthHelper.VALID_PNI_TWO)).thenReturn(Optional.of(profileAccount)); when(accountsManager.getByUsername("n00bkiller")).thenReturn(Optional.of(profileAccount)); when(accountsManager.getByE164(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(capabilitiesAccount)); @@ -218,7 +221,7 @@ class ProfileControllerTest { } @Test - void testProfileGetByUuid() throws RateLimitExceededException { + void testProfileGetByAci() throws RateLimitExceededException { BaseProfileResponse profile = resources.getJerseyTest() .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO) .request() @@ -234,7 +237,7 @@ class ProfileControllerTest { } @Test - void testProfileGetByUuidRateLimited() throws RateLimitExceededException { + void testProfileGetByAciRateLimited() throws RateLimitExceededException { doThrow(new RateLimitExceededException(Duration.ofSeconds(13))).when(rateLimiter).validate(AuthHelper.VALID_UUID); Response response= resources.getJerseyTest() @@ -248,7 +251,7 @@ class ProfileControllerTest { } @Test - void testProfileGetByUuidUnidentified() throws RateLimitExceededException { + void testProfileGetByAciUnidentified() throws RateLimitExceededException { BaseProfileResponse profile = resources.getJerseyTest() .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO) .request() @@ -264,7 +267,7 @@ class ProfileControllerTest { } @Test - void testProfileGetByUuidUnidentifiedBadKey() { + void testProfileGetByAciUnidentifiedBadKey() { final Response response = resources.getJerseyTest() .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO) .request() @@ -275,7 +278,7 @@ class ProfileControllerTest { } @Test - void testProfileGetByUuidUnidentifiedAccountNotFound() { + void testProfileGetByAciUnidentifiedAccountNotFound() { final Response response = resources.getJerseyTest() .target("/v1/profile/" + UUID.randomUUID()) .request() @@ -285,6 +288,64 @@ class ProfileControllerTest { assertThat(response.getStatus()).isEqualTo(401); } + @Test + void testProfileGetByPni() throws RateLimitExceededException { + BaseProfileResponse profile = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_PNI_TWO) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(BaseProfileResponse.class); + + assertThat(profile.getIdentityKey()).isEqualTo("baz"); + assertThat(profile.getBadges()).isEmpty(); + assertThat(profile.getUuid()).isEqualTo(AuthHelper.VALID_PNI_TWO); + assertThat(profile.getCapabilities()).isNotNull(); + assertThat(profile.isUnrestrictedUnidentifiedAccess()).isFalse(); + assertThat(profile.getUnidentifiedAccess()).isNull(); + + verify(accountsManager).getByPhoneNumberIdentifier(AuthHelper.VALID_PNI_TWO); + verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID); + } + + @Test + void testProfileGetByPniRateLimited() throws RateLimitExceededException { + doThrow(new RateLimitExceededException(Duration.ofSeconds(13))).when(rateLimiter).validate(AuthHelper.VALID_UUID); + + Response response= resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_PNI_TWO) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(); + + assertThat(response.getStatus()).isEqualTo(413); + assertThat(response.getHeaderString("Retry-After")).isEqualTo(String.valueOf(Duration.ofSeconds(13).toSeconds())); + } + + @Test + void testProfileGetByPniUnidentified() throws RateLimitExceededException { + final Response response = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_PNI_TWO) + .request() + .header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("1337".getBytes())) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + + verify(accountsManager).getByPhoneNumberIdentifier(AuthHelper.VALID_PNI_TWO); + verify(rateLimiter, never()).validate(AuthHelper.VALID_UUID); + } + + @Test + void testProfileGetByPniUnidentifiedBadKey() { + final Response response = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_PNI_TWO) + .request() + .header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("incorrect".getBytes())) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + } + @Test void testProfileGetByUsername() throws RateLimitExceededException { BaseProfileResponse profile = resources.getJerseyTest() diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java index 655f813c5..5c2ee6c71 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java @@ -42,6 +42,7 @@ public class AuthHelper { public static final String VALID_NUMBER_TWO = "+201511111110"; public static final UUID VALID_UUID_TWO = UUID.randomUUID(); + public static final UUID VALID_PNI_TWO = UUID.randomUUID(); public static final String VALID_PASSWORD_TWO = "baz"; public static final String VALID_NUMBER_3 = "+14445556666"; @@ -139,6 +140,7 @@ public class AuthHelper { when(VALID_ACCOUNT.getPhoneNumberIdentifier()).thenReturn(VALID_PNI); when(VALID_ACCOUNT_TWO.getNumber()).thenReturn(VALID_NUMBER_TWO); when(VALID_ACCOUNT_TWO.getUuid()).thenReturn(VALID_UUID_TWO); + when(VALID_ACCOUNT_TWO.getPhoneNumberIdentifier()).thenReturn(VALID_PNI_TWO); when(DISABLED_ACCOUNT.getNumber()).thenReturn(DISABLED_NUMBER); when(DISABLED_ACCOUNT.getUuid()).thenReturn(DISABLED_UUID); when(UNDISCOVERABLE_ACCOUNT.getNumber()).thenReturn(UNDISCOVERABLE_NUMBER); @@ -169,6 +171,7 @@ public class AuthHelper { when(ACCOUNTS_MANAGER.getByE164(VALID_NUMBER_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO)); when(ACCOUNTS_MANAGER.getByAccountIdentifier(VALID_UUID_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO)); + when(ACCOUNTS_MANAGER.getByPhoneNumberIdentifier(VALID_PNI_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO)); when(ACCOUNTS_MANAGER.getByE164(DISABLED_NUMBER)).thenReturn(Optional.of(DISABLED_ACCOUNT)); when(ACCOUNTS_MANAGER.getByAccountIdentifier(DISABLED_UUID)).thenReturn(Optional.of(DISABLED_ACCOUNT));