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 78415331e..1cbcd6238 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java @@ -24,6 +24,7 @@ import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.UsernamesManager; +import org.whispersystems.textsecuregcm.util.ExactlySize; import org.whispersystems.textsecuregcm.util.Pair; import javax.ws.rs.GET; @@ -156,7 +157,7 @@ public class ProfileController { @PUT @Produces(MediaType.APPLICATION_JSON) @Path("/name/{name}") - public void setProfile(@Auth Account account, @PathParam("name") @UnwrapValidatedValue(true) @Length(min = 72,max= 72) Optional name) { + public void setProfile(@Auth Account account, @PathParam("name") @UnwrapValidatedValue(true) @ExactlySize({72, 108}) Optional name) { account.setProfileName(name.orElse(null)); accountsManager.update(account); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySize.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySize.java new file mode 100644 index 000000000..182b04593 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySize.java @@ -0,0 +1,34 @@ +package org.whispersystems.textsecuregcm.util; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE }) +@Retention(RUNTIME) +@Constraint(validatedBy = ExactlySizeValidator.class) +@Documented +public @interface ExactlySize { + + String message() default "{org.whispersystems.textsecuregcm.util.ExactlySize." + + "message}"; + + Class[] groups() default { }; + + Class[] payload() default { }; + + int[] value(); + + @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE }) + @Retention(RUNTIME) + @Documented + @interface List { + ExactlySize[] value(); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidator.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidator.java new file mode 100644 index 000000000..2fa8cdd5d --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidator.java @@ -0,0 +1,29 @@ +package org.whispersystems.textsecuregcm.util; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + + +public class ExactlySizeValidator implements ConstraintValidator { + + private int[] permittedSizes; + + @Override + public void initialize(ExactlySize exactlySize) { + this.permittedSizes = exactlySize.value(); + } + + @Override + public boolean isValid(String object, ConstraintValidatorContext constraintContext) { + int objectLength; + + if (object == null) objectLength = 0; + else objectLength = object.length(); + + for (int permittedSize : permittedSizes) { + if (permittedSize == objectLength) return true; + } + + return false; + } +} 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 8d3e26dd3..9cc8ddfd2 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 @@ -5,7 +5,9 @@ import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; +import org.mockito.Mockito; import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount; import org.whispersystems.textsecuregcm.configuration.CdnConfiguration; @@ -20,6 +22,7 @@ import org.whispersystems.textsecuregcm.storage.UsernamesManager; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.util.SystemMapper; +import javax.ws.rs.client.Entity; import javax.ws.rs.core.Response; import java.util.Optional; @@ -89,6 +92,8 @@ public class ProfileControllerTest { when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(capabilitiesAccount)); when(accountsManager.get(argThat((ArgumentMatcher) identifier -> identifier != null && identifier.hasNumber() && identifier.getNumber().equals(AuthHelper.VALID_NUMBER)))).thenReturn(Optional.of(capabilitiesAccount)); + + Mockito.clearInvocations(accountsManager); } @Test @@ -209,4 +214,42 @@ public class ProfileControllerTest { assertThat(profile.getCapabilities().isUuid()).isTrue(); } + @Test + public void testSetProfileName() { + Response response = resources.getJerseyTest() + .target("/v1/profile/name/123456789012345678901234567890123456789012345678901234567890123456789012") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .put(Entity.text("")); + + assertThat(response.getStatus()).isEqualTo(204); + + verify(accountsManager, times(1)).update(any(Account.class)); + } + + @Test + public void testSetProfileNameExtended() { + Response response = resources.getJerseyTest() + .target("/v1/profile/name/123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .put(Entity.text("")); + + assertThat(response.getStatus()).isEqualTo(204); + + verify(accountsManager, times(1)).update(any(Account.class)); + } + + @Test + public void testSetProfileNameWrongSize() { + Response response = resources.getJerseyTest() + .target("/v1/profile/name/1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .put(Entity.text("")); + + assertThat(response.getStatus()).isEqualTo(400); + verifyNoMoreInteractions(accountsManager); + } + }