diff --git a/agate-core/pom.xml b/agate-core/pom.xml index 9f469d5ce..cd2b3b757 100644 --- a/agate-core/pom.xml +++ b/agate-core/pom.xml @@ -159,6 +159,10 @@ postgresql + + org.owasp.esapi + esapi + dev.samstevens.totp totp diff --git a/agate-core/src/main/java/org/obiba/agate/domain/Configuration.java b/agate-core/src/main/java/org/obiba/agate/domain/Configuration.java index 180857197..4a3f06076 100644 --- a/agate-core/src/main/java/org/obiba/agate/domain/Configuration.java +++ b/agate-core/src/main/java/org/obiba/agate/domain/Configuration.java @@ -14,7 +14,6 @@ import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; -import org.hibernate.validator.constraints.NotBlank; import org.obiba.mongodb.domain.AbstractAuditableDocument; import org.obiba.runtime.Version; import org.springframework.data.annotation.Transient; @@ -28,8 +27,6 @@ @Document public class Configuration extends AbstractAuditableDocument { - private static final long serialVersionUID = -9020464712632680519L; - public static final String DEFAULT_NAME = "Agate"; public static final Locale DEFAULT_LOCALE = Locale.ENGLISH; @@ -40,7 +37,6 @@ public class Configuration extends AbstractAuditableDocument { public static final int DEFAULT_INACTIVE_TIMEOUT = 365 * 24; // 1 year (in hours) - @NotBlank private String name = DEFAULT_NAME; private String domain; diff --git a/agate-core/src/main/java/org/obiba/agate/domain/User.java b/agate-core/src/main/java/org/obiba/agate/domain/User.java index 29358ea6a..da62dd795 100644 --- a/agate-core/src/main/java/org/obiba/agate/domain/User.java +++ b/agate-core/src/main/java/org/obiba/agate/domain/User.java @@ -14,7 +14,6 @@ import com.google.common.base.Strings; import com.google.common.collect.Maps; import com.google.common.collect.Sets; -import org.hibernate.validator.constraints.Email; import org.joda.time.DateTime; import org.obiba.agate.security.Roles; import org.obiba.mongodb.domain.AbstractAuditableDocument; @@ -41,7 +40,6 @@ public class User extends AbstractAuditableDocument { private String lastName; - @Email @Indexed(unique = true) private String email; diff --git a/agate-webapp/src/main/java/org/obiba/agate/web/controller/domain/UserProfile.java b/agate-core/src/main/java/org/obiba/agate/domain/UserProfile.java similarity index 91% rename from agate-webapp/src/main/java/org/obiba/agate/web/controller/domain/UserProfile.java rename to agate-core/src/main/java/org/obiba/agate/domain/UserProfile.java index 0e10d7c04..30b35e66a 100644 --- a/agate-webapp/src/main/java/org/obiba/agate/web/controller/domain/UserProfile.java +++ b/agate-core/src/main/java/org/obiba/agate/domain/UserProfile.java @@ -1,6 +1,5 @@ -package org.obiba.agate.web.controller.domain; +package org.obiba.agate.domain; -import org.obiba.agate.domain.User; import org.owasp.esapi.ESAPI; import java.util.Map; @@ -51,7 +50,7 @@ public String getDisplayName() { } public String getPreferredLanguage() { - return user.getPreferredLanguage(); + return ESAPI.encoder().encodeForHTML(user.getPreferredLanguage()); } public boolean getOtpEnabled() { diff --git a/agate-core/src/main/java/org/obiba/agate/service/UserService.java b/agate-core/src/main/java/org/obiba/agate/service/UserService.java index e06d39160..130763d32 100644 --- a/agate-core/src/main/java/org/obiba/agate/service/UserService.java +++ b/agate-core/src/main/java/org/obiba/agate/service/UserService.java @@ -38,6 +38,7 @@ import org.obiba.agate.repository.UserCredentialsRepository; import org.obiba.agate.repository.UserRepository; import org.obiba.agate.service.support.MessageResolverMethod; + import org.obiba.agate.validator.NameValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; @@ -78,6 +79,7 @@ public class UserService { + "(?=.*[@#$%^&+=!])" // a special character that must occur at least once + "(?=\\S+$).{" + PWD_MINIMUM_LENGTH + "," + PWD_MAXIMUM_LENGTH + "}$"); + @Value("${login.otpTimeout:300}") private int otpTimeout; @@ -266,6 +268,10 @@ public void updateUserPassword(@Nonnull User user, @Nonnull String password) { } public User createUser(@Nonnull User user, @Nullable String password) { + checkName(user.getName()); + checkName(user.getFirstName()); + checkName(user.getLastName()); + if (Strings.isNullOrEmpty(password)) { if (user.getRealm() == null) user.setRealm(AgateRealm.AGATE_USER_REALM.getName()); else { @@ -455,7 +461,7 @@ public void sendPendingEmail(UserJoinedEvent userJoinedEvent) { String organization = configurationService.getConfiguration().getName(); Map context = Maps.newHashMap(); - context.put("user", user); + context.put("user", new UserProfile(user)); administrators.forEach(u -> sendEmail(u, "[" + organization + "] " + env.getProperty("registration.pendingForReviewSubject"), "pendingForReviewEmail", context)); @@ -479,7 +485,7 @@ private void sendEmail(User user, String subject, String templateName, Map ctx = context == null ? Maps.newHashMap() : Maps.newHashMap(context); if (!ctx.containsKey("user")) - ctx.put("user", user); + ctx.put("user", new UserProfile(user)); ctx.put("organization", configurationService.getConfiguration().getName()); // get user's realm and find if there is a specific agate base url for this realm Optional realmConfig = getRealmConfigs(user).stream().findFirst(); @@ -711,4 +717,10 @@ private List getRealmConfigs(User user) { return realmConfigRepository.findAll().stream().filter(realmConfig -> user.getRealm().equals(realmConfig.getName())).collect(Collectors.toList()); } + private void checkName(String name) { + if (!NameValidator.isValid(name)) { + throw new BadRequestException("Name contains invalid characters"); + } + } + } diff --git a/agate-core/src/main/java/org/obiba/agate/validator/EmailValidator.java b/agate-core/src/main/java/org/obiba/agate/validator/EmailValidator.java new file mode 100644 index 000000000..82be7043d --- /dev/null +++ b/agate-core/src/main/java/org/obiba/agate/validator/EmailValidator.java @@ -0,0 +1,40 @@ +package org.obiba.agate.validator; + +import jakarta.mail.internet.InternetAddress; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.regex.Pattern.CASE_INSENSITIVE; + +public final class EmailValidator { + + public static boolean isValid(String email) { + if (email == null || email.trim().isEmpty()) { + return false; + } + + // Check length constraints + if (email.length() > 254) { + return false; // Email too long + } + + String[] parts = email.split("@"); + if (parts[0].length() > 64) { + return false; // Local part too long + } + + // Check for common issues + if (email.contains("..") || email.startsWith(".") || email.endsWith(".")) { + return false; // Invalid dot placement + } + + try { + InternetAddress emailAddr = new InternetAddress(email); + emailAddr.validate(); + return true; + } catch (Exception e) { + return false; + } + } +} diff --git a/agate-core/src/main/java/org/obiba/agate/validator/NameValidator.java b/agate-core/src/main/java/org/obiba/agate/validator/NameValidator.java new file mode 100644 index 000000000..7919db97e --- /dev/null +++ b/agate-core/src/main/java/org/obiba/agate/validator/NameValidator.java @@ -0,0 +1,18 @@ +package org.obiba.agate.validator; + +import java.util.regex.Pattern; + +public final class NameValidator { + + // International pattern supporting accents and other characters + private static final Pattern INTERNATIONAL_NAME_PATTERN = Pattern.compile( + "^[\\p{L}][\\p{L}\\s\\-']{1,49}$"); + + public static boolean isValid(String name) { + if (name == null || name.isEmpty()) { + return true; + } + + return INTERNATIONAL_NAME_PATTERN.matcher(name).matches(); + } +} diff --git a/agate-rest/src/main/java/org/obiba/agate/web/rest/user/UsersPublicResource.java b/agate-rest/src/main/java/org/obiba/agate/web/rest/user/UsersPublicResource.java index 842f6f4e5..5ded25ae9 100644 --- a/agate-rest/src/main/java/org/obiba/agate/web/rest/user/UsersPublicResource.java +++ b/agate-rest/src/main/java/org/obiba/agate/web/rest/user/UsersPublicResource.java @@ -19,13 +19,12 @@ import com.google.common.collect.Sets; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; -import org.bson.types.ObjectId; -import org.hibernate.validator.internal.constraintvalidators.hv.EmailValidator; import org.joda.time.DateTime; import org.obiba.agate.domain.*; import org.obiba.agate.security.AgateUserRealm; import org.obiba.agate.security.Roles; import org.obiba.agate.service.*; +import org.obiba.agate.validator.EmailValidator; import org.obiba.agate.web.rest.config.JerseyConfiguration; import org.obiba.agate.web.rest.security.InvalidApplicationKeyException; import org.obiba.shiro.authc.HttpAuthorizationToken; @@ -185,7 +184,7 @@ public Response create(@Context HttpServletRequest request, MultivaluedMap