diff --git a/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java b/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java
index 5d65a9d73c2..a306c142980 100644
--- a/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java
+++ b/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java
@@ -127,6 +127,11 @@ public class Settings {
public static final String NODE_DEFAULT = "node/default";
public static final String NODE_NAME = "node/name";
+ public static final String SYSTEM_SECURITY_PASSWORDENFORCEMENT_MINLENGH = "system/security/passwordEnforcement/minLength";
+ public static final String SYSTEM_SECURITY_PASSWORDENFORCEMENT_MAXLENGH = "system/security/passwordEnforcement/maxLength";
+ public static final String SYSTEM_SECURITY_PASSWORDENFORCEMENT_USEPATTERN = "system/security/passwordEnforcement/usePattern";
+ public static final String SYSTEM_SECURITY_PASSWORDENFORCEMENT_PATTERN = "system/security/passwordEnforcement/pattern";
+
public static class GNSetting {
private String name;
private boolean nullable;
diff --git a/core/src/main/resources/org/fao/geonet/api/Messages.properties b/core/src/main/resources/org/fao/geonet/api/Messages.properties
index 41cf0ccbc5c..326ea5aefce 100644
--- a/core/src/main/resources/org/fao/geonet/api/Messages.properties
+++ b/core/src/main/resources/org/fao/geonet/api/Messages.properties
@@ -131,3 +131,6 @@ self_registration_disabled=User self-registration is disabled
recaptcha_not_valid=Recaptcha is not valid
metadata.title.createdFromTemplate=Copy of template %s created at %s
metadata.title.createdFromRecord=Copy of record %s created at %s
+username.field.required=Username is required
+password.field.length=Password size should be between {min} and {max} characters
+password.field.invalid=Password must contain at least 1 uppercase, 1 lowercase, 1 number and 1 symbol. Symbols include: `~!@#$%^&*()-_=+[]{}\\|;:'",.<>/?');
diff --git a/core/src/main/resources/org/fao/geonet/api/Messages_fre.properties b/core/src/main/resources/org/fao/geonet/api/Messages_fre.properties
index 97c81fb54e2..6dd76bcd778 100644
--- a/core/src/main/resources/org/fao/geonet/api/Messages_fre.properties
+++ b/core/src/main/resources/org/fao/geonet/api/Messages_fre.properties
@@ -104,3 +104,6 @@ self_registration_disabled=User self-registration is disabled
recaptcha_not_valid=Recaptcha is not valid
metadata.title.createdFromTemplate=Copie du mod\u00e8le %s cr\u00E9\u00E9e le %s
metadata.title.createdFromRecord=Copie de la fiche %s cr\u00E9\u00E9e le %s
+username.field.required=Username is required
+password.field.length=Password size should be between {min} and {max} characters
+password.field.invalid=Password must contain at least 1 uppercase, 1 lowercase, 1 number and 1 symbol. Symbols include: `~!@#$%^&*()-_=+[]{}\\|;:'",.<>/?');
diff --git a/services/src/main/java/org/fao/geonet/api/ApiUtils.java b/services/src/main/java/org/fao/geonet/api/ApiUtils.java
index 8170086c26a..255d2b6e722 100644
--- a/services/src/main/java/org/fao/geonet/api/ApiUtils.java
+++ b/services/src/main/java/org/fao/geonet/api/ApiUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2001-2016 Food and Agriculture Organization of the
+ * Copyright (C) 2001-2021 Food and Agriculture Organization of the
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
* and United Nations Environment Programme (UNEP)
*
@@ -48,6 +48,8 @@
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.util.StringUtils;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.ObjectError;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
@@ -66,6 +68,9 @@
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
+import java.util.Iterator;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
import java.util.Set;
@@ -330,4 +335,47 @@ public static void createFavicon(Image img, Path outFile) throws IOException {
ImageIO.write(bimg, type, out);
}
}
+
+ /**
+ * Process request validation, returning an string with the validation errors.
+ *
+ * @param bindingResult
+ * @param messages
+ */
+ public static String processRequestValidation(BindingResult bindingResult, ResourceBundle messages) {
+ if (bindingResult.hasErrors()) {
+ java.util.List errorList = bindingResult.getAllErrors();
+
+ StringBuilder sb = new StringBuilder();
+ Iterator it = errorList.iterator();
+ while (it.hasNext()) {
+ ObjectError err = it.next();
+ String msg = "";
+ for(int i = 0; i < err.getCodes().length; i++) {
+ try {
+ msg = messages.getString(err.getCodes()[i]);
+
+ if (!StringUtils.isEmpty(msg)) {
+ break;
+ }
+ } catch (MissingResourceException ex) {
+ // Ignore
+ }
+ }
+
+ if (StringUtils.isEmpty(msg)) {
+ msg = err.getDefaultMessage();
+ }
+
+ sb.append(msg);
+ if (it.hasNext()) {
+ sb.append(", ");
+ }
+ }
+
+ return sb.toString();
+ } else {
+ return "";
+ }
+ }
}
diff --git a/services/src/main/java/org/fao/geonet/api/users/RegisterApi.java b/services/src/main/java/org/fao/geonet/api/users/RegisterApi.java
index b9be74e955f..55c8c72fedf 100644
--- a/services/src/main/java/org/fao/geonet/api/users/RegisterApi.java
+++ b/services/src/main/java/org/fao/geonet/api/users/RegisterApi.java
@@ -1,5 +1,5 @@
//=============================================================================
-//=== Copyright (C) 2001-2007 Food and Agriculture Organization of the
+//=== Copyright (C) 2001-2021 Food and Agriculture Organization of the
//=== United Nations (FAO-UN), United Nations World Food Programme (WFP)
//=== and United Nations Environment Programme (UNEP)
//===
@@ -44,11 +44,15 @@
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import javax.servlet.http.HttpServletRequest;
import java.sql.SQLException;
+import java.util.Iterator;
+import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
@@ -78,7 +82,10 @@ public ResponseEntity registerUser(
required = true)
@RequestBody
UserRegisterDto userRegisterDto,
- HttpServletRequest request)
+ @Parameter(hidden = true)
+ BindingResult bindingResult,
+ @Parameter(hidden = true)
+ HttpServletRequest request)
throws Exception {
Locale locale = languageUtils.parseAcceptLanguage(request.getLocales());
@@ -105,6 +112,22 @@ public ResponseEntity registerUser(
}
}
+ // Validate the user registration
+ if (bindingResult.hasErrors()) {
+ List errorList = bindingResult.getAllErrors();
+
+ StringBuilder sb = new StringBuilder();
+ Iterator it = errorList.iterator();
+ while (it.hasNext()) {
+ sb.append(messages.getString(it.next().getDefaultMessage()));
+ if (it.hasNext()) {
+ sb.append(", ");
+ }
+ }
+
+ return new ResponseEntity<>(sb.toString(), HttpStatus.PRECONDITION_FAILED);
+ }
+
final UserRepository userRepository = context.getBean(UserRepository.class);
if (userRepository.findOneByEmail(userRegisterDto.getEmail()) != null) {
return new ResponseEntity<>(String.format(
diff --git a/services/src/main/java/org/fao/geonet/api/users/UsersApi.java b/services/src/main/java/org/fao/geonet/api/users/UsersApi.java
index 79ce2a88cd0..4c47bd45e00 100644
--- a/services/src/main/java/org/fao/geonet/api/users/UsersApi.java
+++ b/services/src/main/java/org/fao/geonet/api/users/UsersApi.java
@@ -33,7 +33,11 @@
import org.fao.geonet.ApplicationContextHolder;
import org.fao.geonet.api.ApiParams;
import org.fao.geonet.api.ApiUtils;
+import org.fao.geonet.api.tools.i18n.LanguageUtils;
+import org.fao.geonet.api.users.model.PasswordResetDto;
import org.fao.geonet.api.users.model.UserDto;
+import org.fao.geonet.api.users.validation.PasswordResetDtoValidator;
+import org.fao.geonet.api.users.validation.UserDtoValidator;
import org.fao.geonet.constants.Params;
import org.fao.geonet.domain.*;
import org.fao.geonet.exceptions.UserNotFoundEx;
@@ -53,6 +57,7 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
+import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import javax.imageio.ImageIO;
@@ -98,6 +103,9 @@ public class UsersApi {
@Autowired
DataManager dataManager;
+ @Autowired
+ LanguageUtils languageUtils;
+
private BufferedImage pixel;
public UsersApi() {
@@ -386,6 +394,8 @@ public ResponseEntity createUser(
)
@RequestBody
UserDto userDto,
+ @Parameter(hidden = true)
+ BindingResult bindingResult,
@Parameter(hidden = true)
ServletRequest request,
@Parameter(hidden = true)
@@ -395,6 +405,9 @@ public ResponseEntity createUser(
UserSession session = ApiUtils.getUserSession(httpSession);
Profile myProfile = session.getProfile();
+ Locale locale = languageUtils.parseAcceptLanguage(request.getLocales());
+ ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale);
+
if (profile == Profile.Administrator) {
checkIfAtLeastOneAdminIsEnabled(userDto, userRepository);
}
@@ -407,10 +420,12 @@ public ResponseEntity createUser(
+ " max profile permitted is: " + myProfile);
}
- if (StringUtils.isEmpty(userDto.getUsername())) {
- throw new IllegalArgumentException(Params.USERNAME
- + " is a required parameter for "
- + Params.Operation.NEWUSER + " " + "operation");
+ // Validate userDto data
+ UserDtoValidator userValidator = new UserDtoValidator();
+ userValidator.validate(userDto, bindingResult);
+ String errorMessage = ApiUtils.processRequestValidation(bindingResult, messages);
+ if (StringUtils.isNotEmpty(errorMessage)) {
+ throw new IllegalArgumentException(errorMessage);
}
List existingUsers = userRepository.findByUsernameIgnoreCase(userDto.getUsername());
@@ -554,26 +569,26 @@ public ResponseEntity resetUserPassword(
)
@PathVariable
Integer userIdentifier,
- @Parameter(
- description = "Password to change (old)."
- )
- @RequestParam(value = Params.PASSWORD + "Old") String passwordOld,
- @Parameter(
- description = "Password to change."
- )
- @RequestParam(value = Params.PASSWORD) String password,
- @Parameter(
- description = "Password to change (repeat)."
- )
- @RequestParam(value = Params.PASSWORD + "2") String password2,
+ @RequestBody
+ PasswordResetDto passwordResetDto,
+ @Parameter(hidden = true)
+ BindingResult bindingResult,
@Parameter(hidden = true)
ServletRequest request,
@Parameter(hidden = true)
HttpSession httpSession
) throws Exception {
- if (!password.equals(password2)) {
- throw new IllegalArgumentException("Passwords should be equal");
+ Locale locale = languageUtils.parseAcceptLanguage(request.getLocales());
+ ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale);
+
+
+ // Validate passwordResetDto data
+ PasswordResetDtoValidator passwordResetValidator = new PasswordResetDtoValidator();
+ passwordResetValidator.validate(passwordResetDto, bindingResult);
+ String errorMessage = ApiUtils.processRequestValidation(bindingResult, messages);
+ if (StringUtils.isNotEmpty(errorMessage)) {
+ throw new IllegalArgumentException(errorMessage);
}
UserSession session = ApiUtils.getUserSession(httpSession);
@@ -591,12 +606,12 @@ public ResponseEntity resetUserPassword(
PasswordEncoder encoder = PasswordUtil.encoder(ApplicationContextHolder.get());
- if (!encoder.matches(passwordOld, user.get().getPassword())) {
+ if (!encoder.matches(passwordResetDto.getPasswordOld(), user.get().getPassword())) {
throw new IllegalArgumentException("The old password is not valid");
}
String passwordHash = PasswordUtil.encoder(ApplicationContextHolder.get()).encode(
- password);
+ passwordResetDto.getPassword());
user.get().getSecurity().setPassword(passwordHash);
user.get().getSecurity().getSecurityNotifications().remove(UserSecurityNotification.UPDATE_HASH_REQUIRED);
userRepository.save(user.get());
diff --git a/services/src/main/java/org/fao/geonet/api/users/model/PasswordResetDto.java b/services/src/main/java/org/fao/geonet/api/users/model/PasswordResetDto.java
new file mode 100644
index 00000000000..7b1bce02edb
--- /dev/null
+++ b/services/src/main/java/org/fao/geonet/api/users/model/PasswordResetDto.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2001-2021 Food and Agriculture Organization of the
+ * United Nations (FAO-UN), United Nations World Food Programme (WFP)
+ * and United Nations Environment Programme (UNEP)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
+ * Rome - Italy. email: geonetwork@osgeo.org
+ */
+
+package org.fao.geonet.api.users.model;
+
+import java.util.Objects;
+
+/**
+ * DTO class for password reset information.
+ *
+ */
+public class PasswordResetDto {
+ private String passwordOld;
+ private String password;
+ private String password2;
+
+ public String getPasswordOld() {
+ return passwordOld;
+ }
+
+ public void setPasswordOld(String passwordOld) {
+ this.passwordOld = passwordOld;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getPassword2() {
+ return password2;
+ }
+
+ public void setPassword2(String password2) {
+ this.password2 = password2;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PasswordResetDto that = (PasswordResetDto) o;
+ return Objects.equals(passwordOld, that.passwordOld) &&
+ Objects.equals(password, that.password) &&
+ Objects.equals(password2, that.password2);
+ }
+
+ @Override
+ public int hashCode() {
+
+ return Objects.hash(passwordOld, password, password2);
+ }
+}
diff --git a/services/src/main/java/org/fao/geonet/api/users/validation/PasswordResetDtoValidator.java b/services/src/main/java/org/fao/geonet/api/users/validation/PasswordResetDtoValidator.java
new file mode 100644
index 00000000000..5e9e9c31dfa
--- /dev/null
+++ b/services/src/main/java/org/fao/geonet/api/users/validation/PasswordResetDtoValidator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2001-2021 Food and Agriculture Organization of the
+ * United Nations (FAO-UN), United Nations World Food Programme (WFP)
+ * and United Nations Environment Programme (UNEP)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
+ * Rome - Italy. email: geonetwork@osgeo.org
+ */
+
+package org.fao.geonet.api.users.validation;
+
+import org.fao.geonet.api.users.model.PasswordResetDto;
+import org.springframework.validation.Errors;
+import org.springframework.validation.Validator;
+
+/**
+ * Validator for PasswordResetDto.
+ *
+ */
+public class PasswordResetDtoValidator implements Validator {
+
+ @Override
+ public boolean supports(Class> clazz) {
+ return PasswordResetDto.class.isAssignableFrom(clazz);
+ }
+
+ @Override
+ public void validate(Object target, Errors errors) {
+ PasswordResetDto passwordReset = (PasswordResetDto) target;
+
+ PasswordValidationUtils.rejectIfInvalid(errors, passwordReset.getPassword());
+
+ if (!passwordReset.getPassword().equals(passwordReset.getPassword2())) {
+
+ errors.rejectValue("password", "match",
+ new Object[]{},
+ "Passwords should be equal");
+ }
+ }
+}
diff --git a/services/src/main/java/org/fao/geonet/api/users/validation/PasswordUpdateParameterValidator.java b/services/src/main/java/org/fao/geonet/api/users/validation/PasswordUpdateParameterValidator.java
new file mode 100644
index 00000000000..607f544092f
--- /dev/null
+++ b/services/src/main/java/org/fao/geonet/api/users/validation/PasswordUpdateParameterValidator.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2001-2021 Food and Agriculture Organization of the
+ * United Nations (FAO-UN), United Nations World Food Programme (WFP)
+ * and United Nations Environment Programme (UNEP)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
+ * Rome - Italy. email: geonetwork@osgeo.org
+ */
+
+package org.fao.geonet.api.users.validation;
+
+import org.fao.geonet.api.users.PasswordUpdateParameter;
+import org.springframework.validation.Errors;
+import org.springframework.validation.Validator;
+
+
+/**
+ * Validator for PasswordUpdateParameter.
+ *
+ */
+public class PasswordUpdateParameterValidator implements Validator {
+
+ @Override
+ public boolean supports(Class> clazz) {
+ return PasswordUpdateParameter.class.isAssignableFrom(clazz);
+ }
+
+ @Override
+ public void validate(Object target, Errors errors) {
+ PasswordUpdateParameter passwordUpdate = (PasswordUpdateParameter) target;
+
+ PasswordValidationUtils.rejectIfInvalid(errors, passwordUpdate.getPassword());
+ }
+}
diff --git a/services/src/main/java/org/fao/geonet/api/users/validation/PasswordValidationUtils.java b/services/src/main/java/org/fao/geonet/api/users/validation/PasswordValidationUtils.java
new file mode 100644
index 00000000000..2657308c88d
--- /dev/null
+++ b/services/src/main/java/org/fao/geonet/api/users/validation/PasswordValidationUtils.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2001-2021 Food and Agriculture Organization of the
+ * United Nations (FAO-UN), United Nations World Food Programme (WFP)
+ * and United Nations Environment Programme (UNEP)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
+ * Rome - Italy. email: geonetwork@osgeo.org
+ */
+
+package org.fao.geonet.api.users.validation;
+
+import org.apache.commons.lang.StringUtils;
+import org.fao.geonet.ApplicationContextHolder;
+import org.fao.geonet.kernel.setting.SettingManager;
+import org.fao.geonet.kernel.setting.Settings;
+import org.springframework.validation.Errors;
+
+import java.util.regex.Pattern;
+
+/**
+ * Utility class for password validation.
+ *
+ */
+public class PasswordValidationUtils {
+ public static void rejectIfInvalid(Errors errors, String password) {
+
+ SettingManager sm =ApplicationContextHolder.get().getBean(SettingManager.class);
+ Integer minLength = sm.getValueAsInt(Settings.SYSTEM_SECURITY_PASSWORDENFORCEMENT_MINLENGH);
+ Integer maxLength = sm.getValueAsInt(Settings.SYSTEM_SECURITY_PASSWORDENFORCEMENT_MAXLENGH);
+ boolean usePattern = sm.getValueAsBool(Settings.SYSTEM_SECURITY_PASSWORDENFORCEMENT_USEPATTERN);
+ String pattern = sm.getValue(Settings.SYSTEM_SECURITY_PASSWORDENFORCEMENT_PATTERN);
+
+ if (StringUtils.isEmpty(password) ||
+ password.trim().length() < minLength) {
+ errors.rejectValue("password", "field.length",
+ new Object[]{Integer.valueOf(minLength), Integer.valueOf(maxLength)},
+ "Password size should be between " + minLength + " and " + maxLength + " characters");
+ }
+
+ if (StringUtils.isNotEmpty(password) &&
+ password.trim().length() > maxLength) {
+ errors.rejectValue("password", "field.length",
+ new Object[]{Integer.valueOf(minLength), Integer.valueOf(maxLength)},
+ "Password size should be between " + minLength + " and " + maxLength + " characters");
+ }
+
+ if (usePattern && !Pattern.matches(pattern, password)) {
+ errors.rejectValue("password", "field.invalid",
+ new Object[]{Integer.valueOf(maxLength)},
+ "Password must contain at least 1 uppercase, 1 lowercase, 1 number and 1 symbol. Symbols include: `~!@#$%^&*()-_=+[]{}\\\\|;:'\",.<>/?');");
+ }
+ }
+}
diff --git a/services/src/main/java/org/fao/geonet/api/users/validation/UserDtoValidator.java b/services/src/main/java/org/fao/geonet/api/users/validation/UserDtoValidator.java
new file mode 100644
index 00000000000..509c1effff5
--- /dev/null
+++ b/services/src/main/java/org/fao/geonet/api/users/validation/UserDtoValidator.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2001-2021 Food and Agriculture Organization of the
+ * United Nations (FAO-UN), United Nations World Food Programme (WFP)
+ * and United Nations Environment Programme (UNEP)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ *
+ * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
+ * Rome - Italy. email: geonetwork@osgeo.org
+ */
+
+package org.fao.geonet.api.users.validation;
+
+import org.fao.geonet.ApplicationContextHolder;
+import org.fao.geonet.api.users.model.UserDto;
+import org.fao.geonet.constants.Params;
+import org.fao.geonet.domain.User;
+import org.fao.geonet.repository.UserRepository;
+import org.springframework.validation.Errors;
+import org.springframework.validation.ValidationUtils;
+import org.springframework.validation.Validator;
+
+import java.util.List;
+
+/**
+ * Validator for UserDto.
+ *
+ */
+public class UserDtoValidator implements Validator {
+
+ @Override
+ public boolean supports(Class> clazz) {
+ return UserDto.class.isAssignableFrom(clazz);
+ }
+
+ @Override
+ public void validate(Object target, Errors errors) {
+ UserDto user = (UserDto) target;
+
+ ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username", "field.required", Params.USERNAME
+ + " is a required parameter for "
+ + Params.Operation.NEWUSER + " " + "operation");
+
+ UserRepository userRepository = ApplicationContextHolder.get().getBean(UserRepository.class);
+
+ List existingUsers = userRepository.findByUsernameIgnoreCase(user.getUsername());
+ if (!existingUsers.isEmpty()) {
+ errors.rejectValue("username", "duplicated",
+ new Object[]{user.getUsername()},
+ "Users with username "
+ + user.getUsername() + " ignore case already exists");
+ }
+
+ PasswordValidationUtils.rejectIfInvalid(errors, user.getPassword());
+ }
+}
diff --git a/services/src/test/java/org/fao/geonet/api/users/UsersApiTest.java b/services/src/test/java/org/fao/geonet/api/users/UsersApiTest.java
index 0f166d58e16..aec1802a7d2 100644
--- a/services/src/test/java/org/fao/geonet/api/users/UsersApiTest.java
+++ b/services/src/test/java/org/fao/geonet/api/users/UsersApiTest.java
@@ -26,6 +26,7 @@
import com.google.gson.Gson;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
+import org.fao.geonet.api.users.model.PasswordResetDto;
import org.fao.geonet.api.users.model.UserDto;
import org.fao.geonet.domain.Group;
import org.fao.geonet.domain.Profile;
@@ -225,7 +226,7 @@ public void createUser() throws Exception {
user.setProfile(Profile.Editor.name());
user.setGroupsEditor(Collections.singletonList("2"));
user.setEmail(Collections.singletonList("mail@test.com"));
- user.setPassword("password");
+ user.setPassword("Password7$");
user.setEnabled(true);
Gson gson = new Gson();
@@ -253,7 +254,7 @@ public void createUserMissingUsername() throws Exception {
user.setProfile(Profile.Editor.name());
user.setGroupsEditor(Collections.singletonList("2"));
user.setEmail(Collections.singletonList("mail@test.com"));
- user.setPassword("password");
+ user.setPassword("Password1$");
user.setEnabled(true);
Gson gson = new Gson();
@@ -281,7 +282,7 @@ public void createDuplicatedUsername() throws Exception {
user.setProfile(Profile.Editor.name());
user.setGroupsEditor(Collections.singletonList("2"));
user.setEmail(Collections.singletonList("mail@test.com"));
- user.setPassword("password");
+ user.setPassword("Password1$");
user.setEnabled(true);
Gson gson = new Gson();
@@ -311,7 +312,7 @@ public void createDuplicatedUsernameIgnoreCase() throws Exception {
user.setProfile(Profile.Editor.name());
user.setGroupsEditor(Collections.singletonList("2"));
user.setEmail(Collections.singletonList("mail@test.com"));
- user.setPassword("password");
+ user.setPassword("Password1$");
user.setEnabled(true);
Gson gson = new Gson();
@@ -342,10 +343,16 @@ public void resetPassword() throws Exception {
this.mockHttpSession = loginAsAdmin();
+ Gson gson = new Gson();
+ PasswordResetDto passwordReset = new PasswordResetDto();
+ passwordReset.setPasswordOld("testuser-editor-password");
+ passwordReset.setPassword("NewPassword1$");
+ passwordReset.setPassword2("NewPassword1$");
+
+ String json = gson.toJson(passwordReset);
+
this.mockMvc.perform(post("/srv/api/users/" + user.getId() + "/actions/forget-password")
- .param("passwordOld", "testuser-editor-password")
- .param("password", "newpassword")
- .param("password2", "newpassword")
+ .content(json)
.contentType(API_JSON_EXPECTED_ENCODING)
.session(this.mockHttpSession)
.accept(MediaType.parseMediaType("application/json")))
@@ -364,11 +371,17 @@ public void resetPasswordToNotAllowedUser() throws Exception {
Assert.assertTrue(user.getProfile().equals(Profile.Editor));
this.mockHttpSession = loginAs(user);
+ Gson gson = new Gson();
+ PasswordResetDto passwordReset = new PasswordResetDto();
+ passwordReset.setPasswordOld("testuser-editor-password");
+ passwordReset.setPassword("NewPassword1$");
+ passwordReset.setPassword2("NewPassword1$");
+
+ String json = gson.toJson(passwordReset);
+
// Try to update the password of admin user from a user with Editor profile
this.mockMvc.perform(post("/srv/api/users/" + admin.getId() + "/actions/forget-password")
- .param("passwordOld", "testuser-editor-password")
- .param("password", "newpassword")
- .param("password2", "newpassword")
+ .content(json)
.contentType(API_JSON_EXPECTED_ENCODING)
.session(this.mockHttpSession)
.accept(MediaType.parseMediaType("application/json")))
@@ -385,12 +398,18 @@ public void resetPasswordNotEqual() throws Exception {
this.mockHttpSession = loginAsAdmin();
+ Gson gson = new Gson();
+ PasswordResetDto passwordReset = new PasswordResetDto();
+ passwordReset.setPasswordOld("testuser-editor-password");
+ passwordReset.setPassword("NewPassword1$");
+ passwordReset.setPassword2("NewPassword2%");
+
+ String json = gson.toJson(passwordReset);
+
// Check 400 is returned and a message indicating that passwords should be equal
this.mockMvc.perform(post("/srv/api/users/" + user.getId() + "/actions/forget-password")
.contentType(API_JSON_EXPECTED_ENCODING)
- .param("passwordOld", "testuser-editor-password")
- .param("password", "newpassword")
- .param("password2", "newpassword2")
+ .content(json)
.session(this.mockHttpSession)
.accept(MediaType.parseMediaType("application/json")))
.andExpect(jsonPath("$.description", is("Passwords should be equal")))
@@ -406,12 +425,18 @@ public void resetPasswordWrongOldPassword() throws Exception {
this.mockHttpSession = loginAsAdmin();
+ Gson gson = new Gson();
+ PasswordResetDto passwordReset = new PasswordResetDto();
+ passwordReset.setPasswordOld("testuser-editor-password-wrong");
+ passwordReset.setPassword("NewPassword1$");
+ passwordReset.setPassword2("NewPassword1$");
+
+ String json = gson.toJson(passwordReset);
+
// Check 400 is returned and a message indicating that passwords should be equal
this.mockMvc.perform(post("/srv/api/users/" + user.getId() + "/actions/forget-password")
.contentType(API_JSON_EXPECTED_ENCODING)
- .param("passwordOld", "testuser-editor-password-wrong")
- .param("password", "newpassword")
- .param("password2", "newpassword")
+ .content(json)
.session(this.mockHttpSession)
.accept(MediaType.parseMediaType("application/json")))
.andExpect(jsonPath("$.description", is("The old password is not valid")))
@@ -426,12 +451,18 @@ public void resetPasswordNotExistingUser() throws Exception {
this.mockHttpSession = loginAsAdmin();
+ Gson gson = new Gson();
+ PasswordResetDto passwordReset = new PasswordResetDto();
+ passwordReset.setPasswordOld("oldpassword");
+ passwordReset.setPassword("NewPassword1$");
+ passwordReset.setPassword2("NewPassword1$");
+
+ String json = gson.toJson(passwordReset);
+
// Check 404 is returned
this.mockMvc.perform(post("/srv/api/users/" + userId + "/actions/forget-password")
.contentType(API_JSON_EXPECTED_ENCODING)
- .param("passwordOld", "oldpassword")
- .param("password", "newpassword")
- .param("password2", "newpassword")
+ .content(json)
.session(this.mockHttpSession)
.accept(MediaType.parseMediaType("application/json")))
.andExpect(jsonPath("$.description", is("User not found")))
@@ -447,11 +478,17 @@ public void resetPasswordSameUser() throws Exception {
Assert.assertTrue(user.getProfile().equals(Profile.Editor));
this.mockHttpSession = loginAs(user);
+ Gson gson = new Gson();
+ PasswordResetDto passwordReset = new PasswordResetDto();
+ passwordReset.setPasswordOld("testuser-editor-password");
+ passwordReset.setPassword("NewPassword1$");
+ passwordReset.setPassword2("NewPassword1$");
+
+ String json = gson.toJson(passwordReset);
+
this.mockMvc.perform(post("/srv/api/users/" + user.getId() + "/actions/forget-password")
.contentType(API_JSON_EXPECTED_ENCODING)
- .param("passwordOld", "testuser-editor-password")
- .param("password", "newpassword")
- .param("password2", "newpassword")
+ .content(json)
.session(this.mockHttpSession)
.accept(MediaType.parseMediaType("application/json")))
.andExpect(status().is(204));
diff --git a/web-ui/src/main/resources/catalog/js/LoginController.js b/web-ui/src/main/resources/catalog/js/LoginController.js
index 2c02272b115..373fd4bfc89 100644
--- a/web-ui/src/main/resources/catalog/js/LoginController.js
+++ b/web-ui/src/main/resources/catalog/js/LoginController.js
@@ -68,6 +68,13 @@
$scope.isDisableLoginForm = gnGlobalSettings.isDisableLoginForm;
$scope.isShowLoginAsLink = gnGlobalSettings.isShowLoginAsLink;
+ $scope.passwordMinLength =
+ Math.min(gnConfig['system.security.passwordEnforcement.minLength'], 6);
+ $scope.passwordMaxLength =
+ Math.max(gnConfig['system.security.passwordEnforcement.maxLength'], 6);
+ $scope.passwordPattern =
+ gnConfig['system.security.passwordEnforcement.pattern'];
+
function initForm() {
if ($window.location.pathname.indexOf('new.password') !== -1) {
// Retrieve username from URL parameter
diff --git a/web-ui/src/main/resources/catalog/js/admin/SystemSettingsController.js b/web-ui/src/main/resources/catalog/js/admin/SystemSettingsController.js
index f59a047acbc..c60c37b4f1c 100644
--- a/web-ui/src/main/resources/catalog/js/admin/SystemSettingsController.js
+++ b/web-ui/src/main/resources/catalog/js/admin/SystemSettingsController.js
@@ -50,7 +50,8 @@
module.filter('hideGeoNetworkInternalSettings', function() {
return function(input) {
var filtered = [];
- var internal = ['system/userFeedback/lastNotificationDate'];
+ var internal = ['system/userFeedback/lastNotificationDate',
+ 'system/security/passwordEnforcement/pattern'];
angular.forEach(input, function(el) {
if (internal.indexOf(el.name) === -1) {
filtered.push(el);
diff --git a/web-ui/src/main/resources/catalog/js/admin/UserGroupController.js b/web-ui/src/main/resources/catalog/js/admin/UserGroupController.js
index 8f175d9bbb2..1ff095e0792 100644
--- a/web-ui/src/main/resources/catalog/js/admin/UserGroupController.js
+++ b/web-ui/src/main/resources/catalog/js/admin/UserGroupController.js
@@ -41,9 +41,9 @@
*/
module.controller('GnUserGroupController', [
'$scope', '$routeParams', '$http', '$rootScope',
- '$translate', '$timeout',
+ '$translate', '$timeout', 'gnConfig',
function($scope, $routeParams, $http, $rootScope,
- $translate, $timeout) {
+ $translate, $timeout, gnConfig) {
$scope.searchObj = {
params: {
@@ -102,6 +102,16 @@
$scope.isLoadingUsers = false;
$scope.isLoadingGroups = false;
+ $scope.passwordMinLength =
+ Math.min(gnConfig['system.security.passwordEnforcement.minLength'], 6);
+ $scope.passwordMaxLength =
+ Math.max(gnConfig['system.security.passwordEnforcement.maxLength'], 6);
+ $scope.usePattern = gnConfig['system.security.passwordEnforcement.usePattern'];
+
+ if ($scope.usePattern) {
+ $scope.passwordPattern = new RegExp(
+ gnConfig['system.security.passwordEnforcement.pattern']);
+ }
// This is to force IE11 NOT to cache json requests
if (!$http.defaults.headers.get) {
@@ -307,10 +317,7 @@
$http.post('../api/users/' + $scope.userSelected.id +
'/actions/forget-password',
- $.param(params),
- {
- headers: {'Content-Type': 'application/x-www-form-urlencoded'}
- })
+ params)
.success(function(data) {
$scope.resetPassword1 = null;
$scope.resetPassword2 = null;
@@ -534,12 +541,20 @@
data)
.then(
function(r) {
- $scope.unselectUser();
- loadUsers();
- $rootScope.$broadcast('StatusUpdated', {
- msg: $translate.instant('userUpdated'),
- timeout: 2,
- type: 'success'});
+ if (r.status === 204) {
+ $scope.unselectUser();
+ loadUsers();
+ $rootScope.$broadcast('StatusUpdated', {
+ msg: $translate.instant('userUpdated'),
+ timeout: 2,
+ type: 'success'});
+ } else {
+ $rootScope.$broadcast('StatusUpdated', {
+ title: $translate.instant('userUpdateError'),
+ error: r.data,
+ timeout: 0,
+ type: 'danger'});
+ }
},
function(r) {
$rootScope.$broadcast('StatusUpdated', {
diff --git a/web-ui/src/main/resources/catalog/locales/en-admin.json b/web-ui/src/main/resources/catalog/locales/en-admin.json
index ee49b113e4f..d61263f5eda 100644
--- a/web-ui/src/main/resources/catalog/locales/en-admin.json
+++ b/web-ui/src/main/resources/catalog/locales/en-admin.json
@@ -893,7 +893,12 @@
"system/searchStats": "Search statistics",
"system/searchStats/enable": "Enable",
"system/searchStats/enable-help": "If set, statistics on searches will be collected and stored in the database.",
- "system/selectionmanager": "Metadata Search Results",
+ "system/security": "Security",
+ "system/security/passwordEnforcement/maxLength": "Password max. length",
+ "system/security/passwordEnforcement/minLength": "Password min. length",
+ "system/security/passwordEnforcement/usePattern": "Password restrictions",
+ "system/security/passwordEnforcement/usePattern-help": "Require that the password must contain at least 1 uppercase, 1 lowercase, 1 number and 1 symbol",
+ "system/selectionmanager": "Metadata Search Results",
"system/selectionmanager/maxrecords": "Maximum selected records",
"system/server": "Catalog server",
"system/server/host": "Host",
@@ -1345,7 +1350,7 @@
"ui-definition": "Definition",
"ui-label": "Label",
"ui-code": "Code",
- "ui-getProj": "Retrieve projection settings from {{value}}",
+ "ui-getProj": "Retrieve projection settings from {{value}}",
"ui-extent-help": "This is the default extent of the map for this projection in projected units (e.g. metre). This is a mandatory field if the projection is not already defined in GeoNetwork.",
"ui-worldExtent-help": "This is the maximum world extent of the map for this projection in degrees (longitude-latitude). This is a mandatory field if the projection is not already defined in GeoNetwork.",
"ui-label-help": "Label to show on the user interface to identify this projection. This is a mandatory field.",
diff --git a/web-ui/src/main/resources/catalog/locales/en-core.json b/web-ui/src/main/resources/catalog/locales/en-core.json
index d5a5c7348fb..ae2fdeaebdf 100644
--- a/web-ui/src/main/resources/catalog/locales/en-core.json
+++ b/web-ui/src/main/resources/catalog/locales/en-core.json
@@ -214,8 +214,10 @@
"privilegesUpdatedError": "Error occurred while updating privileges.",
"publisher": "Publisher",
"password": "Password",
- "passwordMinlength": "Password must contain at least 6 characters!",
+ "passwordMinlength": "Password must contain at least {{length}} characters!",
"passwordNotMatching": "The password does not match!",
+ "passwordMaxlength": "Password must contain at most {{length}} characters!",
+ "passwordPattern": "Password must contain at least 1 uppercase, 1 lowercase, 1 number and 1 symbol. Symbols include: `~!@#$%^&*()-_=+[]{}\\|;:'\",.<>/?');",
"passwordOld": "Old password",
"passwordRepeat": "Repeat password",
"groupNameMaxlength": "Group name can not exceed 32 characters!",
diff --git a/web-ui/src/main/resources/catalog/templates/admin/usergroup/users.html b/web-ui/src/main/resources/catalog/templates/admin/usergroup/users.html
index 34cd3b79c0d..f4a4fd39505 100644
--- a/web-ui/src/main/resources/catalog/templates/admin/usergroup/users.html
+++ b/web-ui/src/main/resources/catalog/templates/admin/usergroup/users.html
@@ -113,19 +113,28 @@