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