From 960ff24597d9299c815a9ddc3b653e208939463a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= Date: Thu, 28 Nov 2024 13:37:29 +0100 Subject: [PATCH] Use caseinsensitive username for login and reset password --- .../fao/geonet/repository/UserRepository.java | 6 +- .../repository/UserRepositoryCustomImpl.java | 18 +++--- .../geonet/repository/UserRepositoryTest.java | 57 ++++++++++++++++--- .../org/fao/geonet/api/users/PasswordApi.java | 32 +++++++---- 4 files changed, 87 insertions(+), 26 deletions(-) diff --git a/domain/src/main/java/org/fao/geonet/repository/UserRepository.java b/domain/src/main/java/org/fao/geonet/repository/UserRepository.java index feaf720afb6..b5ac5138653 100644 --- a/domain/src/main/java/org/fao/geonet/repository/UserRepository.java +++ b/domain/src/main/java/org/fao/geonet/repository/UserRepository.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 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) * @@ -45,6 +45,10 @@ public interface UserRepository extends GeonetRepository, JpaSpec /** * Find all users identified by the provided username ignoring the case. + * + * Old versions allowed to create users with the same username with different case. + * New versions do not allow this. + * * @param username the username. * @return all users with username equals ignore case the provided username. */ diff --git a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java index e5f1efa1166..4c8b54ad482 100644 --- a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java +++ b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 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) * @@ -25,7 +25,6 @@ import org.fao.geonet.domain.*; import org.fao.geonet.utils.Log; -import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import javax.annotation.Nonnull; @@ -60,8 +59,10 @@ public User findOneByEmail(final String email) { CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(User.class); Root root = query.from(User.class); + Join joinedEmailAddresses = root.join(User_.emailAddresses); - query.where(cb.isMember(email, root.get(User_.emailAddresses))); + // Case in-sensitive email search + query.where( cb.equal(cb.lower(joinedEmailAddresses), email.toLowerCase())); final List resultList = _entityManager.createQuery(query).getResultList(); if (resultList.isEmpty()) { return null; @@ -78,10 +79,12 @@ public User findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(final String email) { CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(User.class); Root root = query.from(User.class); + Join joinedEmailAddresses = root.join(User_.emailAddresses); final Path authTypePath = root.get(User_.security).get(UserSecurity_.authType); query.where(cb.and( - cb.isMember(email, root.get(User_.emailAddresses)), + // Case in-sensitive email search + cb.equal(cb.lower(joinedEmailAddresses), email.toLowerCase()), cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), "")))); List results = _entityManager.createQuery(query).getResultList(); @@ -101,7 +104,8 @@ public User findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(final String usern final Path authTypePath = root.get(User_.security).get(UserSecurity_.authType); final Path usernamePath = root.get(User_.username); - query.where(cb.and(cb.equal(usernamePath, username), cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), "")))); + // Case in-sensitive username search + query.where(cb.and(cb.equal(cb.lower(usernamePath), username.toLowerCase()), cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), "")))); List results = _entityManager.createQuery(query).getResultList(); @@ -130,7 +134,7 @@ public List findDuplicatedUsernamesCaseInsensitive() { @Nonnull public List> findAllByGroupOwnerNameAndProfile(@Nonnull final Collection metadataIds, @Nullable final Profile profile) { - List> results = new ArrayList>(); + List> results = new ArrayList<>(); results.addAll(findAllByGroupOwnerNameAndProfileInternal(metadataIds, profile, false)); results.addAll(findAllByGroupOwnerNameAndProfileInternal(metadataIds, profile, true)); @@ -180,7 +184,7 @@ private List> findAllByGroupOwnerNameAndProfileInternal(@Non query.distinct(true); - List> results = new ArrayList>(); + List> results = new ArrayList<>(); for (Tuple result : _entityManager.createQuery(query).getResultList()) { Integer mdId = (Integer) result.get(0); diff --git a/domain/src/test/java/org/fao/geonet/repository/UserRepositoryTest.java b/domain/src/test/java/org/fao/geonet/repository/UserRepositoryTest.java index a6e1ebb6dca..cc020a7d568 100644 --- a/domain/src/test/java/org/fao/geonet/repository/UserRepositoryTest.java +++ b/domain/src/test/java/org/fao/geonet/repository/UserRepositoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 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) * @@ -31,7 +31,6 @@ import org.hamcrest.CoreMatchers; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import javax.annotation.Nullable; @@ -121,6 +120,11 @@ public void testFindByEmailAddress() { assertNotNull(foundUser); assertEquals(user2.getId(), foundUser.getId()); + // Test case-insensitive + foundUser = _userRepo.findOneByEmail(add2b.toUpperCase()); + assertNotNull(foundUser); + assertEquals(user2.getId(), foundUser.getId()); + foundUser = _userRepo.findOneByEmail("xjkjk"); assertNull(foundUser); } @@ -150,10 +154,51 @@ public void testFindByUsernameAndAuthTypeIsNullOrEmpty() { foundUser = _userRepo.findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(user3.getUsername()); assertNull(foundUser); + // Test case-insensitive + foundUser = _userRepo.findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(user3.getUsername().toUpperCase()); + assertNull(foundUser); + foundUser = _userRepo.findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty("blarg"); assertNull(foundUser); } + + @Test + public void testFindOneByEmailAndSecurityAuthTypeIsNullOrEmpty() { + User user1 = newUser(); + user1.getSecurity().setAuthType(""); + user1.getEmailAddresses().add("user1@geonetwork.com"); + user1 = _userRepo.save(user1); + + User user2 = newUser(); + user2.getSecurity().setAuthType(null); + user2.getEmailAddresses().add("user2@geonetwork.com"); + user2 = _userRepo.save(user2); + + User user3 = newUser(); + user3.getSecurity().setAuthType("nonull"); + user3.getEmailAddresses().add("user3@geonetwork.com"); + _userRepo.save(user3); + + User foundUser = _userRepo.findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(user1.getEmail()); + assertNotNull(foundUser); + assertEquals(user1.getId(), foundUser.getId()); + + foundUser = _userRepo.findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(user2.getEmail()); + assertNotNull(foundUser); + assertEquals(user2.getId(), foundUser.getId()); + + foundUser = _userRepo.findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(user3.getEmail()); + assertNull(foundUser); + + // Test case-insensitive + foundUser = _userRepo.findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(user3.getEmail().toUpperCase()); + assertNull(foundUser); + + foundUser = _userRepo.findOneByEmailAndSecurityAuthTypeIsNullOrEmpty("blarg"); + assertNull(foundUser); + } + @Test public void testFindByUsername() { User user1 = newUser(); @@ -219,8 +264,8 @@ public void testFindAllByGroupOwnerNameAndProfile() { assertEquals(4, found.size()); int md1Found = 0; int md2Found = 0; - for (Pair record : found) { - if (record.one() == md1.getId()) { + for (Pair info : found) { + if (info.one() == md1.getId()) { md1Found++; } else { md2Found++; @@ -330,8 +375,6 @@ public void testFindDuplicatedUsernamesCaseInsensitive() { } private User newUser() { - User user = newUser(_inc); - return user; + return newUser(_inc); } - } diff --git a/services/src/main/java/org/fao/geonet/api/users/PasswordApi.java b/services/src/main/java/org/fao/geonet/api/users/PasswordApi.java index 13dcce6d877..09d5dfed6ca 100644 --- a/services/src/main/java/org/fao/geonet/api/users/PasswordApi.java +++ b/services/src/main/java/org/fao/geonet/api/users/PasswordApi.java @@ -1,5 +1,5 @@ //============================================================================= -//=== Copyright (C) 2001-2007 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) //=== @@ -27,7 +27,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jeeves.server.context.ServiceContext; import org.fao.geonet.ApplicationContextHolder; -import org.fao.geonet.api.API; import org.fao.geonet.api.ApiUtils; import org.fao.geonet.api.tools.i18n.LanguageUtils; import org.fao.geonet.constants.Geonet; @@ -57,6 +56,7 @@ import javax.servlet.http.HttpServletRequest; import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.List; import java.util.Locale; import java.util.ResourceBundle; @@ -117,8 +117,9 @@ public ResponseEntity updatePassword( ServiceContext context = ApiUtils.createServiceContext(request); - User user = userRepository.findOneByUsername(username); - if (user == null) { + List existingUsers = userRepository.findByUsernameIgnoreCase(username); + + if (existingUsers.isEmpty()) { Log.warning(LOGGER, String.format("User update password. Can't find user '%s'", username)); @@ -128,6 +129,9 @@ public ResponseEntity updatePassword( XslUtil.encodeForJavaScript(username) ), HttpStatus.PRECONDITION_FAILED); } + + User user = existingUsers.get(0); + if (LDAPConstants.LDAP_FLAG.equals(user.getSecurity().getAuthType())) { Log.warning(LOGGER, String.format("User '%s' is authenticated using LDAP. Password can't be sent by email.", username)); @@ -183,14 +187,16 @@ public ResponseEntity updatePassword( String content = localizedEmail.getParsedMessage(feedbackLocales); // send change link via email with admin in CC - if (!MailUtil.sendMail(user.getEmail(), + Boolean mailSent = MailUtil.sendMail(user.getEmail(), subject, content, null, sm, - adminEmail, "")) { + adminEmail, ""); + if (Boolean.FALSE.equals(mailSent)) { return new ResponseEntity<>(String.format( messages.getString("mail_error")), HttpStatus.PRECONDITION_FAILED); } + return new ResponseEntity<>(String.format( messages.getString("user_password_changed"), XslUtil.encodeForJavaScript(username) @@ -225,8 +231,9 @@ public ResponseEntity sendPasswordByEmail( ServiceContext serviceContext = ApiUtils.createServiceContext(request); - final User user = userRepository.findOneByUsername(username); - if (user == null) { + List existingUsers = userRepository.findByUsernameIgnoreCase(username); + + if (existingUsers.isEmpty()) { Log.warning(LOGGER, String.format("User reset password. Can't find user '%s'", username)); @@ -236,6 +243,7 @@ public ResponseEntity sendPasswordByEmail( XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } + User user = existingUsers.get(0); if (LDAPConstants.LDAP_FLAG.equals(user.getSecurity().getAuthType())) { Log.warning(LOGGER, String.format("User '%s' is authenticated using LDAP. Password can't be sent by email.", @@ -249,7 +257,7 @@ public ResponseEntity sendPasswordByEmail( } String email = user.getEmail(); - if (StringUtils.isEmpty(email)) { + if (StringUtils.hasLength(email)) { Log.warning(LOGGER, String.format("User reset password. User '%s' has no email", username)); @@ -298,14 +306,16 @@ public ResponseEntity sendPasswordByEmail( String content = localizedEmail.getParsedMessage(feedbackLocales); // send change link via email with admin in CC - if (!MailUtil.sendMail(email, + Boolean mailSent = MailUtil.sendMail(email, subject, content, null, sm, - adminEmail, "")) { + adminEmail, ""); + if (Boolean.FALSE.equals(mailSent)) { return new ResponseEntity<>(String.format( messages.getString("mail_error")), HttpStatus.PRECONDITION_FAILED); } + return new ResponseEntity<>(String.format( messages.getString("user_password_sent"), XslUtil.encodeForJavaScript(username)