Skip to content

Commit

Permalink
Password enforcement
Browse files Browse the repository at this point in the history
  • Loading branch information
josegar74 authored and fxprunayre committed Apr 14, 2021
1 parent aef2a2f commit b5b5800
Show file tree
Hide file tree
Showing 21 changed files with 586 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ public class Settings {
public static final String NODE_DEFAULT = "node/default";
public static final String NODE_NAME = "node/name";

public static final String SYSTEM_SECURITY_PASSWORDENFORCEMENT_MINLENGH = "system/security/passwordEnforcement/minLength";
public static final String SYSTEM_SECURITY_PASSWORDENFORCEMENT_MAXLENGH = "system/security/passwordEnforcement/maxLength";
public static final String SYSTEM_SECURITY_PASSWORDENFORCEMENT_USEPATTERN = "system/security/passwordEnforcement/usePattern";
public static final String SYSTEM_SECURITY_PASSWORDENFORCEMENT_PATTERN = "system/security/passwordEnforcement/pattern";

public static class GNSetting {
private String name;
private boolean nullable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,6 @@ self_registration_disabled=User self-registration is disabled
recaptcha_not_valid=Recaptcha is not valid
metadata.title.createdFromTemplate=Copy of template %s created at %s
metadata.title.createdFromRecord=Copy of record %s created at %s
username.field.required=Username is required
password.field.length=Password size should be between {min} and {max} characters
password.field.invalid=Password must contain at least 1 uppercase, 1 lowercase, 1 number and 1 symbol. Symbols include: `~!@#$%^&*()-_=+[]{}\\|;:'",.<>/?');
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,6 @@ self_registration_disabled=User self-registration is disabled
recaptcha_not_valid=Recaptcha is not valid
metadata.title.createdFromTemplate=Copie du mod\u00e8le %s cr\u00E9\u00E9e le %s
metadata.title.createdFromRecord=Copie de la fiche %s cr\u00E9\u00E9e le %s
username.field.required=Username is required
password.field.length=Password size should be between {min} and {max} characters
password.field.invalid=Password must contain at least 1 uppercase, 1 lowercase, 1 number and 1 symbol. Symbols include: `~!@#$%^&*()-_=+[]{}\\|;:'",.<>/?');
50 changes: 49 additions & 1 deletion services/src/main/java/org/fao/geonet/api/ApiUtils.java
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-2021 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 @@ -48,6 +48,8 @@
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
Expand All @@ -66,6 +68,9 @@
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Iterator;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;


Expand Down Expand Up @@ -330,4 +335,47 @@ public static void createFavicon(Image img, Path outFile) throws IOException {
ImageIO.write(bimg, type, out);
}
}

/**
* Process request validation, returning an string with the validation errors.
*
* @param bindingResult
* @param messages
*/
public static String processRequestValidation(BindingResult bindingResult, ResourceBundle messages) {
if (bindingResult.hasErrors()) {
java.util.List<ObjectError> errorList = bindingResult.getAllErrors();

StringBuilder sb = new StringBuilder();
Iterator<ObjectError> it = errorList.iterator();
while (it.hasNext()) {
ObjectError err = it.next();
String msg = "";
for(int i = 0; i < err.getCodes().length; i++) {
try {
msg = messages.getString(err.getCodes()[i]);

if (!StringUtils.isEmpty(msg)) {
break;
}
} catch (MissingResourceException ex) {
// Ignore
}
}

if (StringUtils.isEmpty(msg)) {
msg = err.getDefaultMessage();
}

sb.append(msg);
if (it.hasNext()) {
sb.append(", ");
}
}

return sb.toString();
} else {
return "";
}
}
}
27 changes: 25 additions & 2 deletions services/src/main/java/org/fao/geonet/api/users/RegisterApi.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//=============================================================================
//=== Copyright (C) 2001-2007 Food and Agriculture Organization of the
//=== Copyright (C) 2001-2021 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 @@ -44,11 +44,15 @@
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import javax.servlet.http.HttpServletRequest;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;

Expand Down Expand Up @@ -78,7 +82,10 @@ public ResponseEntity<String> registerUser(
required = true)
@RequestBody
UserRegisterDto userRegisterDto,
HttpServletRequest request)
@Parameter(hidden = true)
BindingResult bindingResult,
@Parameter(hidden = true)
HttpServletRequest request)
throws Exception {

Locale locale = languageUtils.parseAcceptLanguage(request.getLocales());
Expand All @@ -105,6 +112,22 @@ public ResponseEntity<String> registerUser(
}
}

// Validate the user registration
if (bindingResult.hasErrors()) {
List<ObjectError> errorList = bindingResult.getAllErrors();

StringBuilder sb = new StringBuilder();
Iterator<ObjectError> it = errorList.iterator();
while (it.hasNext()) {
sb.append(messages.getString(it.next().getDefaultMessage()));
if (it.hasNext()) {
sb.append(", ");
}
}

return new ResponseEntity<>(sb.toString(), HttpStatus.PRECONDITION_FAILED);
}

