From 4a28d224154564cc7f91d69505f64c6629e897b7 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 1/6] 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..4360fd7875a 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) From b8455c01a4a9eb4ab679d2c988c57bcf181d74ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= Date: Thu, 28 Nov 2024 15:05:08 +0100 Subject: [PATCH 2/6] Fix reset password page to use the password pattern only when enabled the related setting --- .../src/main/resources/catalog/js/LoginController.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/web-ui/src/main/resources/catalog/js/LoginController.js b/web-ui/src/main/resources/catalog/js/LoginController.js index 15a0a862c6e..d4c71283d6f 100644 --- a/web-ui/src/main/resources/catalog/js/LoginController.js +++ b/web-ui/src/main/resources/catalog/js/LoginController.js @@ -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) * @@ -89,7 +89,13 @@ gnConfig["system.security.passwordEnforcement.maxLength"], 6 ); - $scope.passwordPattern = gnConfig["system.security.passwordEnforcement.pattern"]; + + $scope.usePattern = gnConfig["system.security.passwordEnforcement.usePattern"]; + + if ($scope.usePattern) { + $scope.passwordPattern = + gnConfig["system.security.passwordEnforcement.pattern"]; + } }); $scope.resolveRecaptcha = false; From 9da5df511119a99b19b3de70cd0dfe656deaf61d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= Date: Thu, 28 Nov 2024 15:21:36 +0100 Subject: [PATCH 3/6] Documentation / update user create page to describe that usernames are not case sensitive --- .../managing-users-and-groups/creating-user.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/creating-user.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/creating-user.md index 240fac6944b..e1d35ba75eb 100644 --- a/docs/manual/docs/administrator-guide/managing-users-and-groups/creating-user.md +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/creating-user.md @@ -3,8 +3,11 @@ To add a new user to the GeoNetwork system, please do the following: 1. Select the *Administration* button in the menu. On the Administration page, select *User management*. -2. Click the button *Add a new user*; -3. Provide the *information* required for the new user; -4. Assign the correct *profile* (see [Users, Groups and Roles](index.md#user_profiles)); -5. Assign the user to a *group* (see [Creating group](creating-group.md)); +2. Click the button *Add a new user*. +3. Provide the *information* required for the new user. +4. Assign the correct *profile* (see [Users, Groups and Roles](index.md#user_profiles)). +5. Assign the user to a *group* (see [Creating group](creating-group.md)). 6. Click *Save*. + +!!! note + Usernames are not case sensitive. The application does not allow to create different users with the same username in different cases. From 46865112fd1a7994555aac560d2f4e17c8a188f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= Date: Thu, 28 Nov 2024 16:02:24 +0100 Subject: [PATCH 4/6] Documentation / move user reset password to it's own page and content improvements --- .../img/password-forgot.png | Bin 7042 -> 10244 bytes .../img/selfregistration-start.png | Bin 6744 -> 9227 bytes .../managing-users-and-groups/index.md | 1 + .../user-reset-password.md | 36 ++++++++++++++ .../user-self-registration.md | 44 +++--------------- docs/manual/mkdocs.yml | 1 + 6 files changed, 44 insertions(+), 38 deletions(-) create mode 100644 docs/manual/docs/administrator-guide/managing-users-and-groups/user-reset-password.md diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/password-forgot.png b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/password-forgot.png index d1bc512667dfa6f491f8d5d4d736e336111afacf..bdccc9830b26ccdbe6a9c013a3ad47ce26f65e6a 100644 GIT binary patch literal 10244 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 literal 6744 zcmaiZcT`i~)@_8JVxb5EA|NG(BGn*8Ab>)oNRv)LiWKQRbfkzVp%aR9qzeH;fY6)t z9_hXJUZhFOi|_q&$GhLX-x*_{y~jFhpS|`Pdz?AXT!^MRC{(IL9uS_9ycO2zmE&u>|>)&=Gj+>l;U?hPnD8D9|yZwlSllXUg!(Y=Q_-kFb zjDww>xjh^p<81yOZf?fnZUwhwkyB89s}n%}kl-_@0DYzDF}X8iVEaI4R%)M_KlR2Y ztVkd}sftSXHc~EDUh^cKOGN}6)E?Vz6)J;DC~~Y(pN=z$a;z$;4t5Fn_=+*#DchcB zM=D?YAFZ~Ym~RHiBbZ|V$eQLJloPm`^B{XXe;<1Fd=!~zzmIReEw=Q|UAKGhcG2aZdTgOLHm*WCK=I2t7`BD0u5$>bxKHrF{S&dZnUCt3I_ z9RAN_qNn8js+SB>_-1Ut#Qga=e%C<6l^*}~Dh)Tc92~rSwqd$vwdKvAI~@lofeHr& zcTHcvyx*zQAX-$n!|4}e0lV(2KPl=ODyqz`rl>H~+$rCi?!-(pCGJzN>6Pps6#aqB-`aX8>{a@cm- z)iSWH=j@w;kBrtkSWr?nntSI82$WG#AiwHSTs@8Ak=pbMZBf1hzzuaJkR}+j0)^%Mvv2@;ov_f>nS8LSx-VXi^7hU6Z*yze!ff4I$LjJpG#LYQV_5a zu~lb7$S2Z$(%NV>4$hd+SLo6&@->>GcoK_w-=g@Hqq`<0Gm9?9-M3~3qLpt6+hiGqMlcxZHBF`=M zPxBf^%3HTGvCuFsnK(7)qpboFT_6w^#*xS~ay%+ZZfIpL`&HI_5e5_$74+8NoYh{s zct13N#vjtEg3`{nvrHq4!+cmB&pw;rwE7m(6KGfIt(!&9lPZY=)1QzvfG$EMTfCulg z8aG0k&Fnrp>=c=XuPq8Fj7|Ck^k?Ycw*o5@nJ>fVLJ6jZgUWFG$+95R=c5I({pmHp z?m?`EvfYzznTk%jFUT=_gv+ox*6PALSyTQ7KsOT{Wk_k8Iu2Txg+F#RyWZNDyLof( zI$vSJGy7b^_d0N;U{tEnX#YCH&%ExVU+9PC8j1eFunMVq{gR~^nY_`RnZ(38p~D|6 z^A#@`^&S35a*boQR0A@0PLzAskJT1u;Y_Y(X<|pduYPg_M7u|5Hg;%?re%#dM4m}` zR?c4$=Lw`5y_lV!b@W-7hGSN<-0#gw{VL$XFLt`6SzD(lovp9y>{o%IVPzkTn98A0ocFf)t{e3)w%DbvD{8fOk5WrV2Zt&85A|b zXVmZ=A=#ir1enkHLV&9O3{3w52>*f^C4!LI^wdX>9=-Z@15j57F7dtcfW&i6y>Pq2zB>4jlLV6O?CnoB<4sGh_;;E zB|_?6CDqSBpDVnU5amMyJF>V#IWG z!CGYlzshXxI=Fe}*q&5AY&F;Mnt|YTWnw{Zx;1v8I}O$kQST8THO+FIS6EzYc4?Kv zNItn|7}Ss4fW&{KY5GaNUXeCx1O}1IUMwBNFAhp2N_X0U*N?rjMuvpID-l6g!ZC*E zF$Uh|$E z(!I9NQS}9?2oc@B*+h+V-EoKXb%0zA@$xG|MvrQW*s}(VpNY#1S@f%;hb#}3XesZ( z!cU4@hfQ+32UtTMudrL7mDIB*tiYScgw0sGR^r|PTWeC-1B*Q0vLl1sB1e9xx~u9a zsWd7#*$Wf06WF!7lXR6KQEnCzRLEXCX7_h@S#3kJ49@yP%ww%nAOA+RESfB8I`AZD z=a!-(k8sx3Y5_0c*F6v6@1Q3G@7(oqiKgVmQ)`i(Q0b-xavK9Xi7N|wUK8U+ehJ|> z2PL}H>vvBpss-OS`n9`7GFt6_%q!a6+}pFlsCAx>lvDB#_*NApOO1~X4Xt)HOg~Qd zc&#GK_q#v7ct0_Baa@ODHH2y}t|v)c)g!cY*Wu~e>BUXc?`3&;6F^rZmHwfBx}5~I zEuK$|*zs1}RFcT?fKEQPba0Sb=-+hnYNnWr!j%e+u7Y84ygR$oRuA51Of2lKQdArr zrCa42ixm_|gMq-;+YE!AQ_Ty-{=21(d=RX1+ln6C*@%zEW z{)*TYXK3N*kEIwjwKM~8AXXkVYFX6z#5`R3=>!a+^%hV~LLnj1n z-6*7$q@wruL{cXGf*L33@hjULF_dHwH)*N(LKaFIqpwXcRBZ!!*%rI3?g^%ju$*G$ zR^WG+;~!O3pBf4?+gcqYVGDT>K&KRe>KQgN!SU0tWfI{L?;_+IeCqTG>%y($wYF^I zJk{OhS>i2_0})F*lmXBG)YjCAxv{dTZY8^pW-`VHTM)ICS zoX7ay9bb{`{jE}LA(XUhV`SL;Rjmr5T#iaMD~bAPypmzXn!9RvUl7o$hX{+58aKul zd^zhgFV@Rby9!2HXa{OIX#PTM7wRRH!)O#DAYwsDc6x!~A~p)w6o z$_Fq*V7Zs zZ6&(4Uez0xB6V~5=l5Eb62Vjq;Y3L{y`xuSr*d_-Yoc|H_1g5Bs=xJRN$Ya=Ae_7sn2=yEqBY$88wy--`qS%)f4q{?V zDQ}Z4#xZY)cY!*Z8|v92em(JPQ>~t!FY&H15paUwz`f-`fnsPL{oBIuJEbXfFPPR< zB%Rtyd4p_UIQp=#_DJg(JUfqi6>3h0%#=ZF=s)B^uPiP^^xT2b+fsa4?7q^Hl{2t> z3$%KsImY*>+gUWT_2mmlR6?brdS-K2Jx9?1lE%(*J){eCXQ$!TD;uyi?O-M(M|@w) z)@6eDY11=MTTm5<8wus<07FZEG2L$4Q>}Ou|6Y(*H7K{XojH2k78M|N+41BK5N3(y z;+0@j=^Xez0W_Ehwxcc)D6V|$X(kic`t;OA-=&boAKwUC#R4Y;uX z{quXm-z~owi8B`i%9U7%?a%ooRr*^G(A(UP%l$~qq&{dgZh7l?it(c;AKKjd{&0d# zeGu5l%GVdckvUOllE_h{TU^$Fq?mADU7L!C;S0_1R9tlK6Wge&AC|hR^eh*~Nh*(H zp3iC0^keO`XXbu&YJaTsOZL_GlwYxgKY9TarH9uB4pfivZi|%#Y!q|3s`@1qVsF-Y zN*gFN_$CYepdK657rJMqR*@6+7zvM`o_N}1trP@{PEx<9gwYh6Oc*6OJd5`L)`UpM zy||w~`=fMIAh+cKqcMEsSVg(gCC@P3fsAW)^2rAwl*^xehgU5`FHzY8py`JYxjQj1Va;L*T;Qo`oO1DKMs9@zW%dcs>s`Wc6JudAfk?E zn694mJ3i4pZ|v|Qr==akADP_@Co`%!8cId@k&5i}qqeJm>vM~Y?s4%H*NX{XoHb}6yTZF%AgH$bhU^Y zvNE+vt#1BMYgF7N-O*zuta#I~(!s))1`HDmbwBSP8Y(Vp%m3Iu1C$FqIV9I|vNPpN z3nyxdB`6}+tZOs1?yC1cA8(Ci^$SPfIYwYAo&%^S$KnJ>;0FQiDf?V-$g2=zK z!uV{9vs7s*{G|&pnvQ`%hF^*VrCp?s8bQ~e(AN+FXkU|FUjD`*K}AN5zQ>y*>nm&X zyEC;dEiKT`Ab@|zVWDsk6{2+u-55dYVXuug{HwEYP~Cyw1n`8={e9yt2p-f#%Lw3! z{}1X7MBf?6&fA5v>Jlae7#`9^-I}Z?|4XK}j%870qKC=8I})^|d&gv*ihCMVM1RTU zlG{!H92(+5|1rJYNNa8)4l(hmi0Z@E?=wtvK8FJ;(XDKv=J9Oc{CPuyBP}t;4L27z z6Cj)SR0pMP?y3 z*t3bcg@3vzBkNg`+67p$+{xo6HA|s8arb z|E`3I8nHa}vi%T4AbbUHk}nl!%YB^(?a5DsKEZDC_0p3-94 zekzZuw+QN$xRKL2CWhJ>Bcphociyf%u4_u>hS6KS zJDOZVZrFSt^m^wAMsqVScd@QJ4MQ;cm>3xj{H#dd&2IO#7T49)o$&OQ{o`WbyVfh2 zT6`vt+v(b}L93vr$44d?>q>J`?&|r&JI{-yXBzsa@`v{^rFB4^HC5Amah=GW9&Ryb zt8JA=6sLpHP!8eo&hwGio)?q$6qST$g%GD>6>HnG-sr`;#dDNt{(@=kZ!WLh?0b9e zG&Ey|j}^(%$BTyIMvQ7=-h>+z@sI9E3a1c81${PgD zY8}qeF;+`%@}zPeC(+T#T(9K#%nZYcb)Fur2l?Us?d+;)RH96M1&%Q17bZfoUlloA z-Fq8{aAx`hYyoQ93?Fp;2&Mr5Ad1{ZO zNr#bFf|ghD(7(ehJ)tU(#3{Z<9h}TPmDINhuf`Y&XEe1B%9FRkN%d_|js0GRF?uwq<+;W|{JNba-xV{j+(Lq2H18V-A4X5BhP``v zP+pUosqzT`;>W*FiOKD(PQKn9XI`fdW~0Ic@HJ*_iubEqY2wC1yDI2^wD>`gEXbgf zg2BO-h0j36yI?LFMG)B+`kKEo2W88B#HXkzA=yyj$+u%{olr#ZoVGesn97uX;@Y8{ ztqFn(%!s4x!Z^x2dlLf9?&2Q3+efyLpfqUQxwLnmPOGA=$?YPNLLQ264CMEKc`EC~ zKc+aJB4IZ0`R7m*w{A>#Y}yqx z)EpvH#TC0hQZ{5_x)OW(B~dlA`5SJ#!&|?LUbVgRZY^Aet5~GHEz@v#rnmJuv%60T z$VGJ^=2G#tvJ%2+S$K?nZ4xMCgr3_7Tdvx)pccoiP~`Et#2sYR|M8MRWEt3S_ai?i zK%Bj*6cTVD zU_};4nX2;j`eXmE9l%Sdh{No#HMZv)zh!3R9yQT_N236aO6!q=TKBz7zw|spSFDG< zdj$Z50&fjlAMfu!L(Y4CXxiT|(JPYG0W_Ix6f`CED50iOk_{fF{MQ_nn}c;jOAo1>lX8-g!D;k631Slam0 F{{aP$!>|AV diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/index.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/index.md index aa0408ce3f4..c35bb17f71b 100644 --- a/docs/manual/docs/administrator-guide/managing-users-and-groups/index.md +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/index.md @@ -3,6 +3,7 @@ - [Creating group](creating-group.md) - [Creating user](creating-user.md) - [User Self-Registration](user-self-registration.md) +- [User reset password](user-reset-password.md) - [Authentication mode](authentication-mode.md) ## Default user {#user-defaults} diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/user-reset-password.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-reset-password.md new file mode 100644 index 00000000000..2eb887c85d5 --- /dev/null +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-reset-password.md @@ -0,0 +1,36 @@ +# User 'Forgot your password?' function {#user_forgot_password} + +!!! note + This function requires an email server configured. See [System configuration](../configuring-the-catalog/system-configuration.md#system-config-feedback). + +This function allows users who have forgotten their password to request a new one. Go to the sign in page to access the form: + +![](img/password-forgot.png) + +If a user takes this option they will receive an email inviting them to change their password as follows: + + You have requested to change your Greenhouse GeoNetwork Site password. + + You can change your password using the following link: + + http://localhost:8080/geonetwork/srv/en/password.change.form?username=dubya.shrub@greenhouse.gov&changeKey=635d6c84ddda782a9b6ca9dda0f568b011bb7733 + + This link is valid for today only. + + Greenhouse GeoNetwork Site + +The catalog has generated a changeKey from the forgotten password and the current date and emailed that to the user as part of a link to a change password form. + +If you want to change the content of this email, you should modify `xslt/service/account/password-forgotten-email.xsl`. + +When the user clicks on the link, a change password form is displayed in their browser and a new password can be entered. When that form is submitted, the changeKey is regenerated and checked with the changeKey supplied in the link, if they match then the password is changed to the new password supplied by the user. + +The final step in this process is a verification email sent to the email address of the user confirming that a change of password has taken place: + + Your Greenhouse GeoNetwork Site password has been changed. + + If you did not change this password contact the Greenhouse GeoNetwork Site helpdesk + + The Greenhouse GeoNetwork Site team + +If you want to change the content of this email, you should modify `xslt/service/account/password-changed-email.xsl`. diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md index fe3cb2d0142..aa7fdbb254b 100644 --- a/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md @@ -1,5 +1,9 @@ # User Self-Registration {#user_self_registration} +!!! note + This function requires an email server configured. See [System configuration](../configuring-the-catalog/system-configuration.md#system-config-feedback). + + To enable the self-registration functions, see [System configuration](../configuring-the-catalog/system-configuration.md). When self-registration is enabled, for users that are not logged in, an additional link is shown on the login page: ![](img/selfregistration-start.png) @@ -15,8 +19,8 @@ The fields in this form are self-explanatory except for the following: - the user will still be given the `Registered User` profile - an email will be sent to the Email address nominated in the Feedback section of the 'System Administration' menu, informing them of the request for a more privileged profile - **Requested group**: By default, self-registered users are not assigned to any group. If a group is selected: - - the user will still not be assigned to any group - - an email will be sent to the Email address nominated in the Feedback section of the 'System Administration' menu, informing them of the requested group. + - the user will still not be assigned to any group + - an email will be sent to the Email address nominated in the Feedback section of the 'System Administration' menu, informing them of the requested group. ## What happens when a user self-registers? @@ -72,39 +76,3 @@ If you want to change the content of this email, you should modify `xslt/service The Greenhouse GeoNetwork Site If you want to change the content of this email, you should modify `xslt/service/account/registration-prof-email.xsl`. - -## The 'Forgot your password?' function - -This function allows users who have forgotten their password to request a new one. Go to the sign in page to access the form: - -![](img/password-forgot.png) - -For security reasons, only users that have the `Registered User` profile can request a new password. - -If a user takes this option they will receive an email inviting them to change their password as follows: - - You have requested to change your Greenhouse GeoNetwork Site password. - - You can change your password using the following link: - - http://localhost:8080/geonetwork/srv/en/password.change.form?username=dubya.shrub@greenhouse.gov&changeKey=635d6c84ddda782a9b6ca9dda0f568b011bb7733 - - This link is valid for today only. - - Greenhouse GeoNetwork Site - -The catalog has generated a changeKey from the forgotten password and the current date and emailed that to the user as part of a link to a change password form. - -If you want to change the content of this email, you should modify `xslt/service/account/password-forgotten-email.xsl`. - -When the user clicks on the link, a change password form is displayed in their browser and a new password can be entered. When that form is submitted, the changeKey is regenerated and checked with the changeKey supplied in the link, if they match then the password is changed to the new password supplied by the user. - -The final step in this process is a verification email sent to the email address of the user confirming that a change of password has taken place: - - Your Greenhouse GeoNetwork Site password has been changed. - - If you did not change this password contact the Greenhouse GeoNetwork Site helpdesk - - The Greenhouse GeoNetwork Site team - -If you want to change the content of this email, you should modify `xslt/service/account/password-changed-email.xsl`. diff --git a/docs/manual/mkdocs.yml b/docs/manual/mkdocs.yml index 3b103d7474d..d3966458d36 100644 --- a/docs/manual/mkdocs.yml +++ b/docs/manual/mkdocs.yml @@ -311,6 +311,7 @@ nav: - administrator-guide/managing-users-and-groups/creating-group.md - administrator-guide/managing-users-and-groups/creating-user.md - administrator-guide/managing-users-and-groups/user-self-registration.md + - administrator-guide/managing-users-and-groups/user-reset-password.md - 'Classification Systems': - administrator-guide/managing-classification-systems/index.md - administrator-guide/managing-classification-systems/managing-categories.md From 725a9516205ecc8382a79179f3317198c46cea9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Luis=20Rodri=CC=81guez?= Date: Fri, 29 Nov 2024 13:33:59 +0100 Subject: [PATCH 5/6] Order returned users by username --- .../repository/UserRepositoryCustom.java | 2 +- .../repository/UserRepositoryCustomImpl.java | 76 ++++---- .../geonet/repository/UserRepositoryTest.java | 169 +++++++++--------- .../org/fao/geonet/api/users/PasswordApi.java | 29 ++- 4 files changed, 141 insertions(+), 135 deletions(-) diff --git a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustom.java b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustom.java index 65e3162a22e..21148980e14 100644 --- a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustom.java +++ b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustom.java @@ -61,7 +61,7 @@ public interface UserRepositoryCustom { */ @Nonnull List> findAllByGroupOwnerNameAndProfile(@Nonnull Collection metadataIds, - @Nullable Profile profil); + @Nullable Profile profile); /** * Find all the users that own at least one metadata element. 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 4c8b54ad482..4585548d9fe 100644 --- a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java +++ b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java @@ -47,36 +47,37 @@ public class UserRepositoryCustomImpl implements UserRepositoryCustom { @PersistenceContext - private EntityManager _entityManager; + private EntityManager entityManager; @Override public User findOne(final String userId) { - return _entityManager.find(User.class, Integer.valueOf(userId)); + return entityManager.find(User.class, Integer.valueOf(userId)); } @Override - public User findOneByEmail(final String email) { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + public User findOneByEmail(@Nonnull 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); // Case in-sensitive email search - query.where( cb.equal(cb.lower(joinedEmailAddresses), email.toLowerCase())); - final List resultList = _entityManager.createQuery(query).getResultList(); + query.where(cb.equal(cb.lower(joinedEmailAddresses), email.toLowerCase())); + query.orderBy(cb.asc(root.get(User_.username))); + final List resultList = entityManager.createQuery(query).getResultList(); if (resultList.isEmpty()) { return null; } if (resultList.size() > 1) { - Log.error(Constants.DOMAIN_LOG_MODULE, "The database is inconsistent. There are multiple users with the email address: " + - email); + Log.error(Constants.DOMAIN_LOG_MODULE, String.format("The database is inconsistent. There are multiple users with the email address: %s", + email)); } return resultList.get(0); } @Override - public User findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(final String email) { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + public User findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(@Nonnull 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); @@ -85,33 +86,44 @@ public User findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(final String email) { query.where(cb.and( // 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(); + cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), ""))) + ).orderBy(cb.asc(root.get(User_.username))); + List results = entityManager.createQuery(query).getResultList(); if (results.isEmpty()) { return null; } else { + if (results.size() > 1) { + Log.error(Constants.DOMAIN_LOG_MODULE, String.format("The database is inconsistent. There are multiple users with the email address: %s", + email)); + } return results.get(0); } } @Override - public User findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(final String username) { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + public User findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(@Nonnull final String username) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(User.class); Root root = query.from(User.class); final Path authTypePath = root.get(User_.security).get(UserSecurity_.authType); final Path usernamePath = root.get(User_.username); // 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(); - + query.where(cb.and( + cb.equal(cb.lower(usernamePath), username.toLowerCase()), + cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), ""))) + ).orderBy(cb.asc(root.get(User_.username))); + List results = entityManager.createQuery(query).getResultList(); if (results.isEmpty()) { return null; } else { + if (results.size() > 1) { + Log.error(Constants.DOMAIN_LOG_MODULE, String.format("The database is inconsistent. There are multiple users with username: %s", + username)); + } return results.get(0); } } @@ -119,7 +131,7 @@ public User findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(final String usern @Nonnull @Override public List findDuplicatedUsernamesCaseInsensitive() { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(String.class); Root userRoot = query.from(User.class); @@ -127,7 +139,7 @@ public List findDuplicatedUsernamesCaseInsensitive() { query.groupBy(cb.lower(userRoot.get(User_.username))); query.having(cb.gt(cb.count(userRoot), 1)); - return _entityManager.createQuery(query).getResultList(); + return entityManager.createQuery(query).getResultList(); } @Override @@ -143,8 +155,8 @@ public List> findAllByGroupOwnerNameAndProfile(@Nonnull fina } private List> findAllByGroupOwnerNameAndProfileInternal(@Nonnull final Collection metadataIds, - @Nullable final Profile profile, boolean draft) { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + @Nullable final Profile profile, boolean draft) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(Tuple.class); Root userRoot = query.from(User.class); @@ -152,22 +164,20 @@ private List> findAllByGroupOwnerNameAndProfileInternal(@Non Predicate metadataPredicate; Predicate ownerPredicate; - Root metadataRoot = null; - Root metadataDraftRoot = null; if (!draft) { - metadataRoot = query.from(Metadata.class); + Root metadataRoot = query.from(Metadata.class); query.multiselect(metadataRoot.get(Metadata_.id), userRoot); metadataPredicate = metadataRoot.get(Metadata_.id).in(metadataIds); ownerPredicate = cb.equal(metadataRoot.get(Metadata_.sourceInfo).get(MetadataSourceInfo_.groupOwner), userGroupRoot.get(UserGroup_.id).get(UserGroupId_.groupId)); } else { - metadataDraftRoot = query.from(MetadataDraft.class); - query.multiselect(metadataDraftRoot.get(MetadataDraft_.id), userRoot); - metadataPredicate = metadataDraftRoot.get(Metadata_.id).in(metadataIds); + Root metadataRoot = query.from(MetadataDraft.class); + query.multiselect(metadataRoot.get(MetadataDraft_.id), userRoot); + metadataPredicate = metadataRoot.get(MetadataDraft_.id).in(metadataIds); - ownerPredicate = cb.equal(metadataDraftRoot.get(Metadata_.sourceInfo).get(MetadataSourceInfo_.groupOwner), + ownerPredicate = cb.equal(metadataRoot.get(MetadataDraft_.sourceInfo).get(MetadataSourceInfo_.groupOwner), userGroupRoot.get(UserGroup_.id).get(UserGroupId_.groupId)); } @@ -186,7 +196,7 @@ private List> findAllByGroupOwnerNameAndProfileInternal(@Non List> results = new ArrayList<>(); - for (Tuple result : _entityManager.createQuery(query).getResultList()) { + for (Tuple result : entityManager.createQuery(query).getResultList()) { Integer mdId = (Integer) result.get(0); User user = (User) result.get(1); results.add(Pair.read(mdId, user)); @@ -197,7 +207,7 @@ private List> findAllByGroupOwnerNameAndProfileInternal(@Non @Nonnull @Override public List findAllUsersThatOwnMetadata() { - final CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); final CriteriaQuery query = cb.createQuery(User.class); final Root metadataRoot = query.from(Metadata.class); @@ -210,13 +220,13 @@ public List findAllUsersThatOwnMetadata() { query.where(ownerExpression); query.distinct(true); - return _entityManager.createQuery(query).getResultList(); + return entityManager.createQuery(query).getResultList(); } @Nonnull @Override public List findAllUsersInUserGroups(@Nonnull final Specification userGroupSpec) { - final CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); final CriteriaQuery query = cb.createQuery(User.class); final Root userGroupRoot = query.from(UserGroup.class); @@ -229,7 +239,7 @@ public List findAllUsersInUserGroups(@Nonnull final Specification> found = _userRepo.findAllByGroupOwnerNameAndProfile(Arrays.asList(md1.getId()), null); - Collections.sort(found, Comparator.comparing(s -> s.two().getName())); + List> found = userRepo.findAllByGroupOwnerNameAndProfile(List.of(md1.getId()), null); + found.sort(Comparator.comparing(s -> s.two().getName())); assertEquals(2, found.size()); assertEquals(md1.getId(), found.get(0).one().intValue()); @@ -248,9 +248,9 @@ public void testFindAllByGroupOwnerNameAndProfile() { assertEquals(editUser, found.get(0).two()); assertEquals(reviewerUser, found.get(1).two()); - found = _userRepo.findAllByGroupOwnerNameAndProfile(Arrays.asList(md1.getId()), null); + found = userRepo.findAllByGroupOwnerNameAndProfile(List.of(md1.getId()), null); // Sort by user name descending - Collections.sort(found, Comparator.comparing(s -> s.two().getName(), Comparator.reverseOrder())); + found.sort(Comparator.comparing(s -> s.two().getName(), Comparator.reverseOrder())); assertEquals(2, found.size()); assertEquals(md1.getId(), found.get(0).one().intValue()); @@ -259,7 +259,7 @@ public void testFindAllByGroupOwnerNameAndProfile() { assertEquals(reviewerUser, found.get(0).two()); - found = _userRepo.findAllByGroupOwnerNameAndProfile(Arrays.asList(md1.getId(), md2.getId()), null); + found = userRepo.findAllByGroupOwnerNameAndProfile(Arrays.asList(md1.getId(), md2.getId()), null); assertEquals(4, found.size()); int md1Found = 0; @@ -277,21 +277,21 @@ public void testFindAllByGroupOwnerNameAndProfile() { @Test public void testFindAllUsersInUserGroups() { - Group group1 = _groupRepo.save(GroupRepositoryTest.newGroup(_inc)); - Group group2 = _groupRepo.save(GroupRepositoryTest.newGroup(_inc)); + Group group1 = groupRepo.save(GroupRepositoryTest.newGroup(_inc)); + Group group2 = groupRepo.save(GroupRepositoryTest.newGroup(_inc)); - User editUser = _userRepo.save(newUser().setProfile(Profile.Editor)); - User reviewerUser = _userRepo.save(newUser().setProfile(Profile.Reviewer)); - User registeredUser = _userRepo.save(newUser().setProfile(Profile.RegisteredUser)); - _userRepo.save(newUser().setProfile(Profile.Administrator)); + User editUser = userRepo.save(newUser().setProfile(Profile.Editor)); + User reviewerUser = userRepo.save(newUser().setProfile(Profile.Reviewer)); + User registeredUser = userRepo.save(newUser().setProfile(Profile.RegisteredUser)); + userRepo.save(newUser().setProfile(Profile.Administrator)); - _userGroupRepository.save(new UserGroup().setGroup(group1).setUser(editUser).setProfile(Profile.Editor)); - _userGroupRepository.save(new UserGroup().setGroup(group2).setUser(registeredUser).setProfile(Profile.RegisteredUser)); - _userGroupRepository.save(new UserGroup().setGroup(group2).setUser(reviewerUser).setProfile(Profile.Editor)); - _userGroupRepository.save(new UserGroup().setGroup(group1).setUser(reviewerUser).setProfile(Profile.Reviewer)); + userGroupRepository.save(new UserGroup().setGroup(group1).setUser(editUser).setProfile(Profile.Editor)); + userGroupRepository.save(new UserGroup().setGroup(group2).setUser(registeredUser).setProfile(Profile.RegisteredUser)); + userGroupRepository.save(new UserGroup().setGroup(group2).setUser(reviewerUser).setProfile(Profile.Editor)); + userGroupRepository.save(new UserGroup().setGroup(group1).setUser(reviewerUser).setProfile(Profile.Reviewer)); - List found = Lists.transform(_userRepo.findAllUsersInUserGroups(UserGroupSpecs.hasGroupId(group1.getId())), - new Function() { + List found = Lists.transform(userRepo.findAllUsersInUserGroups(UserGroupSpecs.hasGroupId(group1.getId())), + new Function<>() { @Nullable @Override @@ -304,7 +304,7 @@ public Integer apply(@Nullable User input) { assertTrue(found.contains(editUser.getId())); assertTrue(found.contains(reviewerUser.getId())); - found = Lists.transform(_userRepo.findAllUsersInUserGroups(Specification.not(UserGroupSpecs.hasProfile(Profile.RegisteredUser) + found = Lists.transform(userRepo.findAllUsersInUserGroups(Specification.not(UserGroupSpecs.hasProfile(Profile.RegisteredUser) )), new Function() { @Nullable @@ -323,21 +323,20 @@ public Integer apply(@Nullable User input) { @Test public void testFindAllUsersThatOwnMetadata() { - - User editUser = _userRepo.save(newUser().setProfile(Profile.Editor)); - User reviewerUser = _userRepo.save(newUser().setProfile(Profile.Reviewer)); - _userRepo.save(newUser().setProfile(Profile.RegisteredUser)); - _userRepo.save(newUser().setProfile(Profile.Administrator)); + User editUser = userRepo.save(newUser().setProfile(Profile.Editor)); + User reviewerUser = userRepo.save(newUser().setProfile(Profile.Reviewer)); + userRepo.save(newUser().setProfile(Profile.RegisteredUser)); + userRepo.save(newUser().setProfile(Profile.Administrator)); Metadata md1 = MetadataRepositoryTest.newMetadata(_inc); md1.getSourceInfo().setOwner(editUser.getId()); - _metadataRepo.save(md1); + metadataRepo.save(md1); Metadata md2 = MetadataRepositoryTest.newMetadata(_inc); md2.getSourceInfo().setOwner(reviewerUser.getId()); - _metadataRepo.save(md2); + metadataRepo.save(md2); - List found = _userRepo.findAllUsersThatOwnMetadata(); + List found = userRepo.findAllUsersThatOwnMetadata(); assertEquals(2, found.size()); boolean editUserFound = false; @@ -363,12 +362,12 @@ public void testFindDuplicatedUsernamesCaseInsensitive() { User userNonDuplicated1 = newUser(); usernameDuplicated1.setUsername("userNamE1"); usernameDuplicated2.setUsername("usERNAME1"); - _userRepo.save(usernameDuplicated1); - _userRepo.save(usernameDuplicated2); - _userRepo.save(userNonDuplicated1); + userRepo.save(usernameDuplicated1); + userRepo.save(usernameDuplicated2); + userRepo.save(userNonDuplicated1); - List duplicatedUsernames = _userRepo.findDuplicatedUsernamesCaseInsensitive(); - assertThat("Duplicated usernames don't match the expected ones", + List duplicatedUsernames = userRepo.findDuplicatedUsernamesCaseInsensitive(); + MatcherAssert.assertThat("Duplicated usernames don't match the expected ones", duplicatedUsernames, CoreMatchers.is(Lists.newArrayList("username1"))); assertEquals(1, duplicatedUsernames.size()); 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 4360fd7875a..00e4010dad8 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 @@ -76,6 +76,7 @@ public class PasswordApi { public static final String LOGGER = Geonet.GEONETWORK + ".api.user"; public static final String DATE_FORMAT = "yyyy-MM-dd"; + public static final String USER_PASSWORD_SENT = "user_password_sent"; @Autowired LanguageUtils languageUtils; @Autowired @@ -85,14 +86,13 @@ public class PasswordApi { @Autowired FeedbackLanguages feedbackLanguages; - @Autowired(required=false) + @Autowired(required = false) SecurityProviderConfiguration securityProviderConfiguration; @io.swagger.v3.oas.annotations.Operation(summary = "Update user password", description = "Get a valid changekey by email first and then update your password.") - @RequestMapping( + @PatchMapping( value = "/{username}", - method = RequestMethod.PATCH, produces = MediaType.TEXT_PLAIN_VALUE) @ResponseStatus(value = HttpStatus.CREATED) @ResponseBody @@ -100,13 +100,12 @@ public ResponseEntity updatePassword( @Parameter(description = "The user name", required = true) @PathVariable - String username, + String username, @Parameter(description = "The new password and a valid change key", required = true) @RequestBody - PasswordUpdateParameter passwordAndChangeKey, - HttpServletRequest request) - throws Exception { + PasswordUpdateParameter passwordAndChangeKey, + HttpServletRequest request) { Locale locale = languageUtils.parseAcceptLanguage(request.getLocales()); ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale); Locale[] feedbackLocales = feedbackLanguages.getLocales(locale); @@ -208,9 +207,8 @@ public ResponseEntity updatePassword( "reset his password. User MUST have an email to get the link. " + "LDAP users will not be able to retrieve their password " + "using this service.") - @RequestMapping( + @PutMapping( value = "/actions/forgot-password", - method = RequestMethod.PUT, produces = MediaType.TEXT_PLAIN_VALUE) @ResponseStatus(value = HttpStatus.CREATED) @ResponseBody @@ -218,9 +216,8 @@ public ResponseEntity sendPasswordByEmail( @Parameter(description = "The user name", required = true) @RequestParam - String username, - HttpServletRequest request) - throws Exception { + String username, + HttpServletRequest request) { Locale locale = languageUtils.parseAcceptLanguage(request.getLocales()); ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale); Locale[] feedbackLocales = feedbackLanguages.getLocales(locale); @@ -239,7 +236,7 @@ public ResponseEntity sendPasswordByEmail( // Return response not providing details about the issue, that should be logged. return new ResponseEntity<>(String.format( - messages.getString("user_password_sent"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } @@ -251,7 +248,7 @@ public ResponseEntity sendPasswordByEmail( // Return response not providing details about the issue, that should be logged. return new ResponseEntity<>(String.format( - messages.getString("user_password_sent"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } @@ -263,7 +260,7 @@ public ResponseEntity sendPasswordByEmail( // Return response not providing details about the issue, that should be logged. return new ResponseEntity<>(String.format( - messages.getString("user_password_sent"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } @@ -317,7 +314,7 @@ public ResponseEntity sendPasswordByEmail( } return new ResponseEntity<>(String.format( - messages.getString("user_password_sent"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } From f7d21e19dd5c6e713113f0237eb10a7aea63cc99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Luis=20Rodri=CC=81guez?= Date: Fri, 29 Nov 2024 14:47:00 +0100 Subject: [PATCH 6/6] Use Java 8 API --- .../org/fao/geonet/repository/UserRepositoryTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 66528b2e4ca..5c182827020 100644 --- a/domain/src/test/java/org/fao/geonet/repository/UserRepositoryTest.java +++ b/domain/src/test/java/org/fao/geonet/repository/UserRepositoryTest.java @@ -26,6 +26,7 @@ import com.google.common.base.Function; import com.google.common.collect.Lists; +import java.util.Collections; import org.fao.geonet.domain.*; import org.fao.geonet.repository.specification.UserGroupSpecs; import org.hamcrest.CoreMatchers; @@ -239,7 +240,7 @@ public void testFindAllByGroupOwnerNameAndProfile() { userGroupRepository.save(new UserGroup().setGroup(group2).setUser(reviewerUser).setProfile(Profile.Editor)); userGroupRepository.save(new UserGroup().setGroup(group1).setUser(reviewerUser).setProfile(Profile.Reviewer)); - List> found = userRepo.findAllByGroupOwnerNameAndProfile(List.of(md1.getId()), null); + List> found = userRepo.findAllByGroupOwnerNameAndProfile(Collections.singletonList(md1.getId()), null); found.sort(Comparator.comparing(s -> s.two().getName())); assertEquals(2, found.size()); @@ -248,7 +249,7 @@ public void testFindAllByGroupOwnerNameAndProfile() { assertEquals(editUser, found.get(0).two()); assertEquals(reviewerUser, found.get(1).two()); - found = userRepo.findAllByGroupOwnerNameAndProfile(List.of(md1.getId()), null); + found = userRepo.findAllByGroupOwnerNameAndProfile(Collections.singletonList(md1.getId()), null); // Sort by user name descending found.sort(Comparator.comparing(s -> s.two().getName(), Comparator.reverseOrder())); @@ -291,7 +292,7 @@ public void testFindAllUsersInUserGroups() { userGroupRepository.save(new UserGroup().setGroup(group1).setUser(reviewerUser).setProfile(Profile.Reviewer)); List found = Lists.transform(userRepo.findAllUsersInUserGroups(UserGroupSpecs.hasGroupId(group1.getId())), - new Function<>() { + new Function() { @Nullable @Override @@ -368,7 +369,7 @@ public void testFindDuplicatedUsernamesCaseInsensitive() { List duplicatedUsernames = userRepo.findDuplicatedUsernamesCaseInsensitive(); MatcherAssert.assertThat("Duplicated usernames don't match the expected ones", - duplicatedUsernames, CoreMatchers.is(Lists.newArrayList("username1"))); + duplicatedUsernames, CoreMatchers.is(Collections.singletonList("username1"))); assertEquals(1, duplicatedUsernames.size()); }