Skip to content

Commit

Permalink
Use case insensitive username for login and reset password (#8523)
Browse files Browse the repository at this point in the history
* Use caseinsensitive username for login and reset password.
* Fix reset password page to use the password pattern only when enabled the related setting.
* Documentation / update user create page to describe that usernames are not case sensitive.
* Documentation / move user reset password to it's own page and content improvements.

---------

Co-authored-by: Juan Luis Rodríguez <[email protected]>
  • Loading branch information
josegar74 and juanluisrp authored Nov 29, 2024
1 parent 0fc0447 commit 8ca1e5c
Show file tree
Hide file tree
Showing 13 changed files with 275 additions and 193 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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/[email protected]&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`.
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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?

Expand Down Expand Up @@ -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/[email protected]&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`.
1 change: 1 addition & 0 deletions docs/manual/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
*
Expand Down Expand Up @@ -45,6 +45,10 @@ public interface UserRepository extends GeonetRepository<User, Integer>, 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public interface UserRepositoryCustom {
*/
@Nonnull
List<Pair<Integer, User>> findAllByGroupOwnerNameAndProfile(@Nonnull Collection<Integer> metadataIds,
@Nullable Profile profil);
@Nullable Profile profile);

/**
* Find all the users that own at least one metadata element.
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
*
Expand All @@ -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;
Expand All @@ -48,89 +47,106 @@
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<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
Join<User, String> joinedEmailAddresses = root.join(User_.emailAddresses);

query.where(cb.isMember(email, root.get(User_.emailAddresses)));
final List<User> 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<User> 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<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
Join<User, String> joinedEmailAddresses = root.join(User_.emailAddresses);

final Path<String> 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<User> 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<User> 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<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);

final Path<String> authTypePath = root.get(User_.security).get(UserSecurity_.authType);
final Path<String> usernamePath = root.get(User_.username);
query.where(cb.and(cb.equal(usernamePath, username), cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), ""))));
List<User> 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<User> 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);
}
}

@Nonnull
@Override
public List<String> findDuplicatedUsernamesCaseInsensitive() {
CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<String> query = cb.createQuery(String.class);

Root<User> userRoot = query.from(User.class);
query = query.select(cb.lower(userRoot.get(User_.username)));
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<Pair<Integer, User>> findAllByGroupOwnerNameAndProfile(@Nonnull final Collection<Integer> metadataIds,
@Nullable final Profile profile) {
List<Pair<Integer, User>> results = new ArrayList<Pair<Integer, User>>();
List<Pair<Integer, User>> results = new ArrayList<>();

results.addAll(findAllByGroupOwnerNameAndProfileInternal(metadataIds, profile, false));
results.addAll(findAllByGroupOwnerNameAndProfileInternal(metadataIds, profile, true));
Expand All @@ -139,31 +155,29 @@ public List<Pair<Integer, User>> findAllByGroupOwnerNameAndProfile(@Nonnull fina
}

private List<Pair<Integer, User>> findAllByGroupOwnerNameAndProfileInternal(@Nonnull final Collection<Integer> metadataIds,
@Nullable final Profile profile, boolean draft) {
CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
@Nullable final Profile profile, boolean draft) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> query = cb.createQuery(Tuple.class);

Root<User> userRoot = query.from(User.class);
Root<UserGroup> userGroupRoot = query.from(UserGroup.class);

Predicate metadataPredicate;
Predicate ownerPredicate;
Root<Metadata> metadataRoot = null;
Root<MetadataDraft> metadataDraftRoot = null;

if (!draft) {
metadataRoot = query.from(Metadata.class);
Root<Metadata> 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<MetadataDraft> 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));
}

Expand All @@ -180,9 +194,9 @@ private List<Pair<Integer, User>> findAllByGroupOwnerNameAndProfileInternal(@Non

query.distinct(true);

List<Pair<Integer, User>> results = new ArrayList<Pair<Integer, User>>();
List<Pair<Integer, User>> 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));
Expand All @@ -193,7 +207,7 @@ private List<Pair<Integer, User>> findAllByGroupOwnerNameAndProfileInternal(@Non
@Nonnull
@Override
public List<User> findAllUsersThatOwnMetadata() {
final CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
final CriteriaQuery<User> query = cb.createQuery(User.class);

final Root<Metadata> metadataRoot = query.from(Metadata.class);
Expand All @@ -206,13 +220,13 @@ public List<User> findAllUsersThatOwnMetadata() {
query.where(ownerExpression);
query.distinct(true);

return _entityManager.createQuery(query).getResultList();
return entityManager.createQuery(query).getResultList();
}

@Nonnull
@Override
public List<User> findAllUsersInUserGroups(@Nonnull final Specification<UserGroup> userGroupSpec) {
final CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
final CriteriaQuery<User> query = cb.createQuery(User.class);

final Root<UserGroup> userGroupRoot = query.from(UserGroup.class);
Expand All @@ -225,7 +239,7 @@ public List<User> findAllUsersInUserGroups(@Nonnull final Specification<UserGrou
query.where(cb.and(ownerExpression, userGroupSpec.toPredicate(userGroupRoot, query, cb)));
query.distinct(true);

return _entityManager.createQuery(query).getResultList();
return entityManager.createQuery(query).getResultList();
}

}
Loading

0 comments on commit 8ca1e5c

Please sign in to comment.