AdminLogonController.java

package sk.iway.iwcm.logon;

import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.struts.action.ActionMessage;
import org.apache.struts.action.ActionMessages;
import org.apache.struts.util.ResponseUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import sk.iway.Password;
import sk.iway.iwcm.Adminlog;
import sk.iway.iwcm.Constants;
import sk.iway.iwcm.Identity;
import sk.iway.iwcm.InitServlet;
import sk.iway.iwcm.Logger;
import sk.iway.iwcm.PageLng;
import sk.iway.iwcm.Tools;
import sk.iway.iwcm.common.LogonTools;
import sk.iway.iwcm.components.users.userdetail.UserDetailsService;
import sk.iway.iwcm.components.users.userdetail.UserDetailsRepository;
import sk.iway.iwcm.database.SimpleQuery;
import sk.iway.iwcm.doc.DocDB;
import sk.iway.iwcm.doc.DocDetails;
import sk.iway.iwcm.doc.GroupDetails;
import sk.iway.iwcm.doc.GroupsDB;
import sk.iway.iwcm.i18n.Prop;
import sk.iway.iwcm.stat.StatDB;
import sk.iway.iwcm.system.googleauth.GoogleAuthenticator;
import sk.iway.iwcm.system.googleauth.GoogleAuthenticatorKey;
import sk.iway.iwcm.system.googleauth.GoogleAuthenticatorQRGenerator;
import sk.iway.iwcm.system.ntlm.AuthenticationFilter;
import sk.iway.iwcm.system.spring.SpringUrlMapping;
import sk.iway.iwcm.users.PasswordSecurity;
import sk.iway.iwcm.users.UserChangePasswordService;
import sk.iway.iwcm.users.UsersDB;

/**
 * LogonController.java
 *
 * Class LogonController is used for
 *
 *
 * Title        webjet8
 * Company      Interway a.s. (www.interway.sk)
 * Copyright    Interway a.s. (c) 2001-2018
 * @author      $Author: mhruby $
 * @version     $Revision: 1.0 $
 * created      14.9.2018 10:55
 * modified     14.9.2018 10:55
 */

@Controller
@RequestMapping("/admin/")
public class AdminLogonController {

    private static final String LOGON_FORM = "/admin/skins/webjet8/logon-spring";
    private static final String CHANGE_PASSWORD_FORM = "/admin/skins/webjet8/logon-spring-change-password";
    private static final String TWOFA_PASSWORD_FORM = "/admin/skins/webjet8/logon-spring-2fa";
    private static final String LICENSE = "/wjerrorpages/setup/license";

    private final UserDetailsRepository userDetailsRepository;

    @Autowired
    public AdminLogonController(UserDetailsRepository userDetailsRepository) {
        this.userDetailsRepository = userDetailsRepository;
    }

    /**
     * This method is used from email link to change password
     * @param request
     * @param session
     * @return
     */
    @GetMapping("logon/changePassword")
    public String showChangePasswordForm(ModelMap model, HttpServletRequest request, HttpSession session) {
        UserForm userForm = UserChangePasswordService.getPreparedUserForm(request, model);

        if(userForm == null) {
            model.addAttribute("userForm", new UserForm());
            return LOGON_FORM;
        }

        model.addAttribute("userForm", userForm);
        return CHANGE_PASSWORD_FORM;
    }

    @PostMapping("logon/changePassword")
    public String edit(@ModelAttribute("userForm") UserForm userForm, ModelMap model, HttpServletRequest request, HttpServletResponse response, HttpSession session) {

        Identity user = null;
        String selectedLoginFromSelect = userForm.getSelectedLogin();
        String changePasswordAuth = userForm.getAuth();
        ActionMessages errors = new ActionMessages();

        // This is special
        //  -> can contain only 1 login value when reseting password via login
        //  -> when reseting password via email, it can contain multiple login's separeted by UserChangePasswordService.LOGINS_SEPARATOR
        String login = userForm.getLogin();

        if (Tools.isNotEmpty(changePasswordAuth)) {
            // Verify selected login and in one shoot it will verify changePasswordAuth
            if(UserChangePasswordService.verifyLoginValue(login, selectedLoginFromSelect, changePasswordAuth, request) == true)
                user = new Identity( UsersDB.getUser(selectedLoginFromSelect) );
        } else {
            user = (Identity)session.getAttribute(Constants.USER_KEY+"_changepassword");
        }

        if (user == null) {
            // zaskodnik tu nema co robit
            return LOGON_FORM;
        }
        Prop prop = Prop.getInstance(request.getServletContext(), request);

        model.addAttribute("userForm", userForm);

        // je tam daco a je to rovnake?
        if(Tools.isEmpty(userForm.getNewPassword()) || Tools.isEmpty(userForm.getRetypeNewPassword()) || !(userForm.getNewPassword().equals(userForm.getRetypeNewPassword()))) {
            errors.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("errors", prop.getText("logon.change_password.password_not_match")));
            model.addAttribute("errorsList", errors.get(ActionMessages.GLOBAL_MESSAGE));
            return CHANGE_PASSWORD_FORM;
        }

