de.thm.arsnova.service.UserServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for de.thm.arsnova.service.UserServiceImpl.java

Source

/*
 * This file is part of ARSnova Backend.
 * Copyright (C) 2012-2018 The ARSnova Team and Contributors
 *
 * ARSnova Backend 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.
 *
 * ARSnova Backend 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, see <http://www.gnu.org/licenses/>.
 */
package de.thm.arsnova.service;

import com.codahale.metrics.annotation.Gauge;
import de.thm.arsnova.model.ClientAuthentication;
import de.thm.arsnova.model.Room;
import de.thm.arsnova.model.UserProfile;
import de.thm.arsnova.persistence.UserRepository;
import de.thm.arsnova.security.GuestUserDetailsService;
import de.thm.arsnova.security.User;
import de.thm.arsnova.security.jwt.JwtService;
import de.thm.arsnova.security.jwt.JwtToken;
import de.thm.arsnova.web.exceptions.BadRequestException;
import de.thm.arsnova.web.exceptions.NotFoundException;
import de.thm.arsnova.web.exceptions.UnauthorizedException;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.util.UriUtils;
import org.stagemonitor.core.metrics.MonitorGauges;

import javax.annotation.PreDestroy;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

/**
 * Performs all user related operations.
 */
@Service
@MonitorGauges
public class UserServiceImpl extends DefaultEntityServiceImpl<UserProfile> implements UserService {

    private static final int LOGIN_TRY_RESET_DELAY_MS = 30 * 1000;

    private static final int LOGIN_BAN_RESET_DELAY_MS = 2 * 60 * 1000;

    private static final int REPEATED_PASSWORD_RESET_DELAY_MS = 3 * 60 * 1000;

    private static final int PASSWORD_RESET_KEY_DURABILITY_MS = 2 * 60 * 60 * 1000;

    private static final long ACTIVATION_KEY_CHECK_INTERVAL_MS = 30 * 60 * 1000L;
    private static final long ACTIVATION_KEY_DURABILITY_MS = 6 * 60 * 60 * 1000L;

    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    private static final ConcurrentHashMap<UUID, String> socketIdToUserId = new ConcurrentHashMap<>();

    /* used for Socket.IO online check solution (new) */
    private static final ConcurrentHashMap<String, String> userIdToRoomId = new ConcurrentHashMap<>();

    private UserRepository userRepository;
    private JwtService jwtService;
    private JavaMailSender mailSender;

    @Autowired(required = false)
    private GuestUserDetailsService guestUserDetailsService;

    @Autowired(required = false)
    private DaoAuthenticationProvider daoProvider;

    @Autowired(required = false)
    private LdapAuthenticationProvider ldapAuthenticationProvider;

    @Value("${root-url}")
    private String rootUrl;

    @Value("${customization.path}")
    private String customizationPath;

    @Value("${security.user-db.allowed-email-domains}")
    private String allowedEmailDomains;

    @Value("${security.user-db.activation-path}")
    private String activationPath;

    @Value("${security.user-db.reset-password-path}")
    private String resetPasswordPath;

    @Value("${mail.sender.address}")
    private String mailSenderAddress;

    @Value("${mail.sender.name}")
    private String mailSenderName;

    @Value("${security.user-db.registration-mail.subject}")
    private String regMailSubject;

    @Value("${security.user-db.registration-mail.body}")
    private String regMailBody;

    @Value("${security.user-db.reset-password-mail.subject}")
    private String resetPasswordMailSubject;

    @Value("${security.user-db.reset-password-mail.body}")
    private String resetPasswordMailBody;

    @Value("${security.authentication.login-try-limit}")
    private int loginTryLimit;

    @Value("${security.admin-accounts}")
    private String[] adminAccounts;

    private Pattern mailPattern;
    private BytesKeyGenerator keygen;
    private BCryptPasswordEncoder encoder;
    private ConcurrentHashMap<String, Byte> loginTries;
    private Set<String> loginBans;

