001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ui.login;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsUser;
032import org.opencms.main.CmsException;
033import org.opencms.main.CmsLog;
034import org.opencms.main.CmsRuntimeException;
035import org.opencms.main.OpenCms;
036import org.opencms.security.CmsSecurityException;
037import org.opencms.security.CmsUserLog;
038import org.opencms.security.I_CmsPasswordHandler;
039import org.opencms.security.I_CmsPasswordSecurityEvaluator;
040import org.opencms.security.I_CmsPasswordSecurityEvaluator.SecurityLevel;
041import org.opencms.security.twofactor.CmsSecondFactorInfo;
042import org.opencms.security.twofactor.CmsTwoFactorAuthenticationHandler;
043import org.opencms.ui.A_CmsUI;
044import org.opencms.ui.CmsVaadinUtils;
045import org.opencms.ui.I_CmsDialogContext;
046import org.opencms.ui.Messages;
047import org.opencms.ui.components.CmsBasicDialog;
048import org.opencms.ui.components.CmsOkCancelActionHandler;
049import org.opencms.ui.components.OpenCmsTheme;
050import org.opencms.util.CmsStringUtil;
051import org.opencms.util.CmsUUID;
052import org.opencms.workplace.CmsWorkplaceLoginHandler;
053
054import java.util.Collections;
055import java.util.Locale;
056import java.util.function.Consumer;
057
058import org.apache.commons.logging.Log;
059
060import com.vaadin.server.UserError;
061import com.vaadin.ui.Button;
062import com.vaadin.ui.Button.ClickEvent;
063import com.vaadin.ui.Button.ClickListener;
064import com.vaadin.ui.UI;
065import com.vaadin.ui.Window;
066import com.vaadin.v7.data.Property.ValueChangeEvent;
067import com.vaadin.v7.data.Property.ValueChangeListener;
068
069/**
070 * Dialog used to change the password.<p>
071 */
072public class CmsChangePasswordDialog extends CmsBasicDialog {
073
074    /** Logger instance for this class. */
075    private static final Log LOG = CmsLog.getLog(CmsChangePasswordDialog.class);
076
077    /** Serial version id. */
078    private static final long serialVersionUID = 1L;
079
080    /** The password form. */
081    protected CmsPasswordForm m_form;
082
083    /** The CMS context. */
084    CmsObject m_cms;
085
086    /** The locale. */
087    Locale m_locale;
088
089    /** The user. */
090    CmsUser m_user;
091
092    /** The cancel button. */
093    private Button m_cancelButton;
094
095    /** The dialog context. */
096    private I_CmsDialogContext m_context;
097
098    /** The button to change the password. */
099    private Button m_passwordChangeButton;
100
101    /**
102     * Creates a new instance.<p>
103     *
104     * @param cms the CMS context
105     * @param user the user
106     * @param locale the locale
107     */
108    public CmsChangePasswordDialog(CmsObject cms, CmsUser user, Locale locale) {
109
110        super();
111        m_locale = locale;
112        m_cms = cms;
113        m_user = user;
114        if (m_user.isManaged()) {
115            throw new CmsRuntimeException(
116                Messages.get().container(Messages.ERR_USER_NOT_SELF_MANAGED_1, m_user.getName()));
117        }
118
119        m_form = new CmsPasswordForm(m_locale);
120        setContent(m_form);
121        if (OpenCms.getPasswordHandler() instanceof I_CmsPasswordSecurityEvaluator) {
122            m_form.setSecurityHint(
123                ((I_CmsPasswordSecurityEvaluator)OpenCms.getPasswordHandler()).getPasswordSecurityHint(m_locale));
124        }
125
126        m_passwordChangeButton = new Button(CmsVaadinUtils.getMessageText(Messages.GUI_CHANGE_PASSWORD_BUTTON_0));
127        addButton(m_passwordChangeButton);
128        m_passwordChangeButton.addClickListener(new ClickListener() {
129
130            /** Serial versino id. */
131            private static final long serialVersionUID = 1L;
132
133            public void buttonClick(ClickEvent event) {
134
135                submit();
136            }
137        });
138
139        m_cancelButton = new Button(CmsVaadinUtils.messageCancel());
140        addButton(m_cancelButton);
141        m_cancelButton.addClickListener(new ClickListener() {
142
143            /** Serial versino id. */
144            private static final long serialVersionUID = 1L;
145
146            public void buttonClick(ClickEvent event) {
147
148                close();
149            }
150        });
151        m_cancelButton.setVisible(false);
152        m_form.getOldPasswordField().setImmediate(true);
153
154        m_form.getOldPasswordField().addValueChangeListener(new ValueChangeListener() {
155
156            private static final long serialVersionUID = 1L;
157
158            public void valueChange(ValueChangeEvent event) {
159
160                clearOldPasswordError();
161            }
162        });
163
164        m_form.getPassword1Field().addValueChangeListener(event -> {
165            checkSecurity(event.getValue());
166        });
167
168        m_form.getPassword2Field().addValueChangeListener(event -> {
169            checkPasswordMatch(event.getValue());
170        });
171    }
172
173    /**
174     * Creates a new instance.<p>
175     *
176     * @param context the dialog context
177     */
178    public CmsChangePasswordDialog(I_CmsDialogContext context) {
179
180        this(context.getCms(), context.getCms().getRequestContext().getCurrentUser(), UI.getCurrent().getLocale());
181        m_context = context;
182        m_cancelButton.setVisible(true);
183        setActionHandler(new CmsOkCancelActionHandler() {
184
185            private static final long serialVersionUID = 1L;
186
187            @Override
188            protected void cancel() {
189
190                close();
191            }
192
193            @Override
194            protected void ok() {
195
196                submit();
197            }
198        });
199    }
200
201    /**
202     * Displays some additional text.<p>
203     *
204     * @param text the text to display
205     */
206    public void setAdditionalMessage(String text) {
207
208        m_form.setAdditionalText(text);
209    }
210
211    /**
212     * Opens the 2FA verification code dialog for the user if necessary, and passes the code obtained from the dialog
213     * to the handler object passed as a parameter.
214     *
215     *  <p>If the user doesn't need 2FA, no dialog is opened and null is passed to the handler.
216     *
217     * @param handler the handler to call with the verification code data
218     */
219    protected void maybeCheckSecondFactor(Consumer<CmsSecondFactorInfo> handler) {
220
221        CmsTwoFactorAuthenticationHandler twoFactorHandler = OpenCms.getTwoFactorAuthenticationHandler();
222        boolean needToCheck = twoFactorHandler.needsTwoFactorAuthentication(m_user)
223            && twoFactorHandler.hasSecondFactor(m_user);
224
225        if (!needToCheck) {
226            handler.accept(null);
227        } else {
228            Window window = CmsBasicDialog.prepareWindow(DialogWidth.narrow);
229            window.setModal(true);
230            window.setResizable(false);
231            window.setCaption(CmsSecondFactorDialog.getCaption(m_user));
232            CmsSecondFactorDialog dialog = new CmsSecondFactorDialog(m_user, code -> {
233                CmsSecondFactorInfo info = new CmsSecondFactorInfo(code);
234                handler.accept(info);
235            });
236            A_CmsUI.get().addWindow(window);
237            window.setContent(dialog);
238        }
239    }
240
241    /**
242     * Checks whether the passwords match.<p>
243     *
244     * @param password2 the password 2 value
245     */
246    void checkPasswordMatch(String password2) {
247
248        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(password2)) {
249            showPasswordMatchError(!password2.equals(m_form.getPassword1()));
250        }
251    }
252
253    /**
254     * Checks the security level of the given password.<p>
255     *
256     * @param password the password
257     */
258    void checkSecurity(String password) {
259
260        I_CmsPasswordHandler handler = OpenCms.getPasswordHandler();
261        try {
262            handler.validatePassword(password);
263            if (handler instanceof I_CmsPasswordSecurityEvaluator) {
264                SecurityLevel level = ((I_CmsPasswordSecurityEvaluator)handler).evaluatePasswordSecurity(password);
265                m_form.setErrorPassword1(null, OpenCmsTheme.SECURITY + "-" + level.name());
266            } else {
267                m_form.setErrorPassword1(null, OpenCmsTheme.SECURITY_STRONG);
268            }
269        } catch (CmsSecurityException e) {
270            m_form.setErrorPassword1(new UserError(e.getLocalizedMessage(m_locale)), OpenCmsTheme.SECURITY_INVALID);
271        }
272        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(m_form.getPassword2())) {
273            showPasswordMatchError(!password.equals(m_form.getPassword2()));
274        }
275    }
276
277    /**
278     * Clears the wrong old password error.<p>
279     */
280    void clearOldPasswordError() {
281
282        m_form.setErrorOldPassword(null, null);
283    }
284
285    /**
286     * Closes the dialog.<p>
287     */
288    void close() {
289
290        if (m_context != null) {
291            m_context.finish(Collections.<CmsUUID> emptyList());
292        }
293    }
294
295    /**
296     * Shows or hides the not matching passwords error.<p>
297     *
298     * @param show <code>true</code> to show the error
299     */
300    void showPasswordMatchError(boolean show) {
301
302        if (show) {
303            m_form.setErrorPassword2(
304                new UserError(Messages.get().getBundle(m_locale).key(Messages.GUI_PWCHANGE_PASSWORD_MISMATCH_0)),
305                OpenCmsTheme.SECURITY_INVALID);
306        } else {
307            m_form.setErrorPassword2(null, OpenCmsTheme.SECURITY_STRONG);
308        }
309    }
310
311    /**
312     * Submits the password change.<p>
313     */
314    void submit() {
315
316        String password1 = m_form.getPassword1();
317        String password2 = m_form.getPassword2();
318
319        if (validatePasswords(password1, password2)) {
320            String oldPassword = m_form.getOldPassword();
321            if (oldPassword.equals(password1)) {
322                m_form.setErrorPassword1(
323                    new UserError(
324                        Messages.get().getBundle(m_locale).key(Messages.GUI_PWCHANGE_DIFFERENT_PASSWORD_REQUIRED_0)),
325                    OpenCmsTheme.SECURITY_INVALID);
326            } else {
327                maybeCheckSecondFactor((CmsSecondFactorInfo secondFactor) -> {
328
329                    try {
330                        m_cms.setPassword(m_user.getName(), oldPassword, secondFactor, password1);
331                        CmsUserLog.logPasswordChangeForRequestedReset(A_CmsUI.getCmsObject(), m_user.getName());
332                        closeOrRedirectToLoginForm();
333                    } catch (CmsException e) {
334                        m_form.setErrorOldPassword(
335                            new UserError(e.getLocalizedMessage(m_locale)),
336                            OpenCmsTheme.SECURITY_INVALID);
337                        LOG.debug(e.getLocalizedMessage(), e);
338                    }
339                });
340            }
341        }
342    }
343
344    /**
345     * Validates the passwords, checking if they match and fulfill the requirements of the password handler.<p>
346     * Will show the appropriate errors if necessary.<p>
347     *
348     * @param password1 password 1
349     * @param password2 password 2
350     *
351     * @return <code>true</code> if valid
352     */
353    boolean validatePasswords(String password1, String password2) {
354
355        if (!password1.equals(password2)) {
356            showPasswordMatchError(true);
357            return false;
358        }
359        showPasswordMatchError(false);
360        try {
361            OpenCms.getPasswordHandler().validatePassword(password1);
362            m_form.getPassword1Field().setComponentError(null);
363            return true;
364        } catch (CmsException e) {
365            m_form.setErrorPassword1(new UserError(e.getLocalizedMessage(m_locale)), OpenCmsTheme.SECURITY_INVALID);
366            return false;
367        }
368    }
369
370    /**
371     * Closes the dialog, or redirects to the login form, depending on context.
372     */
373    private void closeOrRedirectToLoginForm() {
374
375        if (m_context != null) {
376            close();
377        } else {
378            // this will be the case for forced password changes after login
379            CmsVaadinUtils.showAlert(
380                Messages.get().getBundle(m_locale).key(Messages.GUI_PWCHANGE_SUCCESS_HEADER_0),
381                Messages.get().getBundle(m_locale).key(Messages.GUI_PWCHANGE_GUI_PWCHANGE_SUCCESS_CONTENT_0),
382                new Runnable() {
383
384                    public void run() {
385
386                        A_CmsUI.get().getPage().setLocation(
387                            OpenCms.getLinkManager().substituteLinkForUnknownTarget(
388                                CmsLoginUI.m_adminCms,
389                                CmsWorkplaceLoginHandler.LOGIN_HANDLER
390                                    + "?ocUname="
391                                    + m_user.getSimpleName()
392                                    + "&ocOuFqn="
393                                    + m_user.getOuFqn(),
394                                false));
395                    }
396
397                });
398        }
399    }
400}