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 1e5ffe9e3..0ba8a9c9d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java @@ -64,7 +64,6 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; import org.apache.commons.lang3.StringUtils; import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.VerificationFailedException; @@ -178,24 +177,27 @@ public class ProfileController { @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Response setProfile(@Auth AuthenticatedAccount auth, @NotNull @Valid CreateProfileRequest request) { + + final Optional currentProfile = profilesManager.get(auth.getAccount().getUuid(), + request.getVersion()); + if (StringUtils.isNotBlank(request.getPaymentAddress())) { final boolean hasDisallowedPrefix = dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedPrefixes().stream() .anyMatch(prefix -> auth.getAccount().getNumber().startsWith(prefix)); - if (hasDisallowedPrefix) { - return Response.status(Status.FORBIDDEN).build(); + if (hasDisallowedPrefix && currentProfile.map(VersionedProfile::getPaymentAddress).isEmpty()) { + return Response.status(Response.Status.FORBIDDEN).build(); } } - Optional currentProfile = profilesManager.get(auth.getAccount().getUuid(), request.getVersion()); - Optional currentAvatar = Optional.empty(); - if (currentProfile.isPresent() && currentProfile.get().getAvatar() != null && currentProfile.get().getAvatar().startsWith("profiles/")) { + if (currentProfile.isPresent() && currentProfile.get().getAvatar() != null && currentProfile.get().getAvatar() + .startsWith("profiles/")) { currentAvatar = Optional.of(currentProfile.get().getAvatar()); } - String avatar = switch (request.getAvatarChange()) { + final String avatar = switch (request.getAvatarChange()) { case UNCHANGED -> currentAvatar.orElse(null); case CLEAR -> null; case UPDATE -> generateAvatarObjectName(); @@ -218,7 +220,7 @@ public class ProfileController { .build())); } - List updatedBadges = request.getBadges() + final List updatedBadges = request.getBadges() .map(badges -> mergeBadgeIdsWithExistingAccountBadges(badges, auth.getAccount().getBadges())) .orElseGet(() -> auth.getAccount().getBadges()); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java index 96565ea01..b5ebe1d15 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java @@ -59,6 +59,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.ServerPublicParams; @@ -729,7 +730,9 @@ class ProfileControllerTest { .target("/v1/profile") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) - .put(Entity.entity(new CreateProfileRequest(commitment, "yetanotherversion", name, null, null, paymentAddress, false, false, List.of()), MediaType.APPLICATION_JSON_TYPE)); + .put(Entity.entity( + new CreateProfileRequest(commitment, "yetanotherversion", name, null, null, paymentAddress, false, false, + List.of()), MediaType.APPLICATION_JSON_TYPE)); assertThat(response.getStatus()).isEqualTo(403); assertThat(response.hasEntity()).isFalse(); @@ -737,13 +740,68 @@ class ProfileControllerTest { verify(profilesManager, never()).set(any(), any()); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSetProfilePaymentAddressCountryNotAllowedExistingPaymentAddress( + final boolean existingPaymentAddressOnProfile) throws InvalidInputException { + when(dynamicPaymentsConfiguration.getDisallowedPrefixes()) + .thenReturn(List.of(AuthHelper.VALID_NUMBER_TWO.substring(0, 3))); + + ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(AuthHelper.VALID_UUID); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + + when(profilesManager.get(eq(AuthHelper.VALID_UUID_TWO), any())) + .thenReturn(Optional.of( + new VersionedProfile("1", "name", null, null, null, + existingPaymentAddressOnProfile ? RandomStringUtils.randomAlphanumeric(776) : null, + commitment.serialize()))); + + final String name = RandomStringUtils.randomAlphabetic(380); + final String paymentAddress = RandomStringUtils.randomAlphanumeric(776); + + Response response = resources.getJerseyTest() + .target("/v1/profile") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity( + new CreateProfileRequest(commitment, "yetanotherversion", name, null, null, paymentAddress, false, false, + List.of()), MediaType.APPLICATION_JSON_TYPE)); + + if (existingPaymentAddressOnProfile) { + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager).get(eq(AuthHelper.VALID_UUID_TWO), eq("yetanotherversion")); + verify(profilesManager).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture()); + + verifyNoMoreInteractions(s3client); + + final VersionedProfile profile = profileArgumentCaptor.getValue(); + assertThat(profile.getCommitment()).isEqualTo(commitment.serialize()); + assertThat(profile.getAvatar()).isNull(); + assertThat(profile.getVersion()).isEqualTo("yetanotherversion"); + assertThat(profile.getName()).isEqualTo(name); + assertThat(profile.getAboutEmoji()).isNull(); + assertThat(profile.getAbout()).isNull(); + assertThat(profile.getPaymentAddress()).isEqualTo(paymentAddress); + } else { + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.hasEntity()).isFalse(); + + verify(profilesManager, never()).set(any(), any()); + } + } + @Test void testGetProfileByVersion() throws RateLimitExceededException { VersionedProfileResponse profile = resources.getJerseyTest() - .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion") - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .get(VersionedProfileResponse.class); + .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(VersionedProfileResponse.class); assertThat(profile.getBaseProfileResponse().getIdentityKey()).isEqualTo(ACCOUNT_TWO_IDENTITY_KEY); assertThat(profile.getName()).isEqualTo("validname");