    {
        loginTries = new ConcurrentHashMap<>();
        loginBans = Collections.synchronizedSet(new HashSet<String>());
    }

    public UserServiceImpl(UserRepository repository, JavaMailSender mailSender,
            @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) {
        super(UserProfile.class, repository, jackson2HttpMessageConverter.getObjectMapper());
        this.userRepository = repository;
        this.mailSender = mailSender;
    }

    @Scheduled(fixedDelay = LOGIN_TRY_RESET_DELAY_MS)
    public void resetLoginTries() {
        if (!loginTries.isEmpty()) {
            logger.debug("Reset failed login counters.");
            loginTries.clear();
        }
    }

    @Scheduled(fixedDelay = LOGIN_BAN_RESET_DELAY_MS)
    public void resetLoginBans() {
        if (!loginBans.isEmpty()) {
            logger.info("Reset temporary login bans.");
            loginBans.clear();
        }
    }

    @Scheduled(fixedDelay = ACTIVATION_KEY_CHECK_INTERVAL_MS)
    public void deleteInactiveUsers() {
        logger.info("Delete inactive users.");
        long unixTime = System.currentTimeMillis();
        long lastActivityBefore = unixTime - ACTIVATION_KEY_DURABILITY_MS;
        userRepository.deleteInactiveUsers(lastActivityBefore);
    }

    @Override
    public UserProfile getCurrentUserProfile() {
        final User user = getCurrentUser();
        return getByAuthProviderAndLoginId(user.getAuthProvider(), user.getUsername());
    }

