Introduce a username validator
This commit is contained in:
parent
17c9b4c5d3
commit
efb410444b
|
@ -91,6 +91,8 @@ import org.whispersystems.textsecuregcm.util.ForwardedIpUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.Hex;
|
import org.whispersystems.textsecuregcm.util.Hex;
|
||||||
import org.whispersystems.textsecuregcm.util.ImpossiblePhoneNumberException;
|
import org.whispersystems.textsecuregcm.util.ImpossiblePhoneNumberException;
|
||||||
import org.whispersystems.textsecuregcm.util.NonNormalizedPhoneNumberException;
|
import org.whispersystems.textsecuregcm.util.NonNormalizedPhoneNumberException;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Username;
|
||||||
|
import org.whispersystems.textsecuregcm.util.UsernameValidator;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||||
|
|
||||||
|
@ -617,22 +619,12 @@ public class AccountController {
|
||||||
@PUT
|
@PUT
|
||||||
@Path("/username/{username}")
|
@Path("/username/{username}")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Response setUsername(@Auth AuthenticatedAccount auth, @PathParam("username") String username)
|
public Response setUsername(@Auth AuthenticatedAccount auth, @PathParam("username") @Username String username)
|
||||||
throws RateLimitExceededException {
|
throws RateLimitExceededException {
|
||||||
rateLimiters.getUsernameSetLimiter().validate(auth.getAccount().getUuid());
|
rateLimiters.getUsernameSetLimiter().validate(auth.getAccount().getUuid());
|
||||||
|
|
||||||
if (username == null || username.isEmpty()) {
|
|
||||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
username = username.toLowerCase();
|
|
||||||
|
|
||||||
if (!username.matches("^[a-z_][a-z0-9_]+$")) {
|
|
||||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
accounts.setUsername(auth.getAccount(), username);
|
accounts.setUsername(auth.getAccount(), UsernameValidator.getCanonicalUsername(username));
|
||||||
} catch (final UsernameNotAvailableException e) {
|
} catch (final UsernameNotAvailableException e) {
|
||||||
return Response.status(Response.Status.CONFLICT).build();
|
return Response.status(Response.Status.CONFLICT).build();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013-2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.util;
|
||||||
|
|
||||||
|
import static java.lang.annotation.ElementType.FIELD;
|
||||||
|
import static java.lang.annotation.ElementType.PARAMETER;
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
import javax.validation.Constraint;
|
||||||
|
import javax.validation.Payload;
|
||||||
|
|
||||||
|
@Target({ FIELD, PARAMETER })
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
@Constraint(validatedBy = UsernameValidator.class)
|
||||||
|
public @interface Username {
|
||||||
|
|
||||||
|
String message() default "{org.whispersystems.textsecuregcm.util.Username.message}";
|
||||||
|
|
||||||
|
Class<?>[] groups() default { };
|
||||||
|
|
||||||
|
Class<? extends Payload>[] payload() default { };
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013-2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.util;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import javax.validation.ConstraintValidator;
|
||||||
|
import javax.validation.ConstraintValidatorContext;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class UsernameValidator implements ConstraintValidator<Username, String> {
|
||||||
|
|
||||||
|
private static final Pattern USERNAME_PATTERN =
|
||||||
|
Pattern.compile("^[a-z_][a-z0-9_]{3,25}$", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(final String username, final ConstraintValidatorContext context) {
|
||||||
|
return StringUtils.isNotBlank(username) && USERNAME_PATTERN.matcher(getCanonicalUsername(username)).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCanonicalUsername(final String username) {
|
||||||
|
return username != null ? username.toLowerCase() : null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013-2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
|
class UsernameValidatorTest {
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
void isValid(final String username, final boolean expectValid) {
|
||||||
|
final UsernameValidator usernameValidator = new UsernameValidator();
|
||||||
|
|
||||||
|
assertEquals(expectValid, usernameValidator.isValid(username, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> isValid() {
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of("test", true),
|
||||||
|
Arguments.of("_test", true),
|
||||||
|
Arguments.of("test123", true),
|
||||||
|
Arguments.of("a", false), // Too short
|
||||||
|
Arguments.of("thisIsAReallyReallyReallyLongUsernameThatWeWouldNotAllow", false),
|
||||||
|
Arguments.of("Illegal character", false),
|
||||||
|
Arguments.of("0test", false), // Illegal first character
|
||||||
|
Arguments.of("pаypal", false), // Unicode confusable characters
|
||||||
|
Arguments.of("test\uD83D\uDC4E", false), // Emoji
|
||||||
|
Arguments.of(" ", false),
|
||||||
|
Arguments.of("", false),
|
||||||
|
Arguments.of(null, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
void getCanonicalUsername(final String username, final String expectedCanonicalUsername) {
|
||||||
|
assertEquals(expectedCanonicalUsername, UsernameValidator.getCanonicalUsername(username));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> getCanonicalUsername() {
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of("test", "test"),
|
||||||
|
Arguments.of("TEst", "test"),
|
||||||
|
Arguments.of("t_e_S_T", "t_e_s_t"),
|
||||||
|
Arguments.of(null, null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue