Skip to content

Commit

Permalink
feat: The password policy is checked in the backend and has been exte…
Browse files Browse the repository at this point in the history
  • Loading branch information
jekutzsche authored Aug 12, 2021
1 parent 55284fd commit d6bca44
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package iris.client_bff.core.utils;

import static org.apache.commons.lang3.StringUtils.indexOf;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.*;

import iris.client_bff.config.HealthDepartmentConfig;
import iris.client_bff.core.alert.AlertService;
Expand All @@ -12,7 +11,9 @@

import java.util.Arrays;
import java.util.Optional;
import java.util.regex.Pattern;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Range;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
Expand All @@ -24,7 +25,12 @@ public class ValidationHelper {
private final AlertService alertService;
private final HealthDepartmentConfig hdConfig;

private static final String UUID_REGEX = "([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})";
public static final Pattern UUID_REGEX = Pattern
.compile("([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})");
public static final Pattern PW_REGEX = Pattern
.compile("^(?=.*[0-9].*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[_\\-#()@§!])(?!.*[\\s\\u202F\\u00A0]).{8,}$");
public static final String PW_ERROR_MESSAGE = "The specified password does not follow the password policy (≥ 8 characters, no spaces, lowercase and uppercase letters, two numbers, one special character [_-#()@§!]).";
static final String[] PW_SYMBOLS = "_-#()@§!".split("(?!^)");
private static final String[] FORBIDDEN_SYMBOLS = {
"=",
"<",
Expand Down Expand Up @@ -94,14 +100,18 @@ public class ValidationHelper {

public static boolean isUUIDInputValid(String id, String idName) {

if (id.matches(UUID_REGEX)) {
if (UUID_REGEX.matcher(id).matches()) {
return true;
}

log.warn(ErrorMessages.INVALID_INPUT + idName + id);
return false;
}

public boolean isPasswordValid(String password) {
return PW_REGEX.matcher(password).matches();
}

public boolean isPossibleAttackForRequiredValue(String input, String field, boolean obfuscateLogging) {
if (isBlank(input)) {
log.warn(ErrorMessages.MISSING_REQUIRED_INPUT + " - {}" + field);
Expand All @@ -111,24 +121,26 @@ public boolean isPossibleAttackForRequiredValue(String input, String field, bool
return isPossibleAttack(input, field, obfuscateLogging);
}

public boolean isPossibleAttack(String input, String field, boolean obfuscateLogging) {
public boolean isPossibleAttack(String input, String field, boolean obfuscateLogging,
String[][] forbiddenKeywordTuples, String[] forbiddenSymbols) {
if (input == null) {
return false;
}

// Test for attacks
String inputUpper = input.toUpperCase();
Optional<Range<Integer>> range = findAnyOfKeywordTuples(inputUpper, FORBIDDEN_KEYWORD_TUPLES);
if(range.isEmpty()) {
range = findSymbolsAtStart(input, FORBIDDEN_SYMBOLS);
Optional<Range<Integer>> range = findAnyOfKeywordTuples(inputUpper, forbiddenKeywordTuples);
if (range.isEmpty()) {
range = findSymbolsAtStart(input, forbiddenSymbols);
}

if (range.isPresent()) {
String logString = calculateLogableValue(input, obfuscateLogging, range.get());
log.warn(ErrorMessages.INVALID_INPUT + " - {}: {}", field, logString);

alertService.createAlertMessage("Input validation - possible attack",
String.format("Input `%s` in health department with zip code `%s` contains the character or keyword `%s` that is a potential attack!",
String.format(
"Input `%s` in health department with zip code `%s` contains the character or keyword `%s` that is a potential attack!",
field, hdConfig.getZipCode(), logString));

return true;
Expand All @@ -137,11 +149,28 @@ public boolean isPossibleAttack(String input, String field, boolean obfuscateLog
return false;
}

public boolean isPossibleAttack(String input, String field, boolean obfuscateLogging) {
return isPossibleAttack(input, field, obfuscateLogging, FORBIDDEN_KEYWORD_TUPLES, FORBIDDEN_SYMBOLS);
}

public boolean isPossibleAttackForPassword(String input, String field) {

if (isBlank(input)) {
log.warn(ErrorMessages.MISSING_REQUIRED_INPUT + " - {}" + field);
return true;
}

return isPossibleAttack(input, field, true, FORBIDDEN_KEYWORD_TUPLES,
ArrayUtils.removeElements(FORBIDDEN_SYMBOLS, PW_SYMBOLS));
}

/**
* Searches for any occurrence of given keyword tuples and stops at first match.
*
* @param str String to be tested
* @param keywordTuples Array of tuples with keywords to find
* @return If found: range of first matching tuple in tested string starting at beginning of first keyword, ending at end of last keyword.
* @return If found: range of first matching tuple in tested string starting at beginning of first keyword, ending at
* end of last keyword.
*/
private static Optional<Range<Integer>> findAnyOfKeywordTuples(String str, String[][] keywordTuples) {

Expand All @@ -153,6 +182,7 @@ private static Optional<Range<Integer>> findAnyOfKeywordTuples(String str, Strin

/**
* Searches for the keyword tuple in the input string.
*
* @param input String to be tested
* @param keywordTuple tuple with keywords to find
* @return If found: range in tested string starting at beginning of first keyword, ending at end of last keyword.
Expand All @@ -166,7 +196,8 @@ private static Optional<Range<Integer>> findKeywordTuple(String input, String[]

var lastKeyword = keywordTuple[keywordTuple.length - 1];

return Optional.of(Range.between(indexOf(input, keywordTuple[0]), indexOf(input, lastKeyword) + lastKeyword.length() - 1));
return Optional
.of(Range.between(indexOf(input, keywordTuple[0]), indexOf(input, lastKeyword) + lastKeyword.length() - 1));
}

private static Optional<Range<Integer>> findSymbolsAtStart(String input, String[] forbiddenSymbolsArray) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package iris.client_bff.users.web;

import static iris.client_bff.users.web.UserMappers.map;
import static iris.client_bff.users.web.UserMappers.*;

import iris.client_bff.core.utils.ValidationHelper;
import iris.client_bff.ui.messages.ErrorMessages;
Expand Down Expand Up @@ -50,7 +50,8 @@ public class UserController {
@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasAuthority('ADMIN')")
public UserListDTO getAllUsers() {
return new UserListDTO().users(this.userService.loadAll().stream().map(UserMappers::map).collect(Collectors.toList()));
return new UserListDTO()
.users(this.userService.loadAll().stream().map(UserMappers::map).collect(Collectors.toList()));
}

@PostMapping
Expand Down Expand Up @@ -98,13 +99,10 @@ private UserUpdateDTO validateUserUpdateDTO(UserUpdateDTO userUpdateDTO) {
userUpdateDTO.setLastName(ErrorMessages.INVALID_INPUT_STRING);
}

if (validationHelper.isPossibleAttackForRequiredValue(userUpdateDTO.getUserName(), FIELD_USER_NAME, false)) {
userUpdateDTO.setUserName(ErrorMessages.INVALID_INPUT_STRING);
}

boolean isInvalid = false;
var isInvalid = false;

if (validationHelper.isPossibleAttackForRequiredValue(userUpdateDTO.getPassword(), FIELD_PASSWORD, true)) {
if (validationHelper.isPossibleAttackForRequiredValue(userUpdateDTO.getUserName(), FIELD_USER_NAME, false)
|| validationHelper.isPossibleAttackForPassword(userUpdateDTO.getPassword(), FIELD_PASSWORD)) {
isInvalid = true;
}

Expand All @@ -117,6 +115,11 @@ private UserUpdateDTO validateUserUpdateDTO(UserUpdateDTO userUpdateDTO) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ErrorMessages.INVALID_INPUT);
}

if (!validationHelper.isPasswordValid(userUpdateDTO.getPassword())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
ValidationHelper.PW_ERROR_MESSAGE);
}

return userUpdateDTO;
}

Expand All @@ -133,14 +136,11 @@ private UserInsertDTO validateUserInsertDTO(UserInsertDTO userInsertDTO) {
userInsertDTO.setLastName(ErrorMessages.INVALID_INPUT_STRING);
}

if (validationHelper
.isPossibleAttackForRequiredValue(userInsertDTO.getUserName(), FIELD_USER_NAME, false)) {
userInsertDTO.setUserName(ErrorMessages.INVALID_INPUT_STRING);
}

boolean isInvalid = false;
var isInvalid = false;

if (validationHelper.isPossibleAttackForRequiredValue(userInsertDTO.getPassword(), FIELD_PASSWORD, true)) {
if (validationHelper
.isPossibleAttackForRequiredValue(userInsertDTO.getUserName(), FIELD_USER_NAME, false)
|| validationHelper.isPossibleAttackForPassword(userInsertDTO.getPassword(), FIELD_PASSWORD)) {
isInvalid = true;
}

Expand All @@ -153,6 +153,11 @@ private UserInsertDTO validateUserInsertDTO(UserInsertDTO userInsertDTO) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ErrorMessages.INVALID_INPUT);
}

if (!validationHelper.isPasswordValid(userInsertDTO.getPassword())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
ValidationHelper.PW_ERROR_MESSAGE);
}

return userInsertDTO;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package iris.client_bff.users;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import iris.client_bff.config.HealthDepartmentConfig;
import iris.client_bff.core.alert.AlertService;
import iris.client_bff.core.utils.ValidationHelper;
import iris.client_bff.users.entities.UserAccount;
import iris.client_bff.users.entities.UserRole;
import iris.client_bff.users.web.UserController;
import iris.client_bff.users.web.dto.UserInsertDTO;
import iris.client_bff.users.web.dto.UserRoleDTO;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.server.ResponseStatusException;

/**
* @author Jens Kutzsche
*/
@ExtendWith(MockitoExtension.class)
class UserControlerTests {

@Mock(lenient = true)
UserDetailsServiceImpl userService;
@Mock(lenient = true)
AlertService alertService;
@Mock(lenient = true)
HealthDepartmentConfig hdConfig;

@InjectMocks
ValidationHelper validationHelper;

UserController userController;

@BeforeEach
public void init() {

when(hdConfig.getZipCode()).thenReturn("01665");
when(userService.create(any(UserInsertDTO.class))).thenAnswer(it -> {
var user = it.getArgument(0, UserInsertDTO.class);
var account = new UserAccount();
account.setFirstName(user.getFirstName());
account.setLastName(user.getLastName());
account.setPassword(user.getPassword());
account.setUserName(user.getUserName());
account.setRole(UserRole.valueOf(user.getRole().name()));
return account;
});

userController = new UserController(userService, validationHelper);
}

@ParameterizedTest
@ValueSource(
strings = { "Password", "X1ab€2a", "X1ab€aae", "X1aba2ae", "x1ab€2ae", "X1ab€2 ae", "X1ab€2 ae", "X1ab€2 ae" })
void testWrongPasswords(String pw) {

var dto = new UserInsertDTO().firstName("fn").lastName("ln").userName("un").password(pw).role(UserRoleDTO.USER);

Assertions.assertThrows(ResponseStatusException.class, () -> {
userController.createUser(dto);
}, ValidationHelper.PW_ERROR_MESSAGE);
}

@ParameterizedTest
@ValueSource(strings = { "Password12A_", "X1ab_2ae" })
void testCorrectPasswords(String pw) {

var dto = new UserInsertDTO().firstName("fn").lastName("ln").userName("un").password(pw).role(UserRoleDTO.USER);
var user = userController.createUser(dto);

assertThat(user).isNotNull();
}
}

0 comments on commit d6bca44

Please sign in to comment.