    @Override
    public User getCurrentUser() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !(authentication.getPrincipal() instanceof User)) {
            return null;
        }

        return (User) authentication.getPrincipal();
    }

    @Override
    public de.thm.arsnova.model.ClientAuthentication getCurrentClientAuthentication() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !(authentication.getPrincipal() instanceof User)) {
            return null;
        }
        User user = (User) authentication.getPrincipal();
        String jwt = authentication instanceof JwtToken ? (String) authentication.getCredentials()
                : jwtService.createSignedToken(user);

        ClientAuthentication clientAuthentication = new ClientAuthentication(user.getId(), user.getUsername(),
                user.getAuthProvider(), jwt);

        return clientAuthentication;
    }

    @Override
    public boolean isAdmin(final String username) {
        return Arrays.asList(adminAccounts).contains(username);
    }

    @Override
    public boolean isBannedFromLogin(String addr) {
        return loginBans.contains(addr);
    }

    @Override
    public void increaseFailedLoginCount(String addr) {
        Byte tries = loginTries.get(addr);
        if (null == tries) {
            tries = 0;
        }
        if (tries < loginTryLimit) {
            loginTries.put(addr, ++tries);
            if (loginTryLimit == tries) {
                logger.info("Temporarily banned {} from login.", addr);
                loginBans.add(addr);
            }
        }
    }

    @Override
    public String getUserIdToSocketId(final UUID socketId) {
        return socketIdToUserId.get(socketId);
    }

    @Override
    public void putUserIdToSocketId(final UUID socketId, final String userId) {
        socketIdToUserId.put(socketId, userId);
    }

    @Override
    public Set<Entry<UUID, String>> getSocketIdToUserId() {
        return socketIdToUserId.entrySet();
    }

    @Override
    public void removeUserToSocketId(final UUID socketId) {
        socketIdToUserId.remove(socketId);
    }

    @Override
    public boolean isUserInRoom(final String userId, final String expectedRoomId) {
        String actualRoomId = userIdToRoomId.get(userId);

        return actualRoomId != null && actualRoomId.equals(expectedRoomId);
    }

    @Override
    public Set<String> getUsersByRoomId(final String roomId) {
        final Set<String> result = new HashSet<>();
        for (final Entry<String, String> e : userIdToRoomId.entrySet()) {
            if (e.getValue().equals(roomId)) {
                result.add(e.getKey());
            }
        }

        return result;
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void addUserToRoomBySocketId(final UUID socketId, final String roomId) {
        final String userId = socketIdToUserId.get(socketId);
        userIdToRoomId.put(userId, roomId);
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void removeUserFromRoomBySocketId(final UUID socketId) {
        final String userId = socketIdToUserId.get(socketId);
        if (null == userId) {
            logger.warn("No user exists for socket {}.", socketId);

            return;
        }
        userIdToRoomId.remove(userId);
    }

    @Override
    public String getRoomIdByUserId(final String userId) {
        for (final Entry<String, String> entry : userIdToRoomId.entrySet()) {
            if (entry.getKey().equals(userId)) {
                return entry.getValue();
            }
        }

        return null;
    }

    @PreDestroy
    public void destroy() {
        logger.error("Destroy UserServiceImpl");
    }

    @Override
    public void removeUserIdFromMaps(final String userId) {
        if (userId != null) {
            userIdToRoomId.remove(userId);
        }
    }

    @Override
    @Gauge
    public int loggedInUsers() {
        return userIdToRoomId.size();
    }

    @Override
    public void authenticate(final UsernamePasswordAuthenticationToken token,
            final UserProfile.AuthProvider authProvider) {
        Authentication auth;
        switch (authProvider) {
        case LDAP:
            auth = ldapAuthenticationProvider.authenticate(token);
            break;
        case ARSNOVA:
            auth = daoProvider.authenticate(token);
            break;
        case ARSNOVA_GUEST:
            String id = token.getName();
            boolean autoCreate = false;
            if (id == null || id.isEmpty()) {
                id = generateGuestId();
                autoCreate = true;
            }
            UserDetails userDetails = guestUserDetailsService.loadUserByUsername(id, autoCreate);
            if (userDetails == null) {
                throw new UsernameNotFoundException("Guest user does not exist");
            }
            auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

            break;
        default:
            throw new IllegalArgumentException("Unsupported authentication provider");
        }

        if (!auth.isAuthenticated()) {
            throw new BadRequestException();
        }
        SecurityContextHolder.getContext().setAuthentication(auth);
    }

    @Override
    public User loadUser(final UserProfile.AuthProvider authProvider, final String loginId,
            final Collection<GrantedAuthority> grantedAuthorities, final boolean autoCreate)
            throws UsernameNotFoundException {
        logger.debug("Load user: LoginId: {}, AuthProvider: {}", loginId, authProvider);
        UserProfile userProfile = userRepository.findByAuthProviderAndLoginId(authProvider, loginId);
        if (userProfile == null) {
            if (autoCreate) {
                userProfile = new UserProfile(authProvider, loginId);
                /* Repository is accessed directly without EntityService to skip permission check */
                userRepository.save(userProfile);
            } else {
                throw new UsernameNotFoundException("User does not exist.");
            }
        }

        return new User(userProfile, grantedAuthorities);
    }

    @Override
    public User loadUser(final String userId, final Collection<GrantedAuthority> grantedAuthorities)
            throws UsernameNotFoundException {
        logger.debug("Load user: UserId: {}", userId);
        UserProfile userProfile = userRepository.findOne(userId);
        if (userProfile == null) {
            throw new UsernameNotFoundException("User does not exist.");
        }

        return new User(userProfile, grantedAuthorities);
    }

    @Override
    public UserProfile getByAuthProviderAndLoginId(final UserProfile.AuthProvider authProvider,
            final String loginId) {
        return userRepository.findByAuthProviderAndLoginId(authProvider, loginId);
    }

    @Override
    public UserProfile getByUsername(String username) {
        return userRepository.findByAuthProviderAndLoginId(UserProfile.AuthProvider.ARSNOVA,
                username.toLowerCase());
    }

    @Override
    public UserProfile create(String username, String password) {
        String lcUsername = username.toLowerCase();

        if (null == keygen) {
            keygen = KeyGenerators.secureRandom(16);
        }

        if (null == mailPattern) {
            parseMailAddressPattern();
        }

        if (null == mailPattern || !mailPattern.matcher(lcUsername).matches()) {
            logger.info("User registration failed. {} does not match pattern.", lcUsername);

            return null;
        }

        if (null != userRepository.findByAuthProviderAndLoginId(UserProfile.AuthProvider.ARSNOVA, lcUsername)) {
            logger.info("User registration failed. {} already exists.", lcUsername);

            return null;
        }

        UserProfile userProfile = new UserProfile();
        UserProfile.Account account = new UserProfile.Account();
        userProfile.setAccount(account);
        userProfile.setAuthProvider(UserProfile.AuthProvider.ARSNOVA);
        userProfile.setLoginId(lcUsername);
        account.setPassword(encodePassword(password));
        account.setActivationKey(RandomStringUtils.randomAlphanumeric(32));
        userProfile.setCreationTimestamp(new Date());

        /* Repository is accessed directly without EntityService to skip permission check */
        UserProfile result = userRepository.save(userProfile);
        if (null != result) {
            sendActivationEmail(result);
        } else {
            logger.error("User registration failed. {} could not be created.", lcUsername);
        }

        return result;
    }

    private String encodePassword(String password) {
        if (null == encoder) {
            encoder = new BCryptPasswordEncoder(12);
        }

        return encoder.encode(password);
    }

    private void sendActivationEmail(UserProfile userProfile) {
        String activationUrl = MessageFormat.format("{0}{1}/{2}?action=activate&username={3}&key={4}", rootUrl,
                customizationPath, activationPath, UriUtils.encodeQueryParam(userProfile.getLoginId(), "UTF-8"),
                userProfile.getAccount().getActivationKey());

        sendEmail(userProfile, regMailSubject, MessageFormat.format(regMailBody, activationUrl));
    }

    private void parseMailAddressPattern() {
        /* TODO: Add Unicode support */

        List<String> domainList = Arrays.asList(allowedEmailDomains.split(","));

        if (!domainList.isEmpty()) {
            List<String> patterns = new ArrayList<>();
            if (domainList.contains("*")) {
                patterns.add("([a-z0-9-]+\\.)+[a-z0-9-]+");
            } else {
                Pattern patternPattern = Pattern.compile("[a-z0-9.*-]+", Pattern.CASE_INSENSITIVE);
                for (String patternStr : domainList) {
                    if (patternPattern.matcher(patternStr).matches()) {
                        patterns.add(patternStr.replaceAll("[.]", "[.]").replaceAll("[*]", "[a-z0-9-]+?"));
                    }
                }
            }

            mailPattern = Pattern.compile("[a-z0-9._-]+?@(" + StringUtils.join(patterns, "|") + ")",
                    Pattern.CASE_INSENSITIVE);
            logger.info("Allowed e-mail addresses (pattern) for registration: '{}'.", mailPattern.pattern());
        }
    }

    @Override
    public UserProfile update(UserProfile userProfile) {
        if (null != userProfile.getId()) {
            return userRepository.save(userProfile);
        }

        return null;
    }

    @Override
    public UserProfile deleteByUsername(String username) {
        User user = getCurrentUser();
        if (!user.getUsername().equals(username.toLowerCase()) && !SecurityContextHolder.getContext()
                .getAuthentication().getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
            throw new UnauthorizedException();
        }

        UserProfile userProfile = getByUsername(username);
        if (null == userProfile) {
            throw new NotFoundException();
        }

        userRepository.delete(userProfile);

        return userProfile;
    }

    @Override
    @PreAuthorize("hasPermission(#userProfile, 'update')")
    public void addRoomToHistory(final UserProfile userProfile, final Room room) {
        if (userProfile.getId().equals(room.getOwnerId())) {
            return;
        }
        Set<UserProfile.RoomHistoryEntry> roomHistory = userProfile.getRoomHistory();
        UserProfile.RoomHistoryEntry entry = new UserProfile.RoomHistoryEntry(room.getId(), new Date());
        /* TODO: lastVisit in roomHistory is currently not updated by subsequent method invocations */
        if (!roomHistory.contains(entry)) {
            roomHistory.add(entry);
            Map<String, Object> changes = Collections.singletonMap("roomHistory", roomHistory);
            try {
                super.patch(userProfile, changes);
            } catch (IOException e) {
                logger.error("Could not patch RoomHistory");
            }
        }
    }

    @Override
    public void initiatePasswordReset(String username) {
        UserProfile userProfile = getByUsername(username);
        if (null == userProfile) {
            logger.info("Password reset failed. User {} does not exist.", username);

            throw new NotFoundException();
        }
        UserProfile.Account account = userProfile.getAccount();
        if (System.currentTimeMillis() < account.getPasswordResetTime().getTime()
                + REPEATED_PASSWORD_RESET_DELAY_MS) {
            logger.info("Password reset failed. The reset delay for User {} is still active.", username);

            throw new BadRequestException();
        }

        account.setPasswordResetKey(RandomStringUtils.randomAlphanumeric(32));
        account.setPasswordResetTime(new Date());

        if (null == userRepository.save(userProfile)) {
            logger.error("Password reset failed. {} could not be updated.", username);
        }

        String resetPasswordUrl = MessageFormat.format("{0}{1}/{2}?action=resetpassword&username={3}&key={4}",
                rootUrl, customizationPath, resetPasswordPath,
                UriUtils.encodeQueryParam(userProfile.getLoginId(), "UTF-8"), account.getPasswordResetKey());

        sendEmail(userProfile, resetPasswordMailSubject,
                MessageFormat.format(resetPasswordMailBody, resetPasswordUrl));
    }

    @Override
    public boolean resetPassword(UserProfile userProfile, String key, String password) {
        UserProfile.Account account = userProfile.getAccount();
        if (null == key || "".equals(key) || !key.equals(account.getPasswordResetKey())) {
            logger.info("Password reset failed. Invalid key provided for User {}.", userProfile.getLoginId());

            return false;
        }
        if (System.currentTimeMillis() > account.getPasswordResetTime().getTime()
                + PASSWORD_RESET_KEY_DURABILITY_MS) {
            logger.info("Password reset failed. Key provided for User {} is no longer valid.",
                    userProfile.getLoginId());

            account.setPasswordResetKey(null);
            account.setPasswordResetTime(new Date(0));
            update(userProfile);

            return false;
        }

        account.setPassword(encodePassword(password));
        account.setPasswordResetKey(null);
        if (null == update(userProfile)) {
            logger.error("Password reset failed. {} could not be updated.", userProfile.getLoginId());
        }

        return true;
    }

    private void sendEmail(UserProfile userProfile, String subject, String body) {
        MimeMessage msg = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(msg, "UTF-8");
        try {
            helper.setFrom(mailSenderName + "<" + mailSenderAddress + ">");
            helper.setTo(userProfile.getLoginId());
            helper.setSubject(subject);
            helper.setText(body);

            logger.info("Sending mail \"{}\" from \"{}\" to \"{}\"", subject, msg.getFrom(),
                    userProfile.getLoginId());
            mailSender.send(msg);
        } catch (MailException | MessagingException e) {
            logger.warn("Mail \"{}\" could not be sent.", subject, e);
        }
    }

    private String generateGuestId() {
        if (null == keygen) {
            keygen = KeyGenerators.secureRandom(16);
        }

        return new String(Hex.encode(keygen.generateKey()));
    }

    @Autowired
    public void setJwtService(final JwtService jwtService) {
        this.jwtService = jwtService;
    }
}