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 d4482cdcd7a..6b30d61e810 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2023 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -83,6 +83,7 @@ public class Settings { public static final String SYSTEM_CSW_CAPABILITY_RECORD_UUID = "system/csw/capabilityRecordUuid"; public static final String SYSTEM_CSW_METADATA_PUBLIC = "system/csw/metadataPublic"; public static final String SYSTEM_USERSELFREGISTRATION_ENABLE = "system/userSelfRegistration/enable"; + public static final String SYSTEM_USERSELFREGISTRATION_EMAIL_DOMAINS = "system/userSelfRegistration/domainsAllowed"; public static final String SYSTEM_USERSELFREGISTRATION_RECAPTCHA_ENABLE = "system/userSelfRegistration/recaptcha/enable"; public static final String SYSTEM_USERSELFREGISTRATION_RECAPTCHA_PUBLICKEY = "system/userSelfRegistration/recaptcha/publickey"; public static final String SYSTEM_USERSELFREGISTRATION_RECAPTCHA_SECRETKEY = "system/userSelfRegistration/recaptcha/secretkey"; diff --git a/core/src/test/resources/org/fao/geonet/api/Messages.properties b/core/src/test/resources/org/fao/geonet/api/Messages.properties index 1cde2820cbb..24310e00c25 100644 --- a/core/src/test/resources/org/fao/geonet/api/Messages.properties +++ b/core/src/test/resources/org/fao/geonet/api/Messages.properties @@ -159,12 +159,16 @@ user_watchlist_message=The following records have been updated:\n\ \n\ self_registration_disabled=User self-registration is disabled +self_registration_no_valid_mail=The email address is not allowed 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: `~!@#$%^&*()-_=+[]{}\\|;:'",.<>/?'); +field.required.name=Name is required +field.required.email=Email address is required +field.notvalid.email=Email address is not valid api.exception.forbidden=Access denied api.exception.forbidden.description=Access is denied. To access, try again with a user containing more privileges. api.exception.resourceNotFound=Resource not found diff --git a/core/src/test/resources/org/fao/geonet/api/Messages_fre.properties b/core/src/test/resources/org/fao/geonet/api/Messages_fre.properties index 55a00915b44..6fe2d2fc083 100644 --- a/core/src/test/resources/org/fao/geonet/api/Messages_fre.properties +++ b/core/src/test/resources/org/fao/geonet/api/Messages_fre.properties @@ -145,10 +145,14 @@ user_watchlist_message=Les fiches suivantes ont \u00E9t\u00E9 mises \u00e0 jour \n\ self_registration_disabled=La cr\u00E9ation de compte par les utilisateurs est d\u00E9sactiv\u00E9e +self_registration_no_valid_mail=L''adresse email n''est pas autoris\u00E9e recaptcha_not_valid=Recaptcha invalide 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=Le nom d''utilisateur est requis +field.required.name=Nom est obligatoire +field.required.email=L''adresse mail est obligatoire +field.notvalid.email=L''adresse mail n''est pas valide password.field.length=Le mot de passe doit contenir entre {min} et {max} caract\u00E8res password.field.invalid=Le mot de passe doit contenir a minima 1 lettre majuscule, 1 minuscule, 1 chiffre et 1 symbole (ie. `~!@#$%^&*()-_=+[]{}\\|;:'",.<>/?')); api.exception.forbidden=L''acc\u00E8s est refus\u00E9 diff --git a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md index 9d923dd46d8..8f9f567d1e4 100644 --- a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md +++ b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md @@ -91,6 +91,9 @@ See [Configuring Shibboleth](../managing-users-and-groups/authentication-mode.md Enable the self registration form. See [User Self-Registration](../managing-users-and-groups/user-self-registration.md). +You can configure optionally re-Captcha, to protect you and your users from spam and abuse. And a list of email domains (separated by commas) +that can request an account. If not configured any email address is allowed. + ## system/userFeedback !!! warning "Deprecated" 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 848f6e53e5d..d8974a61687 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 @@ -30,6 +30,7 @@ import org.fao.geonet.api.tools.i18n.LanguageUtils; import org.fao.geonet.api.users.model.UserRegisterDto; import org.fao.geonet.api.users.recaptcha.RecaptchaChecker; +import org.fao.geonet.api.users.validation.UserRegisterDtoValidator; import org.fao.geonet.domain.*; import org.fao.geonet.kernel.security.SecurityProviderConfiguration; import org.fao.geonet.kernel.setting.SettingManager; @@ -46,7 +47,6 @@ import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -68,6 +68,9 @@ public class RegisterApi { @Autowired(required=false) SecurityProviderConfiguration securityProviderConfiguration; + @Autowired + UserRepository userRepository; + @Autowired GroupRepository groupRepository; @@ -123,39 +126,30 @@ 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); + // Validate userDto data + UserRegisterDtoValidator userRegisterDtoValidator = new UserRegisterDtoValidator(); + userRegisterDtoValidator.validate(userRegisterDto, bindingResult); + String errorMessage = ApiUtils.processRequestValidation(bindingResult, messages); + if (org.apache.commons.lang.StringUtils.isNotEmpty(errorMessage)) { + return new ResponseEntity<>(errorMessage, HttpStatus.PRECONDITION_FAILED); } - final UserRepository userRepository = context.getBean(UserRepository.class); - if (userRepository.findOneByEmail(userRegisterDto.getEmail()) != null) { - return new ResponseEntity<>(String.format( - messages.getString("user_with_that_email_found"), - userRegisterDto.getEmail() - ), HttpStatus.PRECONDITION_FAILED); - } - if (!userRepository.findByUsernameIgnoreCase(userRegisterDto.getEmail()).isEmpty()) { - // username is ignored and the email is used as username in selfregister - return new ResponseEntity<>(String.format( - messages.getString("user_with_that_username_found"), - userRegisterDto.getEmail() - ), HttpStatus.PRECONDITION_FAILED); + String emailDomainsAllowed = settingManager.getValue(Settings.SYSTEM_USERSELFREGISTRATION_EMAIL_DOMAINS); + if (StringUtils.hasLength(emailDomainsAllowed)) { + List emailDomainsAllowedList = Arrays.asList(emailDomainsAllowed.split(",")); + + String userEmailDomain = userRegisterDto.getEmail().split("@")[1]; + + if (!emailDomainsAllowedList.contains(userEmailDomain)) { + return new ResponseEntity<>(String.format( + messages.getString("self_registration_no_valid_mail") + ), HttpStatus.PRECONDITION_FAILED); + } } User user = new User(); + user.setName(userRegisterDto.getName()); user.setSurname(userRegisterDto.getSurname()); user.setOrganisation(userRegisterDto.getOrganisation()); diff --git a/services/src/main/java/org/fao/geonet/api/users/validation/UserRegisterDtoValidator.java b/services/src/main/java/org/fao/geonet/api/users/validation/UserRegisterDtoValidator.java new file mode 100644 index 00000000000..2ba53946b18 --- /dev/null +++ b/services/src/main/java/org/fao/geonet/api/users/validation/UserRegisterDtoValidator.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2001-2024 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.UserRegisterDto; +import org.fao.geonet.constants.Params; +import org.fao.geonet.repository.UserRepository; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +/** + * Validator for UserRegisterDto. + * + */ +public class UserRegisterDtoValidator implements Validator { + private static final String OWASP_EMAIL_REGEX = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; + private static final java.util.regex.Pattern OWASP_EMAIL_PATTERN = java.util.regex.Pattern.compile(OWASP_EMAIL_REGEX); + + @Override + public boolean supports(Class clazz) { + return UserRegisterDto.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + UserRegisterDto userRegisterDto = (UserRegisterDto) target; + + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "field.required", Params.NAME + + " is required"); + + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "field.required", Params.EMAIL + + " is required"); + + if (StringUtils.hasLength(userRegisterDto.getEmail()) && !OWASP_EMAIL_PATTERN.matcher(userRegisterDto.getEmail()).matches()) { + errors.rejectValue("email", "field.notvalid", "Email address is not valid"); + } + + UserRepository userRepository = ApplicationContextHolder.get().getBean(UserRepository.class); + if (userRepository.findOneByEmail(userRegisterDto.getEmail()) != null) { + errors.rejectValue("", "user_with_that_email_found", "A user with this email or username already exists."); + } + + if (userRepository.findByUsernameIgnoreCase(userRegisterDto.getEmail()).size() != 0) { + errors.rejectValue("", "user_with_that_username_found", "A user with this email or username already exists."); + } + } +} 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 111798db13d..ec570ee7c95 100644 --- a/web-ui/src/main/resources/catalog/locales/en-admin.json +++ b/web-ui/src/main/resources/catalog/locales/en-admin.json @@ -843,6 +843,8 @@ "system/userSelfRegistration": "User self-registration", "system/userSelfRegistration/enable": "Enable self-registration", "system/userSelfRegistration/enable-help": "When enabled, make sure a mail server is also configured.", + "system/userSelfRegistration/domainsAllowed": "Email domains allowed", + "system/userSelfRegistration/domainsAllowed-help": "Comma separated list of email domains that can request an account. If not configured, any email address is allowed.", "system/userSelfRegistration/recaptcha/enable": "Enable re-captcha", "system/userSelfRegistration/recaptcha/enable-help": "Enabling re-captcha will protect you and your users from spam and abuse. This is highly recommended when you enable feedback or self-registration. Create your re-captcha key on https://www.google.com/recaptcha/", "system/userSelfRegistration/recaptcha/publickey": "Re-captcha public key", diff --git a/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages.properties b/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages.properties index f1ce00195c7..1b25459a27f 100644 --- a/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages.properties +++ b/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages.properties @@ -159,12 +159,16 @@ user_watchlist_message=The following records have been updated:\n\ \n\ self_registration_disabled=User self-registration is disabled +self_registration_no_valid_mail=The email address is not allowed 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: `~!@#$%^&*()-_=+[]{}\\|;:'",.<>/?'); +field.required.name=Name is required +field.required.email=Email address is required +field.notvalid.email=Email address is not valid api.exception.forbidden=Access denied api.exception.forbidden.description=Access is denied. To access, try again with a user containing more privileges. api.exception.resourceNotFound=Resource not found diff --git a/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages_fre.properties b/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages_fre.properties index bdf83dc0908..cdcb274f49b 100644 --- a/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages_fre.properties +++ b/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages_fre.properties @@ -145,12 +145,16 @@ user_watchlist_message=Les fiches suivantes ont \u00E9t\u00E9 mises \u00e0 jour \n\ self_registration_disabled=La cr\u00E9ation de compte par les utilisateurs est d\u00E9sactiv\u00E9e +self_registration_no_valid_mail=L''adresse email n''est pas autoris\u00E9e recaptcha_not_valid=Recaptcha invalide 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=Le nom d''utilisateur est requis password.field.length=Le mot de passe doit contenir entre {min} et {max} caract\u00E8res password.field.invalid=Le mot de passe doit contenir a minima 1 lettre majuscule, 1 minuscule, 1 chiffre et 1 symbole (ie. `~!@#$%^&*()-_=+[]{}\\|;:'",.<>/?')); +field.required.name=Nom est obligatoire +field.required.email=L''adresse mail est obligatoire +field.notvalid.email=L''adresse mail n''est pas valide api.exception.forbidden=L''acc\u00E8s est refus\u00E9 api.exception.forbidden.description=L''acc\u00E8s est refus\u00E9. Pour y acc\u00E9der, r\u00E9essayez avec un utilisateur disposant de plus de privil\u00E8ges. api.exception.resourceNotFound=Ressource introuvable diff --git a/web/src/main/webapp/WEB-INF/classes/setup/sql/data/data-db-default.sql b/web/src/main/webapp/WEB-INF/classes/setup/sql/data/data-db-default.sql index 5c9e89a1858..ce49770366f 100644 --- a/web/src/main/webapp/WEB-INF/classes/setup/sql/data/data-db-default.sql +++ b/web/src/main/webapp/WEB-INF/classes/setup/sql/data/data-db-default.sql @@ -726,10 +726,11 @@ INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('metada INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/ui/defaultView', 'default', 0, 10100, 'n'); + INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/userSelfRegistration/recaptcha/enable', 'false', 2, 1910, 'n'); INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/userSelfRegistration/recaptcha/publickey', '', 0, 1910, 'n'); INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/userSelfRegistration/recaptcha/secretkey', '', 0, 1910, 'y'); - +INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/userSelfRegistration/domainsAllowed', '', 0, 1911, 'y'); INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/publication/doi/doienabled', 'false', 2, 100191, 'n'); INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/publication/doi/doiurl', '', 0, 100192, 'n'); diff --git a/web/src/main/webapp/WEB-INF/classes/setup/sql/migrate/v446/migrate-default.sql b/web/src/main/webapp/WEB-INF/classes/setup/sql/migrate/v446/migrate-default.sql index 12551f10c8e..cdac905504d 100644 --- a/web/src/main/webapp/WEB-INF/classes/setup/sql/migrate/v446/migrate-default.sql +++ b/web/src/main/webapp/WEB-INF/classes/setup/sql/migrate/v446/migrate-default.sql @@ -1,2 +1,4 @@ UPDATE Settings SET value='4.4.6' WHERE name='system/platform/version'; UPDATE Settings SET value='SNAPSHOT' WHERE name='system/platform/subVersion'; + +INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/userSelfRegistration/domainsAllowed', '', 0, 1911, 'y');