dk.dma.msinm.user.UserService.java Source code

Java tutorial

Introduction

Here is the source code for dk.dma.msinm.user.UserService.java

Source

/* Copyright (c) 2011 Danish Maritime Authority
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this library.  If not, see <http://www.gnu.org/licenses/>.
 */
package dk.dma.msinm.user;

import dk.dma.msinm.common.mail.MailService;
import dk.dma.msinm.common.service.BaseService;
import dk.dma.msinm.common.templates.TemplateContext;
import dk.dma.msinm.common.templates.TemplateService;
import dk.dma.msinm.common.templates.TemplateType;
import dk.dma.msinm.user.security.JbossJaasCacheFlusher;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;

import javax.annotation.Resource;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import javax.inject.Inject;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Business interface for managing User entities
 */
@Stateless
public class UserService extends BaseService {

    /** Require 6-20 characters, at least 1 digit and 1 character */
    private static final Pattern PASSWORD_PATTERN = Pattern.compile("((?=.*\\d)(?=.*[a-zA-Z]).{6,20})");

    @Inject
    private Logger log;

    @Resource
    SessionContext ctx;

    @Inject
    private MailService mailService;

    @Inject
    private TemplateService templateService;

    @Inject
    private JbossJaasCacheFlusher jbossJaasCacheFlusher;

