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. 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 d1bc512667d..bdccc9830b2 100644 Binary files a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/password-forgot.png and b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/password-forgot.png differ diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-start.png b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-start.png index 7e9a6f8084f..1c617a5d007 100644 Binary files a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-start.png and b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-start.png differ 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 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/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 e5f1efa1166..4585548d9fe 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; @@ -48,66 +47,83 @@ 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); - query.where(cb.isMember(email, root.get(User_.emailAddresses))); - final List resultList = _entityManager.createQuery(query).getResultList(); + // Case in-sensitive email search + 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); final Path authTypePath = root.get(User_.security).get(UserSecurity_.authType); query.where(cb.and( - cb.isMember(email, root.get(User_.emailAddresses)), - cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), "")))); - List results = _entityManager.createQuery(query).getResultList(); + // Case in-sensitive email search + cb.equal(cb.lower(joinedEmailAddresses), email.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 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); - query.where(cb.and(cb.equal(usernamePath, username), cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), "")))); - List results = _entityManager.createQuery(query).getResultList(); - + // 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), ""))) + ).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); } } @@ -115,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); @@ -123,14 +139,14 @@ 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 @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)); @@ -139,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); @@ -148,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)); } @@ -180,9 +194,9 @@ private List> findAllByGroupOwnerNameAndProfileInternal(@Non query.distinct(true); - List> results = new ArrayList>(); + 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)); @@ -193,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); @@ -206,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); @@ -225,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(Collections.singletonList(md1.getId()), null); + found.sort(Comparator.comparing(s -> s.two().getName())); assertEquals(2, found.size()); assertEquals(md1.getId(), found.get(0).one().intValue()); @@ -203,9 +249,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(Collections.singletonList(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()); @@ -214,13 +260,13 @@ 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; int md2Found = 0; - for (Pair record : found) { - if (record.one() == md1.getId()) { + for (Pair info : found) { + if (info.one() == md1.getId()) { md1Found++; } else { md2Found++; @@ -232,20 +278,20 @@ 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())), + List found = Lists.transform(userRepo.findAllUsersInUserGroups(UserGroupSpecs.hasGroupId(group1.getId())), new Function() { @Nullable @@ -259,7 +305,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 @@ -278,21 +324,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; @@ -318,20 +363,18 @@ 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", - duplicatedUsernames, CoreMatchers.is(Lists.newArrayList("username1"))); + List duplicatedUsernames = userRepo.findDuplicatedUsernamesCaseInsensitive(); + MatcherAssert.assertThat("Duplicated usernames don't match the expected ones", + duplicatedUsernames, CoreMatchers.is(Collections.singletonList("username1"))); assertEquals(1, duplicatedUsernames.size()); } 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..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 @@ -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; @@ -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); @@ -117,8 +116,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 +128,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 +186,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) @@ -202,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 @@ -212,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); @@ -225,17 +228,19 @@ 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)); // 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); } + 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.", @@ -243,19 +248,19 @@ 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); } 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)); // 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); } @@ -298,16 +303,18 @@ 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"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } 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;