/** * This file is part of alf.io. * * alf.io 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 3 of the License, or * (at your option) any later version. * * alf.io 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 alf.io. If not, see <http://www.gnu.org/licenses/>. */ package alfio.manager.user; import alfio.model.result.ValidationResult; import alfio.model.user.*; import alfio.repository.InvoiceSequencesRepository; import alfio.repository.user.AuthorityRepository; import alfio.repository.user.OrganizationRepository; import alfio.repository.user.UserRepository; import alfio.repository.user.join.UserOrganizationRepository; import alfio.util.PasswordGenerator; import ch.digitalfondue.npjt.AffectedRowCountAndKey; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import java.util.*; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.stream.Collectors.toList; @Component public class UserManager { public static final String ADMIN_USERNAME = "admin"; private static final Function<Integer, Integer> ID_EVALUATOR = id -> Optional.ofNullable(id).orElse(Integer.MIN_VALUE); private final AuthorityRepository authorityRepository; private final OrganizationRepository organizationRepository; private final UserOrganizationRepository userOrganizationRepository; private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final InvoiceSequencesRepository invoiceSequencesRepository; @Autowired public UserManager(AuthorityRepository authorityRepository, OrganizationRepository organizationRepository, UserOrganizationRepository userOrganizationRepository, UserRepository userRepository, PasswordEncoder passwordEncoder, InvoiceSequencesRepository invoiceSequencesRepository) { this.authorityRepository = authorityRepository; this.organizationRepository = organizationRepository; this.userOrganizationRepository = userOrganizationRepository; this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; this.invoiceSequencesRepository = invoiceSequencesRepository; } private List<Authority> getUserAuthorities(User user) { return authorityRepository.findGrantedAuthorities(user.getUsername()); } public List<User> findAllUsers(String username) { return findUserOrganizations(username) .stream() .flatMap(o -> userOrganizationRepository.findByOrganizationId(o.getId()).stream()) .map(uo -> userRepository.findById(uo.getUserId())) .collect(toList()); } public List<User> findAllEnabledUsers(String username) { return findUserOrganizations(username) .stream() .flatMap(o -> userOrganizationRepository.findByOrganizationId(o.getId()).stream()) .map(uo -> userRepository.findById(uo.getUserId())) .filter(User::isEnabled) .collect(toList()); } public User findUserByUsername(String username) { return userRepository.findEnabledByUsername(username).orElseThrow(IllegalArgumentException::new); } public User findUser(int id) { return userRepository.findById(id); } public Collection<Role> getAvailableRoles(String username) { User user = findUserByUsername(username); return isAdmin(user) || isOwner(user) ? EnumSet.of(Role.OWNER, Role.OPERATOR, Role.SUPERVISOR, Role.SPONSOR) : Collections.emptySet(); } /** * Return the most privileged role of a user * @param user * @return user role */ public Role getUserRole(User user) { return getUserAuthorities(user).stream().map(Authority::getRole).sorted().findFirst().orElse(Role.OPERATOR); } public List<Organization> findUserOrganizations(String username) { return findUserOrganizations(userRepository.getByUsername(username)); } public Organization findOrganizationById(int id, String username) { return findUserOrganizations(username) .stream() .filter(o -> o.getId() == id) .findFirst() .orElseThrow(IllegalArgumentException::new); } public List<Organization> findUserOrganizations(User user) { if (isAdmin(user)) { return organizationRepository.findAll(); } return userOrganizationRepository.findByUserId(user.getId()) .stream() .map(uo -> organizationRepository.getById(uo.getOrganizationId())) .collect(toList()); } public boolean isAdmin(User user) { return checkRole(user, Collections.singleton(Role.ADMIN)); } public boolean isOwner(User user) { return checkRole(user, EnumSet.of(Role.ADMIN, Role.OWNER)); } public boolean isOwnerOfOrganization(User user, int organizationId) { return isAdmin(user) || (isOwner(user) && userOrganizationRepository.findByUserId(user.getId()).stream().anyMatch(uo -> uo.getOrganizationId() == organizationId)); } private boolean checkRole(User user, Set<Role> expectedRoles) { Set<String> roleNames = expectedRoles.stream().map(Role::getRoleName).collect(Collectors.toSet()); return authorityRepository.checkRole(user.getUsername(), roleNames); } @Transactional public void createOrganization(String name, String description, String email) { organizationRepository.create(name, description, email); int orgId = organizationRepository.findByName(name).stream().findFirst().orElseThrow(IllegalStateException::new).getId(); invoiceSequencesRepository.initFor(orgId); } @Transactional public void updateOrganization(Integer id, String name, String email, String description) { organizationRepository.update(id, name, description, email); } public ValidationResult validateOrganization(Integer id, String name, String email, String description) { int orgId = ID_EVALUATOR.apply(id); final long existing = organizationRepository.findByName(name) .stream() .filter(o -> o.getId() != orgId) .count(); if(existing > 0) { return ValidationResult.failed(new ValidationResult.ErrorDescriptor("name", "There is already another organization with the same name.")); } Validate.notBlank(name, "name can't be empty"); Validate.notBlank(email, "email can't be empty"); Validate.notBlank(description, "description can't be empty"); return ValidationResult.success(); } @Transactional public void editUser(int id, int organizationId, String username, String firstName, String lastName, String emailAddress, Role role, String currentUsername) { boolean admin = ADMIN_USERNAME.equals(username) && Role.ADMIN == role; if(!admin) { int userOrganizationResult = userOrganizationRepository.updateUserOrganization(id, organizationId); Assert.isTrue(userOrganizationResult == 1, "unexpected error during organization update"); } int userResult = userRepository.update(id, username, firstName, lastName, emailAddress); Assert.isTrue(userResult == 1, "unexpected error during user update"); if(!admin) { Assert.isTrue(getAvailableRoles(currentUsername).contains(role), "cannot assign role "+role); authorityRepository.revokeAll(username); authorityRepository.create(username, role.getRoleName()); } } @Transactional public UserWithPassword insertUser(int organizationId, String username, String firstName, String lastName, String emailAddress, Role role) { Organization organization = organizationRepository.getById(organizationId); String userPassword = PasswordGenerator.generateRandomPassword(); AffectedRowCountAndKey<Integer> result = userRepository.create(username, passwordEncoder.encode(userPassword), firstName, lastName, emailAddress, true); userOrganizationRepository.create(result.getKey(), organization.getId()); authorityRepository.create(username, role.getRoleName()); return new UserWithPassword(userRepository.findById(result.getKey()), userPassword, UUID.randomUUID().toString()); } @Transactional public UserWithPassword resetPassword(int userId) { User user = findUser(userId); String password = PasswordGenerator.generateRandomPassword(); Validate.isTrue(userRepository.resetPassword(userId, passwordEncoder.encode(password)) == 1, "error during password reset"); return new UserWithPassword(user, password, UUID.randomUUID().toString()); } @Transactional public boolean updatePassword(String username, String newPassword) { User user = userRepository.findByUsername(username).stream().findFirst().orElseThrow(IllegalStateException::new); Validate.isTrue(PasswordGenerator.isValid(newPassword), "invalid password"); Validate.isTrue(userRepository.resetPassword(user.getId(), passwordEncoder.encode(newPassword)) == 1, "error during password update"); return true; } @Transactional public void deleteUser(int userId, String currentUsername) { User currentUser = userRepository.findEnabledByUsername(currentUsername).orElseThrow(IllegalArgumentException::new); Assert.isTrue(userId != currentUser.getId(), "sorry but you cannot commit suicide"); userRepository.deleteUserFromSponsorScan(userId); userRepository.deleteUserFromOrganization(userId); userRepository.deleteUser(userId); } @Transactional public void enable(int userId, String currentUsername, boolean status) { User currentUser = userRepository.findEnabledByUsername(currentUsername).orElseThrow(IllegalArgumentException::new); Assert.isTrue(userId != currentUser.getId(), "sorry but you cannot commit suicide"); userRepository.toggleEnabled(userId, status); } public ValidationResult validateUser(Integer id, String username, int organizationId, String role, String firstName, String lastName, String emailAddress) { int userId = ID_EVALUATOR.apply(id); final long existing = userRepository.findByUsername(username) .stream() .filter(u -> u.getId() != userId) .count(); if(existing > 0) { return ValidationResult.failed(new ValidationResult.ErrorDescriptor("username", "There is already another user with the same username.")); } return ValidationResult.of(Stream.of(Pair.of(firstName, "firstName"), Pair.of(lastName, "lastName"), Pair.of(emailAddress, "emailAddress")) .filter(p -> StringUtils.isEmpty(p.getKey())) .map(p -> new ValidationResult.ErrorDescriptor(p.getKey(), p.getValue() + " is required")) .collect(toList())); } public ValidationResult validateNewPassword(String username, String oldPassword, String newPassword, String newPasswordConfirm) { return userRepository.findByUsername(username) .stream() .findFirst() .map(u -> { List<ValidationResult.ErrorDescriptor> errors = new ArrayList<>(); Optional<String> password = userRepository.findPasswordByUsername(username); if(!password.filter(p -> passwordEncoder.matches(oldPassword, p)).isPresent()) { errors.add(new ValidationResult.ErrorDescriptor("alfio.old-password-invalid", "wrong password")); } if(!PasswordGenerator.isValid(newPassword)) { errors.add(new ValidationResult.ErrorDescriptor("alfio.new-password-invalid", "new password is not strong enough")); } if(!StringUtils.equals(newPassword, newPasswordConfirm)) { errors.add(new ValidationResult.ErrorDescriptor("alfio.new-password-does-not-match", "new password has not been confirmed")); } return ValidationResult.of(errors); }) .orElseGet(ValidationResult::failed); } }