    /**
     * Looks up the {@code User} with the given id and preloads the roles
     *
     * @param id the id
     * @return the user or null
     */
    public User findById(Integer id) {
        try {
            User user = em.createNamedQuery("User.findById", User.class).setParameter("id", id).getSingleResult();
            user.preload();
            return user;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Looks up the {@code User} with the given email address and pre-loads the roles
     *
     * @param email the email
     * @return the user or null
     */
    public User findByEmail(String email) {
        try {
            User user = em.createNamedQuery("User.findByEmail", User.class).setParameter("email", email)
                    .getSingleResult();
            user.preload();
            return user;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Looks up the {@code User} based on the given principal
     *
     * @param principal the principal
     * @return the user or null
     */
    public User findByPrincipal(Principal principal) {
        // Throughout MSI-NM, the email is used as the Principal name
        return findByEmail(principal.getName());
    }

    /**
     * Searches for users matching the given term
     * @param term the search term
     * @param limit the maximum number of results
     * @return the search result
     */
    public List<UserVo> searchUsers(String term, int limit) {
        List<UserVo> result = new ArrayList<>();
        if (StringUtils.isNotBlank(term)) {
            List<User> users = em.createNamedQuery("User.searchUsers", User.class)
                    .setParameter("term", "%" + term + "%").setParameter("sort", term).setMaxResults(limit)
                    .getResultList();

            users.forEach(user -> result.add(new UserVo(user)));
        }
        return result;
    }

    /**
     * Returns the current caller or null if none is defined
     * @return the current caller or null if none is defined
     */
    public User getCurrentUser() {
        if (ctx != null && ctx.getCallerPrincipal() != null) {
            return findByPrincipal(ctx.getCallerPrincipal());
        }
        return null;
    }

    /**
     * Updates the current user.
     *
     * @param user the template user entity
     * @return the updated user
     */
    public User updateCurrentUser(User user) throws Exception {
        User existingUser = getCurrentUser();

        if (existingUser == null || !existingUser.getEmail().equalsIgnoreCase(user.getEmail())) {
            throw new IllegalArgumentException("Invalid user " + user.getEmail());
        }

        // Update the existing user
        existingUser.setFirstName(user.getFirstName());
        existingUser.setLastName(user.getLastName());
        existingUser.setLanguage(user.getLanguage());
        existingUser.setVesselName(user.getVesselName());
        existingUser.setMmsi(user.getMmsi());

        // And save the user
        existingUser = saveEntity(existingUser);

        return existingUser;
    }

    /**
     * Finds the role with the given name
     *
     * @param name the name of the role
     * @return the role or null if not found
     */
    public Role findRoleByName(String name) {
        try {
            return em.createNamedQuery("Role.findByName", Role.class).setParameter("name", name).getSingleResult();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * When creating a new user, check the roles assigned to the user.
     * <p>
     * A new user can always get the "user" role, e.g.via self-registration
     * on the website.
     * <p>
     * When an editor or administrator updates a user, they can only assign
     * roles they hold themselves.
     *
     * @param roles the roles to check
     */
    private void validateRoleAssignment(String... roles) {

        // The "user" role can always be assigned
        if (roles.length == 1 && roles[0].equals("user")) {
            return;
        }

        // All other role assignments require a calling user with compatible roles
        User caller = findByPrincipal(ctx.getCallerPrincipal());
        if (caller == null) {
            throw new SecurityException("Invalid caller " + ctx.getCallerPrincipal());
        }
        Set<String> callerRoles = caller.getRoles().stream().map(Role::getName).collect(Collectors.toSet());
        for (String role : roles) {
            if (!callerRoles.contains(role)) {
                throw new SecurityException(
                        "Calling user " + ctx.getCallerPrincipal() + " cannot assign role " + role);
            }
        }
    }

    /**
     * Validates the strength of the password
     * @param password the password to validate
     * @return if the password is valid or not
     */
    public boolean validatePasswordStrength(String password) {
        return password != null && PASSWORD_PATTERN.matcher(password).matches();
    }

    /**
     * Called when a person registers a new user via the website.
     * The user will automatically get the "user" role.
     *
     * @param user the template user entity
     * @return the updated user
     */
    public User registerUser(User user) throws Exception {
        return registerUserWithRoles(user, true, "user");
    }

    /**
     * Called when a user has logged in via OAuth and did not exist in advance.
     * No activation email is sent.
     *
     * @param user the template user entity
     * @return the updated user
     */
    public User registerOAuthOnlyUser(User user) throws Exception {
        return registerUserWithRoles(user, false, "user");
    }

    /**
     * Registers a new user with the given roles.
     * Sends an activation email to the user.
     *
     * @param user the template user entity
     * @param sendEmail whether to send activation email or not
     * @param roles the list of roles to assign the user
     * @return the updated user
     */
    private User registerUserWithRoles(User user, boolean sendEmail, String... roles) throws Exception {
        // Validate that the email address is not already registered
        if (findByEmail(user.getEmail()) != null) {
            throw new Exception("Email " + user.getEmail() + " is already registered");
        }

        // Validate the role assignment
        validateRoleAssignment(roles);

        // Associate the user with the roles
        user.getRoles().clear();
        for (String role : roles) {
            user.getRoles().add(findRoleByName(role));
        }

        // Set a reset-password token
        if (sendEmail) {
            user.setResetPasswordToken(UUID.randomUUID().toString());
        }

        // Persist the user
        user = saveEntity(user);

        // Send registration email
        if (sendEmail) {
            Map<String, Object> data = new HashMap<>();
            data.put("token", user.getResetPasswordToken());
            data.put("name", user.getName());
            data.put("email", user.getEmail());

            sendEmail(data, "user-activation.ftl", "user.registration.subject", user);
        }

        return user;
    }

    /**
     * Called when an administrator creates or edits a user.
     *
     * @param user the template user entity
     * @param roles the list of roles to assign the user
     * @param activationEmail whether to send an activation email for new users or not
     * @return the updated user
     */
    public User createOrUpdateUser(User user, String[] roles, boolean activationEmail) throws Exception {
        // Check if the user is already registered
        User existnigUser = findByEmail(user.getEmail());

        if (existnigUser == null) {
            // Create a new user
            existnigUser = registerUserWithRoles(user, activationEmail, roles);

        } else {

            // Validate the role assignment
            validateRoleAssignment(roles);

            // Update the existing user
            existnigUser.setFirstName(user.getFirstName());
            existnigUser.setLastName(user.getLastName());
            existnigUser.setLanguage(user.getLanguage());
            existnigUser.getRoles().clear();
            for (String role : roles) {
                existnigUser.getRoles().add(findRoleByName(role));
            }

            existnigUser = saveEntity(existnigUser);
        }

        return existnigUser;
    }

    /**
     * First step of setting a new password.
     * A reset-password token is generated and an email sent to the user
     * with a link to reset the password.
     * @param email the email address
     */
    public void resetPassword(String email) throws Exception {
        User user = findByEmail(email);
        if (user == null) {
            throw new Exception("Invalid email " + email);
        }

        user.setResetPasswordToken(UUID.randomUUID().toString());
        saveEntity(user);

        // Send reset-password email
        Map<String, Object> data = new HashMap<>();
        data.put("token", user.getResetPasswordToken());
        data.put("name", user.getName());
        data.put("email", user.getEmail());
        sendEmail(data, "reset-password.ftl", "reset.password.subject", user);
    }

    /**
     * Sends an email based on the parameters
     * @param data the email data
     * @param template the Freemarker template
     * @param subjectKey the subject key
     * @param user the recipient user
     */
    private void sendEmail(Map<String, Object> data, String template, String subjectKey, User user)
            throws Exception {
        TemplateContext ctx = templateService.getTemplateContext(TemplateType.MAIL, template, data,
                user.getLanguage(), "Mails");

        String subject = ctx.getBundle().getString(subjectKey);
        String content = templateService.process(ctx);
        String baseUri = (String) ctx.getData().get("baseUri");
        mailService.sendMail(content, subject, baseUri, user.getEmail());
    }

    /**
     * Second step of setting a new password.
     * The used submits the token generated by calling {@code resetPassword()}
     * along with the new password.
     *
     * @param email the email address
     *
     */
    public void updatePassword(String email, String password, String token) throws Exception {
        User user = findByEmail(email);
        if (user == null) {
            throw new Exception("Invalid email " + email);
        }

        if (!token.equals(user.getResetPasswordToken())) {
            throw new Exception("Invalid token " + token);
        }

        // Validate the password strength
        if (!validatePasswordStrength(password)) {
            throw new Exception(
                    "Invalid password. Must be at least 6 characters long and contain letters and digits");
        }

        if (user.getPassword() == null) {
            user.setPassword(new SaltedPasswordHash());
        }

        // E-mail used as salt for now
        user.getPassword().setPassword(password, email);

        // Reset the password token, so the same mail cannot be used again...
        user.setResetPasswordToken(null);

        // Persist the user entity
        saveEntity(user);

        // Flush the jboss JAAS cache
        jbossJaasCacheFlusher.flushJaasCache(email);
    }

}