        String currentPassword = userDetailsRepository.getPasswordByUserId((long)user.getUserId());
        if (Constants.getBoolean("passwordUseHash") && currentPassword.equals(PasswordSecurity.calculateHash(userForm.getNewPassword(), userDetailsRepository.getPasswordSaltByUserId((long)user.getUserId()))) || currentPassword.equals(userForm.getNewPassword())) {
            // povodne heslo je rovnake ako nove heslo
            errors.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("errors", prop.getText("logon.change_password.old_password_match_new")));
            model.addAttribute("errorsList", errors.get(ActionMessages.GLOBAL_MESSAGE));
            return CHANGE_PASSWORD_FORM;
        } else if (Password.checkPassword(true, userForm.getNewPassword(), true, user.getUserId(), session, errors)){
            user.setPassword(userForm.getNewPassword());
            UserDetailsService.savePassword(userForm.getNewPassword(), user.getUserId());

            Adminlog.add(Adminlog.TYPE_USER_CHANGE_PASSWORD, user.getUserId(), "UsrLogonAction - user (" + user.getLogin() + ") successfully changed password", -1, -1);

            //During password change action, user is obtained via login, not via session
            if(Tools.isEmpty(changePasswordAuth) == true) {
                session.removeAttribute(Constants.USER_KEY+"_changepassword");
                LogonTools.setUserToSession(session, user);
            }

            this.determineLanguage(session, request, response);
            this.determineDefaultWebPagesDirectory(user, session);
            this.checkForNewHelp(session, user);
            this.determineRootWebPageDirectory(session, user);

            if (Tools.isNotEmpty(changePasswordAuth)) {
                // Delete admin log - so change password action will no longer be available
                UserChangePasswordService.deleteChangePasswordAdminlogBean(login, changePasswordAuth);

                // Redirect to login page where they can test new password
                // - with changePasswordActionSuccess parameter
                return "redirect:/admin/logon/?act=changePasswordActionSuccess";
            } else {
                String forwardAfterToken = (String)session.getAttribute("adminAfterLogonRedirect");
                if (Tools.isEmpty(forwardAfterToken))
                    return "redirect:/admin/v9/";
                return "redirect:" + forwardAfterToken;
            }
        } else {
            if (errors.size()>0) model.addAttribute("errorsList", errors.get(ActionMessages.GLOBAL_MESSAGE));
            return CHANGE_PASSWORD_FORM;
        }
    }

    @GetMapping("logon.struts")
    @PostMapping("logon.struts")
    //presmerovanie stareho Struts volania /admin/logon.do na novy form
    public String logonDoRedirect() {
        return "redirect:/admin/logon/";
    }

    @GetMapping("logon/")
    public String showForm(UserForm userForm, HttpServletRequest request, HttpSession session)
    {
        Identity user = UsersDB.getCurrentUser(session);
        if (user != null && user.isAdmin())
        {
            //user je uz prihlaseny, preforwardnime ho na /admin/
            return "redirect:/admin/v9/";
        }

        //--------------Prihlasenie prebieha cez NTLM, nema sa co prihlasovat cez formularik-----------
        if (AuthenticationFilter.weTrustIIS())
        {
            return "redirect:"+ AuthenticationFilter.getForbiddenURL();
        }

        if  (request.getParameter("language")==null && request.getSession().getAttribute(Prop.SESSION_I18N_PROP_LNG)==null && Tools.isNotEmpty(Constants.getString("defaultLanguage")))
        {
            request.getSession().setAttribute(Prop.SESSION_I18N_PROP_LNG, Constants.getString("defaultLanguage"));
        }

        String adminHost = Constants.getString("multiDomainAdminHost");
//out.println("adminHost="+adminHost+" domain="+DocDB.getDomain(request));

        String serverName = Tools.getServerName(request);
        if (("iwcm.interway.sk".equals(request.getServerName())==false && "localhost".equals(request.getServerName())==false) && Tools.isNotEmpty(adminHost) && (","+adminHost+",").indexOf(","+serverName+",")==-1)
        {
            if (Constants.getBoolean("adminLogonShowSimpleErrorMessage"))
            {
                return "/404.jsp";
            }
            else
            {
                String[] hosts = Tools.getTokens(adminHost, ",");
                return "redirect:"+request.getScheme()+"://"+hosts[0]+"/admin/";
            }
        }

        if (Tools.isNotEmpty(Constants.getString("defaultLanguage"))) userForm.setLanguage(Constants.getString("defaultLanguage"));

        String language = request.getParameter("language");
        if (Tools.isNotEmpty(language) && language.length()==2)
        {
            userForm.setLanguage(language);
        }

        LogonTools.saveAfterLogonRedirect(request);

        if(request.getParameter("loginName") != null)
        {
            String loginName = request.getParameter("loginName");
            request.setAttribute(UsersDB.IS_ADMIN_SECTION_KEY, true);
            UserChangePasswordService.sendPassword(request,loginName);
        }

        return LOGON_FORM;
    }

    @PostMapping("logon/")
    public String submit(@ModelAttribute("userForm") UserForm userForm, BindingResult result, ModelMap model, HttpServletRequest request, HttpServletResponse response) {
        if (InitServlet.verify(request) == false)
            return SpringUrlMapping.redirect(LICENSE);

        //session fixation ochrana
        LogonTools.invalidateSessionOnFirstPost(request);
        HttpSession session = request.getSession();

        //fix: after passwordExpiryDay notifycation it was possible to go to logon and skip verification
        session.removeAttribute(Constants.USER_KEY+"_changepassword");

        Prop prop = Prop.getInstance(request.getServletContext(), request);

        String twoFaRedirect = verify2FaKey(request);
        if (Tools.isNotEmpty(twoFaRedirect)) return twoFaRedirect;

        Identity user = new Identity();
        Map<String, String> errors = new Hashtable<>();
        LogonTools.logon(userForm.getUsername(), userForm.getPassword(), user, errors, request, prop);

        if (errors.get("ERROR_KEY")!=null) {
            Logger.error(this,"su nejake chyby v logovacom formulari");
            model.addAttribute("errors", errors.get("ERROR_KEY"));
            return LOGON_FORM;
        }

        // Save our logged-in userForm in the session
        user.setLoginName(userForm.getUsername());
        user.setValid(true);

        // pouzivatel je prihlaseny
        LogonTools.setUserToSession(session, user);

        if (!(Password.checkPassword(true, userForm.getPassword(), user.isAdmin(), user.getUserId(), session, null))) {
            // ma slabe heslo
            session.removeAttribute(Constants.USER_KEY);
            session.setAttribute(Constants.USER_KEY+"_changepassword", user);
            userForm.setPassword(userForm.getPassword().replace(".", "*")); // bezpecny placeholder
            model.addAttribute("userForm", userForm);
            model.addAttribute("isAdmin", user.isAdmin());
            return CHANGE_PASSWORD_FORM;
        }

		twoFaRedirect = set2FaAuthForm(user, request);
        if (Tools.isNotEmpty(twoFaRedirect)) return twoFaRedirect;


        this.determineLanguage(session, request, response);
        this.determineDefaultWebPagesDirectory(user, session);
        this.checkForNewHelp(session, user);
        this.determineRootWebPageDirectory(session, user);
        StatDB.addAdmin(request);

        String adminAfterLogonRedirect = (String)session.getAttribute("adminAfterLogonRedirect");
        if (Tools.isNotEmpty(adminAfterLogonRedirect)) {
            if (adminAfterLogonRedirect.startsWith("/admin/v9/") || (adminAfterLogonRedirect.startsWith("/apps/") && adminAfterLogonRedirect.contains("/admin/"))) {
                return "redirect:" + adminAfterLogonRedirect;
            }
        }

        return "redirect:/admin/v9/";
    }



    private void determineLanguage(HttpSession session, HttpServletRequest request, HttpServletResponse response) {
        String lng = ResponseUtils.filter(request.getParameter("language"));
        if (Tools.isEmpty(lng)) lng = Constants.getString("defaultLanguage");
        PageLng.setUserLng(request, response, lng);
        session.setAttribute(Prop.SESSION_I18N_PROP_LNG, lng);
    }

    private void determineDefaultWebPagesDirectory(Identity user, HttpSession session) {
        //nastav predvoleny adresar na Moje Stranky
        if (user.getEditablePages()!=null && user.getEditablePages().length()>0 && user.isDisabledItem("menuUsers"))
            session.setAttribute(Constants.SESSION_GROUP_ID, Constants.getInt("systemPagesMyPages"));

        //nastav predvoleny adresar Na Schalenie
        DocDB docDB = DocDB.getInstance();
        List<DocDetails> docsToApprove = docDB.getDocsForApprove(user.getUserId());
        if (docsToApprove!=null && docsToApprove.size()>0)
            session.setAttribute(Constants.SESSION_GROUP_ID, Integer.toString(Constants.getInt("systemPagesDocsToApprove")));
    }


    private void determineRootWebPageDirectory(HttpSession session, Identity user) {
        if (Tools.isNotEmpty(user.getEditableGroups())) {
            //prestav v session default host na prvy z editable groups
            int groupId = getUserFirstEditableGroup(user);
            if (groupId > 0)
                setSessionGroup(groupId, session);
        } else if (Constants.getBoolean("enableStaticFilesExternalDir")) {
            int groupId = Constants.getInt("rootGroupId");
            setSessionGroup(groupId, session);
        }
    }

    private void checkForNewHelp(HttpSession session, Identity user) {
        //ziskaj datum posledneho prihlasenia a datum najnovsej novinky
        /*try
        {
            IwcmFile[] newHelpFiles = (new IwcmFile(Tools.getRealPath("/admin/help/sk/new"))).listFiles();
            Arrays.sort(newHelpFiles,
                    new Comparator<IwcmFile>()
                    {
                        @Override
                        public int compare(IwcmFile f1, IwcmFile f2)
                        {
                            if (f1.isDirectory())
                            {
                                //return(-1);
                            }
                            if (f2.isDirectory())
                            {
                                //return(1);
                            }

                            int lm1 = 0;
                            int lm2 = 0;

                            try
                            {
                                if (f1.isFile()) lm1 = Tools.getIntValue(f1.getName().substring(0, f1.getName().indexOf('.')), -1);
                                if (f2.isFile()) lm2 = Tools.getIntValue(f2.getName().substring(0, f2.getName().indexOf('.')), -1);
                            }
                            catch (Exception e)
                            {
                                //ignoruj
                            }

                            //Logger.println(this,"compare: " + lm1 + " " + f1.getName() + " vs " + lm2 + " " + f2.getName());

                            if (lm1 == lm2) return(0);
                            else if (lm1 > lm2) return(-1);
                            else return(1);
                        }
                    });

            //najnovsi je na pozicii 0
            IwcmFile newestHelpFile = newHelpFiles[0];
            //Logger.println(this,"NEWEST: " + newestHelpFile.getName());

            //skontroluj, ci uz videl tento subor
            long lastSeenDate = Adminlog.getLastDate(Adminlog.TYPE_HELP_LAST_SEEN, user.getUserId());

            if (lastSeenDate < newestHelpFile.lastModified())
            {
                //este nevidel, treba mu zobrazit
                session.setAttribute("show_help_file_after_logon", newestHelpFile.getName());
                Adminlog.add(Adminlog.TYPE_HELP_LAST_SEEN, user.getUserId(), "helpfile: " + newestHelpFile.getName(), -1, -1);
            }
        }
        catch (Exception e)
        {
            Logger.error(AdminLogonController.class, e);
        }*/
    }

    /**
     * Nastavi session ID adresara web stranok a preview domenu
     * @param groupId
     * @param session
     */
    private static void setSessionGroup(int groupId, HttpSession session)
    {
        session.setAttribute(Constants.SESSION_GROUP_ID, String.valueOf(groupId));

        GroupDetails root = GroupsDB.getInstance().getGroup(groupId);
        if (root != null && Tools.isNotEmpty(root.getDomainName()))
        {
            session.setAttribute("preview.editorDomainName", root.getDomainName());
        }
    }

    /**
     * Overi odoslanu hodnotu 2FA cisla, ak je nespravna vrati linku na formular, inak presmerovanie do administracie
     * Vrati NULL ak 2FA nie je aktivovana
     * @param request
     * @return
     */
    private static String verify2FaKey(HttpServletRequest request) {
        HttpSession session = request.getSession();
        if (session.getAttribute("token")!=null)
		{
			//ocakava sa token
			String generatedToken = (String)session.getAttribute("token");

			try{

				String insertedCodeString = request.getParameter("token");
				int insertedCode = Integer.parseInt(insertedCodeString);

				GoogleAuthenticator gAuth = new GoogleAuthenticator();

				int generatedCode = gAuth.getTotpPassword(generatedToken);

				Logger.debug(AdminLogonController.class,"userToken : " + insertedCode + "\n token : "+gAuth.getTotpPassword(generatedToken)+ "\n code : "+generatedCode );

				if (insertedCode == generatedCode)
				{
                    String token = (String)session.getAttribute("token");
					session.removeAttribute("token");
					Identity sessionUserAfterToken = (Identity)session.getAttribute("adminUser_waitingForToken");
					session.removeAttribute("adminUser_waitingForToken");
					if (sessionUserAfterToken!=null) {
                        LogonTools.setUserToSession(session, sessionUserAfterToken);

                        //set user code
                        String currentCode = new SimpleQuery().forString("SELECT mobile_device FROM users WHERE user_id = ?", sessionUserAfterToken.getUserId());
                        if (Tools.isNotEmpty(token) && Tools.isEmpty(currentCode)) {
                            new SimpleQuery().execute("UPDATE users SET mobile_device = ? WHERE user_id = ?", token, sessionUserAfterToken.getUserId());
                            sessionUserAfterToken.setMobileDevice(currentCode);
                        }
                    }

					return "redirect:/admin/v9/";
				}

			}catch (NumberFormatException e){
                //asi nebolo zadane cislo, cize kod je zly
			}
			//ak nie znova vrat logon
			request.setAttribute("errors", "wrongCode");
			//wrongCode
			return TWOFA_PASSWORD_FORM;
		}
        return null;
    }

    /**
     * Overi ci je zapnute 2FA, ak ano, vrati linku na formular
     * @param user
     * @param session
     * @return
     */
    private String set2FaAuthForm(Identity user, HttpServletRequest request) {

        String mobileDevice = userDetailsRepository.getMobileDeviceByUserId((long)user.getUserId());

        //ak je aktivovana dvojfaktorova autentifikacia a user ma nastaveny devicekey
        if (Tools.isNotEmpty(mobileDevice) || Constants.getBoolean("isGoogleAuthRequiredForAdmin") ) {
            HttpSession session = request.getSession();

            if (Tools.isEmpty(mobileDevice) || mobileDevice.length()<5) {  // - je forced gauth ^ cfg premennou
                GoogleAuthenticator gAuth = new GoogleAuthenticator();
                final GoogleAuthenticatorKey key = gAuth.createCredentials();
                session.setAttribute("token", key.getKey());	// hodime si do session novo vygenerovane credentials
                session.setAttribute("QRURL", GoogleAuthenticatorQRGenerator.getOtpAuthURL("WebJET " + Constants.getInstallName() + " (" + Tools.getServerName(request) + ")", user.getLogin(), key));
                session.setAttribute("scratchcode", key.getScratchCodes().get(0).toString());
            }else{
                session.setAttribute("token", mobileDevice);
            }

            // Google Authenticator
            //String token = RandomStringUtils.random(4, false, true);
            //sendToken(mobileDevice, token);
            session.setAttribute("adminUser_waitingForToken", user);
            session.removeAttribute(Constants.USER_KEY);
            //Logger.debug(LogonAction.class, "LogonAction dualFactorToken: "+mobileDevice);
            // zobraz naspat admin
            return TWOFA_PASSWORD_FORM;

        }

        return null;
    }

    public static int getUserFirstEditableGroup(Identity user)
	{
		int[] editableGroups = Tools.getTokensInt(user.getEditableGroups(), ",");
		if (editableGroups!=null && editableGroups.length>0)
		{
			for (int groupId : editableGroups)
			{
				if (groupId > 0)
				{
					return groupId;
				}
			}
		}
		return -1;
	}
}