package com.devicehive.service;
/*
* #%L
* DeviceHive Java Server Common business logic
* %%
* Copyright (C) 2016 DataArt
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import com.devicehive.configuration.Constants;
import com.devicehive.configuration.Messages;
import com.devicehive.dao.NetworkDao;
import com.devicehive.dao.UserDao;
import com.devicehive.exceptions.ActionNotAllowedException;
import com.devicehive.exceptions.HiveException;
import com.devicehive.exceptions.IllegalParametersException;
import com.devicehive.model.enums.UserRole;
import com.devicehive.model.enums.UserStatus;
import com.devicehive.model.rpc.ListUserRequest;
import com.devicehive.model.rpc.ListUserResponse;
import com.devicehive.model.updates.UserUpdate;
import com.devicehive.service.configuration.ConfigurationService;
import com.devicehive.service.helpers.PasswordProcessor;
import com.devicehive.service.helpers.ResponseConsumer;
import com.devicehive.service.time.TimestampService;
import com.devicehive.shim.api.Request;
import com.devicehive.shim.api.Response;
import com.devicehive.shim.api.client.RpcClient;
import com.devicehive.util.HiveValidator;
import com.devicehive.vo.NetworkVO;
import com.devicehive.vo.NetworkWithUsersAndDevicesVO;
import com.devicehive.vo.UserVO;
import com.devicehive.vo.UserWithNetworkVO;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static javax.ws.rs.core.Response.Status.FORBIDDEN;
/**
* This class serves all requests to database from controller.
*/
@Component
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
@Autowired
private PasswordProcessor passwordService;
@Autowired
private NetworkDao networkDao;
@Autowired
private UserDao userDao;
@Autowired
private TimestampService timestampService;
@Autowired
private ConfigurationService configurationService;
@Autowired
private HiveValidator hiveValidator;
@Autowired
private RpcClient rpcClient;
/**
* Tries to authenticate with given credentials
*
* @return User object if authentication is successful or null if not
*/
@Transactional(noRollbackFor = ActionNotAllowedException.class)
public UserVO authenticate(String login, String password) {
Optional<UserVO> userOpt = userDao.findByName(login);
if (!userOpt.isPresent()) {
return null;
}
return checkPassword(userOpt.get(), password)
.orElseThrow(() -> new ActionNotAllowedException(String.format(Messages.INCORRECT_CREDENTIALS, login)));
}
@Transactional(noRollbackFor = AccessDeniedException.class)
public UserVO findUser(String login, String password) {
Optional<UserVO> userOpt = userDao.findByName(login);
if (!userOpt.isPresent()) {
logger.error("Can't find user with login {} and password {}", login, password);
throw new AccessDeniedException(Messages.USER_NOT_FOUND);
} else if (userOpt.get().getStatus() != UserStatus.ACTIVE) {
logger.error("User with login {} is not active", login);
throw new AccessDeniedException(Messages.USER_NOT_ACTIVE);
}
return checkPassword(userOpt.get(), password)
.orElseThrow(() -> new AccessDeniedException(String.format(Messages.INCORRECT_CREDENTIALS, login)));
}
private Optional<UserVO> checkPassword(UserVO user, String password) {
boolean validPassword = passwordService.checkPassword(password, user.getPasswordSalt(), user.getPasswordHash());
long loginTimeout = configurationService.getLong(Constants.LAST_LOGIN_TIMEOUT, Constants.LAST_LOGIN_TIMEOUT_DEFAULT);
boolean mustUpdateLoginStatistic = user.getLoginAttempts() != 0
|| user.getLastLogin() == null
|| timestampService.getTimestamp() - user.getLastLogin().getTime() > loginTimeout;
if (validPassword && mustUpdateLoginStatistic) {
UserVO user1 = updateStatisticOnSuccessfulLogin(user, loginTimeout);
return of(user1);
} else if (!validPassword) {
user.setLoginAttempts(user.getLoginAttempts() + 1);
if (user.getLoginAttempts() >=
configurationService.getInt(Constants.MAX_LOGIN_ATTEMPTS, Constants.MAX_LOGIN_ATTEMPTS_DEFAULT)) {
user.setStatus(UserStatus.LOCKED_OUT);
user.setLoginAttempts(0);
}
userDao.merge(user);
return empty();
}
return of(user);
}
private UserVO updateStatisticOnSuccessfulLogin(UserVO user, long loginTimeout) {
boolean update = false;
if (user.getLoginAttempts() != 0) {
update = true;
user.setLoginAttempts(0);
}
if (user.getLastLogin() == null || timestampService.getTimestamp() - user.getLastLogin().getTime() > loginTimeout) {
update = true;
user.setLastLogin(timestampService.getDate());
}
return update ? userDao.merge(user) : user;
}
@Transactional(propagation = Propagation.REQUIRED)
public UserVO updateUser(@NotNull Long id, UserUpdate userToUpdate, UserRole role) {
UserVO existing = userDao.find(id);
if (existing == null) {
logger.error("Can't update user with id {}: user not found", id);
throw new NoSuchElementException(Messages.USER_NOT_FOUND);
}
if (userToUpdate == null) {
return existing;
}
if (userToUpdate.getLogin() != null) {
final String newLogin = StringUtils.trim(userToUpdate.getLogin().orElse(null));
final String oldLogin = existing.getLogin();
Optional<UserVO> withSuchLogin = userDao.findByName(newLogin);
if (withSuchLogin.isPresent() && !withSuchLogin.get().getId().equals(id)) {
throw new ActionNotAllowedException(Messages.DUPLICATE_LOGIN);
}
existing.setLogin(newLogin);
final String googleLogin = StringUtils.isNotBlank(userToUpdate.getGoogleLogin().orElse(null)) ?
userToUpdate.getGoogleLogin().orElse(null) : null;
final String facebookLogin = StringUtils.isNotBlank(userToUpdate.getFacebookLogin().orElse(null)) ?
userToUpdate.getFacebookLogin().orElse(null) : null;
final String githubLogin = StringUtils.isNotBlank(userToUpdate.getGithubLogin().orElse(null)) ?
userToUpdate.getGithubLogin().orElse(null) : null;
if (googleLogin != null || facebookLogin != null || githubLogin != null) {
Optional<UserVO> userWithSameIdentity = userDao.findByIdentityName(oldLogin, googleLogin,
facebookLogin, githubLogin);
if (userWithSameIdentity.isPresent()) {
throw new ActionNotAllowedException(Messages.DUPLICATE_IDENTITY_LOGIN);
}
}
existing.setGoogleLogin(googleLogin);
existing.setFacebookLogin(facebookLogin);
existing.setGithubLogin(githubLogin);
}
if (userToUpdate.getPassword() != null) {
if (userToUpdate.getOldPassword() != null && StringUtils.isNotBlank(userToUpdate.getOldPassword().orElse(null))) {
final String hash = passwordService.hashPassword(userToUpdate.getOldPassword().orElse(null),
existing.getPasswordSalt());
if (!hash.equals(existing.getPasswordHash())) {
logger.error("Can't update user with id {}: incorrect password provided", id);
throw new ActionNotAllowedException(Messages.INCORRECT_CREDENTIALS);
}
} else if (role == UserRole.CLIENT) {
logger.error("Can't update user with id {}: old password required", id);
throw new ActionNotAllowedException(Messages.OLD_PASSWORD_REQUIRED);
}
if (StringUtils.isEmpty(userToUpdate.getPassword().orElse(null))) {
logger.error("Can't update user with id {}: password required", id);
throw new IllegalParametersException(Messages.PASSWORD_REQUIRED);
}
String salt = passwordService.generateSalt();
String hash = passwordService.hashPassword(userToUpdate.getPassword().orElse(null), salt);
existing.setPasswordSalt(salt);
existing.setPasswordHash(hash);
}
if (userToUpdate.getStatus() != null || userToUpdate.getRole() != null) {
if (role != UserRole.ADMIN) {
logger.error("Can't update user with id {}: users eith the 'client' role are only allowed to change their password", id);
throw new HiveException(Messages.INVALID_USER_ROLE, FORBIDDEN.getStatusCode());
} else if (userToUpdate.getRoleEnum() != null) {
existing.setRole(userToUpdate.getRoleEnum());
} else {
existing.setStatus(userToUpdate.getStatusEnum());
}
}
if (userToUpdate.getData() != null) {
existing.setData(userToUpdate.getData().orElse(null));
}
hiveValidator.validate(existing);
return userDao.merge(existing);
}
/**
* Allows user access to given network
*
* @param userId id of user
* @param networkId id of network
*/
@Transactional(propagation = Propagation.REQUIRED)
public void assignNetwork(@NotNull long userId, @NotNull long networkId) {
UserVO existingUser = userDao.find(userId);
if (existingUser == null) {
logger.error("Can't assign network with id {}: user {} not found", networkId, userId);
throw new NoSuchElementException(Messages.USER_NOT_FOUND);
}
NetworkWithUsersAndDevicesVO existingNetwork = networkDao.findWithUsers(networkId)
.orElseThrow(() -> new NoSuchElementException(String.format(Messages.NETWORK_NOT_FOUND, networkId)));
networkDao.assignToNetwork(existingNetwork, existingUser);
}
/**
* Revokes user access to given network
*
* @param userId id of user
* @param networkId id of network
*/
@Transactional(propagation = Propagation.REQUIRED)
public void unassignNetwork(@NotNull long userId, @NotNull long networkId) {
UserVO existingUser = userDao.find(userId);
if (existingUser == null) {
logger.error("Can't unassign network with id {}: user {} not found", networkId, userId);
throw new NoSuchElementException(Messages.USER_NOT_FOUND);
}
userDao.unassignNetwork(existingUser, networkId);
}
//@Transactional(propagation = Propagation.NOT_SUPPORTED)
public CompletableFuture<List<UserVO>> list(String login, String loginPattern, Integer role, Integer status, String sortField,
Boolean sortOrderAsc, Integer take, Integer skip) {
ListUserRequest request = new ListUserRequest();
request.setLogin(login);
request.setLoginPattern(loginPattern);
request.setRole(role);
request.setStatus(status);
request.setSortField(sortField);
request.setSortOrderAsc(sortOrderAsc);
request.setTake(take);
request.setSkip(skip);
CompletableFuture<Response> future = new CompletableFuture<>();
rpcClient.call(Request
.newBuilder()
.withBody(request)
.build(), new ResponseConsumer(future));
return future.thenApply(r -> ((ListUserResponse) r.getBody()).getUsers());
}
/**
* Retrieves user by id (no networks fetched in this case)
*
* @param id user id
* @return User model without networks, or null if there is no such user
*/
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public UserVO findById(@NotNull long id) {
return userDao.find(id);
}
/**
* Retrieves user with networks by id, if there is no networks user hass access to networks will be represented by
* empty set
*
* @param id user id
* @return User model with networks, or null, if there is no such user
*/
@Transactional(propagation = Propagation.SUPPORTS)
public UserWithNetworkVO findUserWithNetworks(@NotNull long id) {
return userDao.getWithNetworksById(id);
}
@Transactional(propagation = Propagation.REQUIRED)
public UserVO createUser(@NotNull UserVO user, String password) {
if (user.getId() != null) {
throw new IllegalParametersException(Messages.ID_NOT_ALLOWED);
}
final String userLogin = StringUtils.trim(user.getLogin());
user.setLogin(userLogin);
Optional<UserVO> existing = userDao.findByName(user.getLogin());
if (existing.isPresent()) {
throw new ActionNotAllowedException(Messages.DUPLICATE_LOGIN);
}
if (StringUtils.isNoneEmpty(password)) {
String salt = passwordService.generateSalt();
String hash = passwordService.hashPassword(password, salt);
user.setPasswordSalt(salt);
user.setPasswordHash(hash);
}
final String googleLogin = StringUtils.isNotBlank(user.getGoogleLogin()) ? user.getGoogleLogin() : null;
final String facebookLogin = StringUtils.isNotBlank(user.getFacebookLogin()) ? user.getFacebookLogin() : null;
final String githubLogin = StringUtils.isNotBlank(user.getGithubLogin()) ? user.getGithubLogin() : null;
if (googleLogin != null || facebookLogin != null || githubLogin != null) {
Optional<UserVO> userWithSameIdentity = userDao.findByIdentityName(userLogin, googleLogin,
facebookLogin, githubLogin);
if (userWithSameIdentity.isPresent()) {
throw new ActionNotAllowedException(Messages.DUPLICATE_IDENTITY_LOGIN);
}
user.setGoogleLogin(googleLogin);
user.setFacebookLogin(facebookLogin);
user.setGithubLogin(githubLogin);
}
user.setLoginAttempts(Constants.INITIAL_LOGIN_ATTEMPTS);
hiveValidator.validate(user);
userDao.persist(user);
return user;
}
/**
* Deletes user by id. deletion is cascade
*
* @param id user id
* @return true in case of success, false otherwise
*/
@Transactional(propagation = Propagation.REQUIRED)
public boolean deleteUser(long id) {
int result = userDao.deleteById(id);
return result > 0;
}
@Transactional(propagation = Propagation.SUPPORTS)
public boolean hasAccessToDevice(UserVO user, String deviceGuid) {
if (!user.isAdmin()) {
long count = userDao.hasAccessToDevice(user, deviceGuid);
return count > 0;
}
return true;
}
@Transactional(propagation = Propagation.SUPPORTS)
public boolean hasAccessToNetwork(UserVO user, NetworkVO network) {
if (!user.isAdmin()) {
long count = userDao.hasAccessToNetwork(user, network);
return count > 0;
}
return true;
}
@Transactional(propagation = Propagation.SUPPORTS)
public UserVO findGoogleUser(String login) {
return userDao.findByGoogleName(login);
}
@Transactional(propagation = Propagation.SUPPORTS)
public UserVO findFacebookUser(String login) {
return userDao.findByFacebookName(login);
}
@Transactional(propagation = Propagation.SUPPORTS)
public UserVO findGithubUser(String login) {
return userDao.findByGithubName(login);
}
@Transactional
public UserVO refreshUserLoginData(UserVO user) {
hiveValidator.validate(user);
final long loginTimeout = configurationService.getLong(Constants.LAST_LOGIN_TIMEOUT, Constants.LAST_LOGIN_TIMEOUT_DEFAULT);
return updateStatisticOnSuccessfulLogin(user, loginTimeout);
}
}