001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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 GmbH & Co. KG, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://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.db;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsUser;
032import org.opencms.main.CmsLog;
033import org.opencms.main.OpenCms;
034import org.opencms.security.CmsAuthentificationException;
035import org.opencms.security.CmsRole;
036import org.opencms.security.CmsRoleViolationException;
037import org.opencms.security.CmsUserDisabledException;
038import org.opencms.security.I_CmsCustomLogin;
039import org.opencms.security.Messages;
040import org.opencms.util.CmsStringUtil;
041
042import java.util.Date;
043import java.util.HashSet;
044import java.util.Hashtable;
045import java.util.Map;
046import java.util.Set;
047
048import org.apache.commons.logging.Log;
049
050/**
051 * Provides functions used to check the validity of a user login.<p>
052 *
053 * Stores invalid login attempts and disables a user account temporarily in case
054 * the configured threshold of invalid logins is reached.<p>
055 *
056 * The invalid login attempt storage operates on a combination of user name, login remote IP address and
057 * user type. This means that a user can be disabled for one remote IP, but still be enabled for
058 * another remote IP.<p>
059 *
060 * Also allows to temporarily disallow logins (for example in case of maintenance work on the system).<p>
061 *
062 * @since 6.0.0
063 */
064public class CmsLoginManager {
065
066    /**
067     * Contains the data stored for each user in the storage for invalid login attempts.<p>
068     */
069    private class CmsUserData {
070
071        /** The start time this account was disabled. */
072        private long m_disableTimeStart;
073
074        /** The count of the failed attempts. */
075        private int m_invalidLoginCount;
076
077        /**
078         * Creates a new user data instance.<p>
079         */
080        protected CmsUserData() {
081
082            // a new instance is creted only if there already was one failed attempt
083            m_invalidLoginCount = 1;
084        }
085
086        /**
087         * Returns the bad attempt count for this user.<p>
088         *
089         * @return the bad attempt count for this user
090         */
091        protected Integer getInvalidLoginCount() {
092
093            return Integer.valueOf(m_invalidLoginCount);
094        }
095
096        /**
097         * Returns the date this disabled user is released again.<p>
098         *
099         * @return the date this disabled user is released again
100         */
101        protected Date getReleaseDate() {
102
103            return new Date(m_disableTimeStart + m_disableMillis + 1);
104        }
105
106        /**
107         * Increases the bad attempt count, disables the data in case the
108         * configured threshold is reached.<p>
109         */
110        protected void increaseInvalidLoginCount() {
111
112            m_invalidLoginCount++;
113            if (m_invalidLoginCount >= m_maxBadAttempts) {
114                // threshold for bad login attempts has been reached for this user
115                if (m_disableTimeStart == 0) {
116                    // only disable in case this user has not already been disabled
117                    m_disableTimeStart = System.currentTimeMillis();
118                }
119            }
120        }
121
122        /**
123         * Returns <code>true</code> in case this user has been temporarily disabled.<p>
124         *
125         * @return <code>true</code> in case this user has been temporarily disabled
126         */
127        protected boolean isDisabled() {
128
129            if (m_disableTimeStart > 0) {
130                // check if the disable time is already over
131                long currentTime = System.currentTimeMillis();
132                if ((currentTime - m_disableTimeStart) > m_disableMillis) {
133                    // disable time is over
134                    m_disableTimeStart = 0;
135                }
136            }
137            return m_disableTimeStart > 0;
138        }
139
140        /**
141         * Reset disable time.<p>
142         */
143        protected void reset() {
144
145            m_disableTimeStart = 0;
146            m_invalidLoginCount = 0;
147        }
148    }
149
150    /** Default token lifetime. */
151    public static final long DEFAULT_TOKEN_LIFETIME = 3600 * 24 * 1000;
152
153    /** Default lock time if treshold for bad login attempts is reached. */
154    public static final int DISABLE_MINUTES_DEFAULT = 15;
155
156    /** Default setting for the security option. */
157    public static final boolean ENABLE_SECURITY_DEFAULT = false;
158
159    /** Separator used for storage keys. */
160    public static final String KEY_SEPARATOR = "_";
161
162    /** Default for bad login attempts. */
163    public static final int MAX_BAD_ATTEMPTS_DEFAULT = 3;
164
165    /**Map holding usernames and userdata for user which are currently locked.*/
166    protected static Map<String, Set<CmsUserData>> TEMP_DISABLED_USER;
167
168    /** The logger instance for this class. */
169    private static final Log LOG = CmsLog.getLog(CmsLoginManager.class);
170
171    /** The milliseconds to disable an account if the threshold is reached. */
172    protected int m_disableMillis;
173
174    /** The minutes to disable an account if the threshold is reached. */
175    protected int m_disableMinutes;
176
177    /** The flag to determine if the security option ahould be enabled on the login dialog. */
178    protected boolean m_enableSecurity;
179
180    /** The number of bad login attempts allowed before an account is temporarily disabled. */
181    protected int m_maxBadAttempts;
182
183    /** The storage for the bad login attempts. */
184    protected Map<String, CmsUserData> m_storage;
185
186    /** The token lifetime. */
187    protected String m_tokenLifetimeStr;
188
189    /** The before login message. */
190    private CmsLoginMessage m_beforeLoginMessage;
191
192    /** The configured custom login. */
193    private I_CmsCustomLogin m_customLogin;
194
195    /** The login message, setting this may also disable logins for non-Admin users. */
196    private CmsLoginMessage m_loginMessage;
197
198    /** The logout URI. */
199    private String m_logoutUri;
200
201    /** Max inactivity time. */
202    private String m_maxInactive;
203
204    /** Password change interval. */
205    private String m_passwordChangeInterval;
206
207    /** Option which determines whether the login dialog should require an organizational unit. */
208    private boolean m_requireOrgUnit;
209
210    /** User data check interval. */
211    private String m_userDateCheckInterval;
212
213    /** If true, forces logout for non-ELEMENT_AUTHOR users when opening /system/login. */
214    private boolean m_forceLogoutForUnprivilegedUsers;
215
216    /**
217     * Creates a new storage for invalid logins.<p>
218     *
219     * @param disableMinutes the minutes to disable an account if the threshold is reached
220     * @param maxBadAttempts the number of bad login attempts allowed before an account is temporarily disabled
221     * @param enableSecurity flag to determine if the security option should be enabled on the login dialog
222     * @param tokenLifetime the lifetime of authorization tokens, i.e. the time for which they are valid
223     * @param maxInactive maximum inactivity time
224     * @param passwordChangeInterval the password change interval
225     * @param userDataCheckInterval the user data check interval
226     * @param requireOrgUnit if true, should require organizational unit selection on login
227     * @param logoutUri the alternative logout handler URI
228     */
229    public CmsLoginManager(
230        int disableMinutes,
231        int maxBadAttempts,
232        boolean enableSecurity,
233        String tokenLifetime,
234        String maxInactive,
235        String passwordChangeInterval,
236        String userDataCheckInterval,
237        boolean requireOrgUnit,
238        String logoutUri,
239        boolean forceLogoutForUnprivilegedUsers) {
240
241        m_maxBadAttempts = maxBadAttempts;
242        if (TEMP_DISABLED_USER == null) {
243            TEMP_DISABLED_USER = new Hashtable<String, Set<CmsUserData>>();
244        }
245        if (m_maxBadAttempts >= 0) {
246            // otherwise the invalid login storage is sisabled
247            m_disableMinutes = disableMinutes;
248            m_disableMillis = disableMinutes * 60 * 1000;
249            m_storage = new Hashtable<String, CmsUserData>();
250
251        }
252        m_enableSecurity = enableSecurity;
253        m_tokenLifetimeStr = tokenLifetime;
254        m_maxInactive = maxInactive;
255        m_passwordChangeInterval = passwordChangeInterval;
256        m_userDateCheckInterval = userDataCheckInterval;
257        m_requireOrgUnit = requireOrgUnit;
258        m_logoutUri = logoutUri;
259        m_forceLogoutForUnprivilegedUsers = forceLogoutForUnprivilegedUsers;
260    }
261
262    /**
263     * Returns the key to use for looking up the user in the invalid login storage.<p>
264     *
265     * @param userName the name of the user
266     * @param remoteAddress the remore address (IP) from which the login attempt was made
267     *
268     * @return the key to use for looking up the user in the invalid login storage
269     */
270    private static String createStorageKey(String userName, String remoteAddress) {
271
272        StringBuffer result = new StringBuffer();
273        result.append(userName);
274        result.append(KEY_SEPARATOR);
275        result.append(remoteAddress);
276        return result.toString();
277    }
278
279    /**
280     * Checks whether a user account can be locked because of inactivity.
281     *
282     * @param cms the CMS context
283     * @param user the user to check
284     * @return true if the user may be locked after being inactive for too long
285     */
286    public boolean canLockBecauseOfInactivity(CmsObject cms, CmsUser user) {
287
288        return !user.isManaged()
289            && !user.isWebuser()
290            && !OpenCms.getDefaultUsers().isDefaultUser(user.getName())
291            && !OpenCms.getRoleManager().hasRole(cms, user.getName(), CmsRole.ROOT_ADMIN);
292    }
293
294    /**
295     * Checks whether the given user has been inactive for longer than the configured limit.<p>
296     *
297     * If no max inactivity time is configured, always returns false.
298     *
299     * @param user the user to check
300     * @return true if the user has been inactive for longer than the configured limit
301     */
302    public boolean checkInactive(CmsUser user) {
303
304        if (m_maxInactive == null) {
305            return false;
306        }
307        try {
308            if (user.getLastlogin() == 0) {
309                return false;
310            }
311            long maxInactive = CmsStringUtil.parseDuration(m_maxInactive, Long.MAX_VALUE);
312            return (System.currentTimeMillis() - user.getLastlogin()) > maxInactive;
313        } catch (Exception e) {
314            LOG.warn(e.getLocalizedMessage(), e);
315            return false;
316        }
317    }
318
319    /**
320     * Checks if the threshold for the invalid logins has been reached for the given user.<p>
321     *
322     * In case the configured threshold is reached, an Exception is thrown.<p>
323     *
324     * @param userName the name of the user
325     * @param remoteAddress the remote address (IP) from which the login attempt was made
326     *
327     * @throws CmsAuthentificationException in case the threshold of invalid login attempts has been reached
328     */
329    public void checkInvalidLogins(String userName, String remoteAddress) throws CmsAuthentificationException {
330
331        if (m_maxBadAttempts < 0) {
332            // invalid login storage is disabled
333            return;
334        }
335
336        String key = createStorageKey(userName, remoteAddress);
337        // look up the user in the storage
338        CmsUserData userData = m_storage.get(key);
339        if ((userData != null) && (userData.isDisabled())) {
340            // threshold of invalid logins is reached
341            Set<CmsUserData> data = TEMP_DISABLED_USER.get(userName);
342            if (data == null) {
343                data = new HashSet<CmsUserData>();
344            }
345            data.add(userData);
346            TEMP_DISABLED_USER.put(userName, data);
347            throw new CmsUserDisabledException(
348                Messages.get().container(
349                    Messages.ERR_LOGIN_FAILED_TEMP_DISABLED_4,
350                    new Object[] {
351                        userName,
352                        remoteAddress,
353                        userData.getReleaseDate(),
354                        userData.getInvalidLoginCount()}));
355        }
356        if (TEMP_DISABLED_USER.containsKey(userName) & (userData != null)) {
357            //User war disabled, but time is over -> remove from list
358            if (TEMP_DISABLED_USER.get(userName).contains(userData)) {
359                TEMP_DISABLED_USER.get(userName).remove(userData);
360                if (TEMP_DISABLED_USER.get(userName).isEmpty()) {
361                    TEMP_DISABLED_USER.remove(userName);
362                }
363            }
364        }
365    }
366
367    /**
368     * Checks if a login is currently allowed.<p>
369     *
370     * In case no logins are allowed, an Exception is thrown.<p>
371     *
372     * @throws CmsAuthentificationException in case no logins are allowed
373     */
374    public void checkLoginAllowed() throws CmsAuthentificationException {
375
376        if ((m_loginMessage != null) && (m_loginMessage.isLoginCurrentlyForbidden())) {
377            // login message has been set and is active
378            throw new CmsAuthentificationException(
379                Messages.get().container(Messages.ERR_LOGIN_FAILED_WITH_MESSAGE_1, m_loginMessage.getMessage()));
380        }
381    }
382
383    /**
384     * Returns the current before login message that is displayed on the login form.<p>
385     *
386     * if <code>null</code> is returned, no login message has been currently set.<p>
387     *
388     * @return  the current login message that is displayed if a user logs in
389     */
390    public CmsLoginMessage getBeforeLoginMessage() {
391
392        return m_beforeLoginMessage;
393    }
394
395    /**
396     * Gets the configured custom login method, if any.
397     *
398     * @return the configured custom login method
399     */
400    public I_CmsCustomLogin getCustomLogin() {
401
402        return m_customLogin;
403    }
404
405    /**
406     * Returns the minutes an account gets disabled after too many failed login attempts.<p>
407     *
408     * @return the minutes an account gets disabled after too many failed login attempts
409     */
410    public int getDisableMinutes() {
411
412        return m_disableMinutes;
413    }
414
415    /**
416     * Returns the current login message that is displayed if a user logs in.<p>
417     *
418     * if <code>null</code> is returned, no login message has been currently set.<p>
419     *
420     * @return  the current login message that is displayed if a user logs in
421     */
422    public CmsLoginMessage getLoginMessage() {
423
424        return m_loginMessage;
425    }
426
427    /**
428     * Gets the logout URI.<p>
429     *
430     * If this is not null, users will be redirected to this JSP when logging out from the workplace
431     * or page editor. The JSP is responsible for invalidating the user's session.
432     *
433     * @return the logout URI
434     */
435    public String getLogoutUri() {
436
437        return m_logoutUri;
438
439    }
440
441    /**
442     * Returns the number of bad login attempts allowed before an account is temporarily disabled.<p>
443     *
444     * @return the number of bad login attempts allowed before an account is temporarily disabled
445     */
446    public int getMaxBadAttempts() {
447
448        return m_maxBadAttempts;
449    }
450
451    /**
452     * Gets the max inactivity time.<p>
453     *
454     * @return the max inactivity time
455     */
456    public String getMaxInactive() {
457
458        return m_maxInactive;
459    }
460
461    /**
462     * Gets the password change interval.<p>
463     *
464     * @return the password change interval
465     */
466    public long getPasswordChangeInterval() {
467
468        if (m_passwordChangeInterval == null) {
469            return Long.MAX_VALUE;
470        } else {
471            return CmsStringUtil.parseDuration(m_passwordChangeInterval, Long.MAX_VALUE);
472        }
473    }
474
475    /**
476     * Gets the raw password change interval string.<p>
477     *
478     * @return the configured string for the password change interval
479     */
480    public String getPasswordChangeIntervalStr() {
481
482        return m_passwordChangeInterval;
483    }
484
485    /**
486     * Gets the authorization token lifetime in milliseconds.<p>
487     *
488     * @return the authorization token lifetime in milliseconds
489     */
490    public long getTokenLifetime() {
491
492        if (m_tokenLifetimeStr == null) {
493            return DEFAULT_TOKEN_LIFETIME;
494        }
495        return CmsStringUtil.parseDuration(m_tokenLifetimeStr, DEFAULT_TOKEN_LIFETIME);
496    }
497
498    /**
499     * Gets the configured token lifetime as a string.<p>
500     *
501     * @return the configured token lifetime as a string
502     */
503    public String getTokenLifetimeStr() {
504
505        return m_tokenLifetimeStr;
506    }
507
508    /**
509     * Gets the user data check interval.<p>
510     *
511     * @return the user data check interval
512     */
513    public long getUserDataCheckInterval() {
514
515        if (m_userDateCheckInterval == null) {
516            return Long.MAX_VALUE;
517        } else {
518            return CmsStringUtil.parseDuration(m_userDateCheckInterval, Long.MAX_VALUE);
519        }
520    }
521
522    /**
523     * Gets the raw user data check interval string.<p>
524     *
525     * @return the configured string for the user data check interval
526     */
527    public String getUserDataCheckIntervalStr() {
528
529        return m_userDateCheckInterval;
530    }
531
532    /**
533     * Returns if the security option ahould be enabled on the login dialog.<p>
534     *
535     * @return <code>true</code> if the security option ahould be enabled on the login dialog, otherwise <code>false</code>
536     */
537    public boolean isEnableSecurity() {
538
539        return m_enableSecurity;
540    }
541
542    /**
543     * Checks if the user should be excluded from password reset.
544     *
545     * @param cms the CmsObject to use
546     * @param user the user to check
547     * @return true if the user should be excluded from password reset
548     */
549    public boolean isExcludedFromPasswordReset(CmsObject cms, CmsUser user) {
550
551        return user.isManaged() || user.isWebuser() || OpenCms.getDefaultUsers().isDefaultUser(user.getName());
552    }
553
554    /**
555     * Checks if non-ELEMENT_AUTHOR users should be logged out when visiting /system/login.
556     *
557     * @return true if unprivileged users should be logged out
558     */
559    public boolean isForceLogoutForUnprivilegedUsers() {
560
561        return m_forceLogoutForUnprivilegedUsers;
562    }
563
564    /**
565     * Returns true if organizational unit selection should be required on login.
566     *
567     * @return true if org unit selection should be required
568     */
569    public boolean isOrgUnitRequired() {
570
571        return m_requireOrgUnit;
572    }
573
574    /**
575     * Checks if password has to be reset.<p>
576     *
577     * @param cms CmsObject
578     * @param user CmsUser
579     * @return true if password should be reset
580     */
581    public boolean isPasswordReset(CmsObject cms, CmsUser user) {
582
583        if (isExcludedFromPasswordReset(cms, user)) {
584            return false;
585        }
586        if (user.getAdditionalInfo().get(CmsUserSettings.ADDITIONAL_INFO_PASSWORD_RESET) != null) {
587            return true;
588        }
589        return false;
590    }
591
592    /**
593     * Checks if a user is locked due to too many failed logins.<p>
594     *
595     * @param user the user to check
596     *
597     * @return true if the user is locked
598     */
599    public boolean isUserLocked(CmsUser user) {
600
601        Set<String> keysForUser = getKeysForUser(user);
602        for (String key : keysForUser) {
603            CmsUserData data = m_storage.get(key);
604            if ((data != null) && data.isDisabled()) {
605                return true;
606            }
607        }
608        return false;
609    }
610
611    /**
612     * Checks if given user it temporarily locked.<p>
613     *
614     * @param username to check
615     * @return true if user is locked
616     */
617    public boolean isUserTempDisabled(String username) {
618
619        Set<CmsUserData> data = TEMP_DISABLED_USER.get(username);
620        if (data == null) {
621            return false;
622        }
623        for (CmsUserData userData : data) {
624            if (!userData.isDisabled()) {
625                data.remove(userData);
626            }
627        }
628        if (data.size() > 0) {
629            TEMP_DISABLED_USER.put(username, data);
630            return true;
631        } else {
632            TEMP_DISABLED_USER.remove(username);
633            return false;
634        }
635    }
636
637    /**
638     * Removes the current login message.<p>
639     *
640     * This operation requires that the current user has role permissions of <code>{@link CmsRole#ROOT_ADMIN}</code>.<p>
641     *
642     * @param cms the current OpenCms user context
643     *
644     * @throws CmsRoleViolationException in case the current user does not have the required role permissions
645     */
646    public void removeLoginMessage(CmsObject cms) throws CmsRoleViolationException {
647
648        OpenCms.getRoleManager().checkRole(cms, CmsRole.ROOT_ADMIN);
649        m_loginMessage = null;
650    }
651
652    /**
653     * Checks if a user is required to change his password now.<p>
654     *
655     * @param cms the current CMS context
656     * @param user the user to check
657     *
658     * @return true if the user should be asked to change his password
659     */
660    public boolean requiresPasswordChange(CmsObject cms, CmsUser user) {
661
662        if (user.isManaged()
663            || user.isWebuser()
664            || OpenCms.getDefaultUsers().isDefaultUser(user.getName())
665            || OpenCms.getRoleManager().hasRole(cms, user.getName(), CmsRole.ROOT_ADMIN)) {
666            return false;
667        }
668        String lastPasswordChangeStr = (String)user.getAdditionalInfo().get(
669            CmsUserSettings.ADDITIONAL_INFO_LAST_PASSWORD_CHANGE);
670        if (lastPasswordChangeStr == null) {
671            return false;
672        }
673        long lastPasswordChange = Long.parseLong(lastPasswordChangeStr);
674        if ((System.currentTimeMillis() - lastPasswordChange) > getPasswordChangeInterval()) {
675            return true;
676        }
677        return false;
678    }
679
680    /**
681     * Checks if a user is required to change his password now.<p>
682     *
683     * @param cms the current CMS context
684     * @param user the user to check
685     *
686     * @return true if the user should be asked to change his password
687     */
688    public boolean requiresUserDataCheck(CmsObject cms, CmsUser user) {
689
690        if (user.isManaged()
691            || user.isWebuser()
692            || OpenCms.getDefaultUsers().isDefaultUser(user.getName())
693            || OpenCms.getRoleManager().hasRole(cms, user.getName(), CmsRole.ROOT_ADMIN)) {
694            return false;
695        }
696
697        String lastCheckStr = (String)user.getAdditionalInfo().get(
698            CmsUserSettings.ADDITIONAL_INFO_LAST_USER_DATA_CHECK);
699        if (lastCheckStr == null) {
700            return !CmsStringUtil.isEmptyOrWhitespaceOnly(getUserDataCheckIntervalStr());
701        }
702        long lastCheck = Long.parseLong(lastCheckStr);
703        if ((System.currentTimeMillis() - lastCheck) > getUserDataCheckInterval()) {
704            return true;
705        }
706        return false;
707    }
708
709    /**
710     * Resets lock from user.<p>
711     *
712     * @param username to reset lock for
713     */
714    public void resetUserTempDisable(String username) {
715
716        Set<CmsUserData> data = TEMP_DISABLED_USER.get(username);
717        if (data == null) {
718            return;
719        }
720        for (CmsUserData userData : data) {
721            userData.reset();
722        }
723        TEMP_DISABLED_USER.remove(username);
724    }
725
726    /**
727     * Sets the before login message to display on the login form.<p>
728     *
729     * This operation requires that the current user has role permissions of <code>{@link CmsRole#ROOT_ADMIN}</code>.<p>
730     *
731     * @param cms the current OpenCms user context
732     * @param message the message to set
733     *
734     * @throws CmsRoleViolationException in case the current user does not have the required role permissions
735     */
736    public void setBeforeLoginMessage(CmsObject cms, CmsLoginMessage message) throws CmsRoleViolationException {
737
738        if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) {
739            // during configuration phase no permission check id required
740            OpenCms.getRoleManager().checkRole(cms, CmsRole.ROOT_ADMIN);
741        }
742        m_beforeLoginMessage = message;
743
744        if (m_beforeLoginMessage != null) {
745            m_beforeLoginMessage.setFrozen();
746        }
747    }
748
749    /**
750     * Sets the custom login method.
751     *
752     * @param customLogin the custom login method
753     */
754    public void setCustomLogin(I_CmsCustomLogin customLogin) {
755
756        if ((customLogin != null) && (m_customLogin != null)) {
757            throw new RuntimeException("Custom login is already set: " + m_customLogin.toString());
758        }
759        m_customLogin = customLogin;
760    }
761
762    /**
763     * Sets the login message to display if a user logs in.<p>
764     *
765     * This operation requires that the current user has role permissions of <code>{@link CmsRole#ROOT_ADMIN}</code>.<p>
766     *
767     * @param cms the current OpenCms user context
768     * @param message the message to set
769     *
770     * @throws CmsRoleViolationException in case the current user does not have the required role permissions
771     */
772    public void setLoginMessage(CmsObject cms, CmsLoginMessage message) throws CmsRoleViolationException {
773
774        if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) {
775            // during configuration phase no permission check id required
776            OpenCms.getRoleManager().checkRole(cms, CmsRole.ROOT_ADMIN);
777        }
778        m_loginMessage = message;
779        if (m_loginMessage != null) {
780            m_loginMessage.setFrozen();
781        }
782    }
783
784    /**
785     * Unlocks a user who has exceeded his number of failed login attempts so that he can try to log in again.<p>
786     * This requires the "account manager" role.
787     *
788     * @param cms the current CMS context
789     * @param user the user to unlock
790     *
791     * @throws CmsRoleViolationException if the permission check fails
792     */
793    public void unlockUser(CmsObject cms, CmsUser user) throws CmsRoleViolationException {
794
795        OpenCms.getRoleManager().checkRole(cms, CmsRole.ACCOUNT_MANAGER.forOrgUnit(cms.getRequestContext().getOuFqn()));
796        Set<String> keysToRemove = getKeysForUser(user);
797        for (String keyToRemove : keysToRemove) {
798            m_storage.remove(keyToRemove);
799        }
800    }
801
802    /**
803     * Adds an invalid attempt to login for the given user / IP to the storage.<p>
804     *
805     * In case the configured threshold is reached, the user is disabled for the configured time.<p>
806     *
807     * @param userName the name of the user
808     * @param remoteAddress the remore address (IP) from which the login attempt was made
809     */
810    protected void addInvalidLogin(String userName, String remoteAddress) {
811
812        if (m_maxBadAttempts < 0) {
813            // invalid login storage is disabled
814            return;
815        }
816
817        String key = createStorageKey(userName, remoteAddress);
818        // look up the user in the storage
819        CmsUserData userData = m_storage.get(key);
820        if (userData != null) {
821            // user data already contained in storage
822            userData.increaseInvalidLoginCount();
823        } else {
824            // create an new data object for this user
825            userData = new CmsUserData();
826            m_storage.put(key, userData);
827        }
828    }
829
830    /**
831     * Removes all invalid attempts to login for the given user / IP.<p>
832     *
833     * @param userName the name of the user
834     * @param remoteAddress the remore address (IP) from which the login attempt was made
835     */
836    protected void removeInvalidLogins(String userName, String remoteAddress) {
837
838        if (m_maxBadAttempts < 0) {
839            // invalid login storage is disabled
840            return;
841        }
842
843        String key = createStorageKey(userName, remoteAddress);
844        // just remove the user from the storage
845        m_storage.remove(key);
846    }
847
848    /**
849     * Helper method to get all the storage keys that match a user's name.<p>
850     *
851     * @param user the user for which to get the storage keys
852     *
853     * @return the set of storage keys
854     */
855    private Set<String> getKeysForUser(CmsUser user) {
856
857        Set<String> keysToRemove = new HashSet<String>();
858        for (Map.Entry<String, CmsUserData> entry : m_storage.entrySet()) {
859            String key = entry.getKey();
860            int separatorPos = key.lastIndexOf(KEY_SEPARATOR);
861            String prefix = key.substring(0, separatorPos);
862            if (user.getName().equals(prefix)) {
863                keysToRemove.add(key);
864            }
865        }
866        return keysToRemove;
867    }
868}