final UserRepository userRepository = context.getBean(UserRepository.class);
if (userRepository.findOneByEmail(userRegisterDto.getEmail()) != null) {
return new ResponseEntity<>(String.format(
Expand Down
55 changes: 35 additions & 20 deletions services/src/main/java/org/fao/geonet/api/users/UsersApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
import org.fao.geonet.ApplicationContextHolder;
import org.fao.geonet.api.ApiParams;
import org.fao.geonet.api.ApiUtils;
import org.fao.geonet.api.tools.i18n.LanguageUtils;
import org.fao.geonet.api.users.model.PasswordResetDto;
import org.fao.geonet.api.users.model.UserDto;
import org.fao.geonet.api.users.validation.PasswordResetDtoValidator;
import org.fao.geonet.api.users.validation.UserDtoValidator;
import org.fao.geonet.constants.Params;
import org.fao.geonet.domain.*;
import org.fao.geonet.exceptions.UserNotFoundEx;
Expand All @@ -53,6 +57,7 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.imageio.ImageIO;
Expand Down Expand Up @@ -98,6 +103,9 @@ public class UsersApi {
@Autowired
DataManager dataManager;

@Autowired
LanguageUtils languageUtils;

private BufferedImage pixel;

public UsersApi() {
Expand Down Expand Up @@ -386,6 +394,8 @@ public ResponseEntity<String> createUser(
)
@RequestBody
UserDto userDto,
@Parameter(hidden = true)
BindingResult bindingResult,
@Parameter(hidden = true)
ServletRequest request,
@Parameter(hidden = true)
Expand All @@ -395,6 +405,9 @@ public ResponseEntity<String> createUser(
UserSession session = ApiUtils.getUserSession(httpSession);
Profile myProfile = session.getProfile();

Locale locale = languageUtils.parseAcceptLanguage(request.getLocales());
ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale);

if (profile == Profile.Administrator) {
checkIfAtLeastOneAdminIsEnabled(userDto, userRepository);
}
Expand All @@ -407,10 +420,12 @@ public ResponseEntity<String> createUser(
+ " max profile permitted is: " + myProfile);
}

if (StringUtils.isEmpty(userDto.getUsername())) {
throw new IllegalArgumentException(Params.USERNAME
+ " is a required parameter for "
+ Params.Operation.NEWUSER + " " + "operation");
// Validate userDto data
UserDtoValidator userValidator = new UserDtoValidator();
userValidator.validate(userDto, bindingResult);
String errorMessage = ApiUtils.processRequestValidation(bindingResult, messages);
if (StringUtils.isNotEmpty(errorMessage)) {
throw new IllegalArgumentException(errorMessage);
}

List<User> existingUsers = userRepository.findByUsernameIgnoreCase(userDto.getUsername());
Expand Down Expand Up @@ -554,26 +569,26 @@ public ResponseEntity<String> resetUserPassword(
)
@PathVariable
Integer userIdentifier,
@Parameter(
description = "Password to change (old)."
)
@RequestParam(value = Params.PASSWORD + "Old") String passwordOld,
@Parameter(
description = "Password to change."
)
@RequestParam(value = Params.PASSWORD) String password,
@Parameter(
description = "Password to change (repeat)."
)
@RequestParam(value = Params.PASSWORD + "2") String password2,
@RequestBody
PasswordResetDto passwordResetDto,
@Parameter(hidden = true)
BindingResult bindingResult,
@Parameter(hidden = true)
ServletRequest request,
@Parameter(hidden = true)
HttpSession httpSession
) throws Exception {

if (!password.equals(password2)) {
throw new IllegalArgumentException("Passwords should be equal");
Locale locale = languageUtils.parseAcceptLanguage(request.getLocales());
ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale);


// Validate passwordResetDto data
PasswordResetDtoValidator passwordResetValidator = new PasswordResetDtoValidator();
passwordResetValidator.validate(passwordResetDto, bindingResult);
String errorMessage = ApiUtils.processRequestValidation(bindingResult, messages);
if (StringUtils.isNotEmpty(errorMessage)) {
throw new IllegalArgumentException(errorMessage);
}

UserSession session = ApiUtils.getUserSession(httpSession);
Expand All @@ -591,12 +606,12 @@ public ResponseEntity<String> resetUserPassword(

PasswordEncoder encoder = PasswordUtil.encoder(ApplicationContextHolder.get());

if (!encoder.matches(passwordOld, user.get().getPassword())) {
if (!encoder.matches(passwordResetDto.getPasswordOld(), user.get().getPassword())) {
throw new IllegalArgumentException("The old password is not valid");
}

String passwordHash = PasswordUtil.encoder(ApplicationContextHolder.get()).encode(
password);
passwordResetDto.getPassword());
user.get().getSecurity().setPassword(passwordHash);
user.get().getSecurity().getSecurityNotifications().remove(UserSecurityNotification.UPDATE_HASH_REQUIRED);
userRepository.save(user.get());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2001-2021 Food and Agriculture Organization of the
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
* and United Nations Environment Programme (UNEP)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*
* Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
* Rome - Italy. email: geonetwork@osgeo.org
*/

package org.fao.geonet.api.users.model;

import java.util.Objects;

/**
* DTO class for password reset information.
*
*/
public class PasswordResetDto {
private String passwordOld;
private String password;
private String password2;

public String getPasswordOld() {
return passwordOld;
}

public void setPasswordOld(String passwordOld) {
this.passwordOld = passwordOld;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getPassword2() {
return password2;
}

public void setPassword2(String password2) {
this.password2 = password2;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PasswordResetDto that = (PasswordResetDto) o;
return Objects.equals(passwordOld, that.passwordOld) &&
Objects.equals(password, that.password) &&
Objects.equals(password2, that.password2);
}

@Override
public int hashCode() {

return Objects.hash(passwordOld, password, password2);
}
}
Loading

0 comments on commit b5b5800

Please sign in to comment.