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 new Integer(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    /** The logger instance for this class. */
165    private static final Log LOG = CmsLog.getLog(CmsLoginManager.class);
166
167    /**Map holding usernames and userdata for user which are currently locked.*/
168    protected static Map<String, Set<CmsUserData>> TEMP_DISABLED_USER;
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 login message, setting this may also disable logins for non-Admin users. */
189    private CmsLoginMessage m_loginMessage;
190
191    /** The before login message. */
192    private CmsLoginMessage m_beforeLoginMessage;
193
194    /** Max inactivity time. */
195    private String m_maxInactive;
196
197    /** Password change interval. */
198    private String m_passwordChangeInterval;
199
200    /** User data check interval. */
201    private String m_userDateCheckInterval;
202
203    /**
204     * Creates a new storage for invalid logins.<p>
205     *
206     * @param disableMinutes the minutes to disable an account if the threshold is reached
207     * @param maxBadAttempts the number of bad login attempts allowed before an account is temporarily disabled
208     * @param enableSecurity flag to determine if the security option should be enabled on the login dialog
209     * @param tokenLifetime the lifetime of authorization tokens, i.e. the time for which they are valid
210     * @param maxInactive maximum inactivity time
211     * @param passwordChangeInterval the password change interval
212     * @param userDataCheckInterval the user data check interval
213     */
214    public CmsLoginManager(
215        int disableMinutes,
216        int maxBadAttempts,
217        boolean enableSecurity,
218        String tokenLifetime,
219        String maxInactive,
220        String passwordChangeInterval,
221        String userDataCheckInterval) {
222
223        m_maxBadAttempts = maxBadAttempts;
224        if (TEMP_DISABLED_USER == null) {
225            TEMP_DISABLED_USER = new Hashtable<String, Set<CmsUserData>>();
226        }
227        if (m_maxBadAttempts >= 0) {
228            // otherwise the invalid login storage is sisabled
229            m_disableMinutes = disableMinutes;
230            m_disableMillis = disableMinutes * 60 * 1000;
231            m_storage = new Hashtable<String, CmsUserData>();
232
233        }
234        m_enableSecurity = enableSecurity;
235        m_tokenLifetimeStr = tokenLifetime;
236        m_maxInactive = maxInactive;
237        m_passwordChangeInterval = passwordChangeInterval;
238        m_userDateCheckInterval = userDataCheckInterval;
239    }
240
241    /**
242     * Returns the key to use for looking up the user in the invalid login storage.<p>
243     *
244     * @param userName the name of the user
245     * @param remoteAddress the remore address (IP) from which the login attempt was made
246     *
247     * @return the key to use for looking up the user in the invalid login storage
248     */
249    private static String createStorageKey(String userName, String remoteAddress) {
250
251        StringBuffer result = new StringBuffer();
252        result.append(userName);
253        result.append(KEY_SEPARATOR);
254        result.append(remoteAddress);
255        return result.toString();
256    }
257
258    /**
259     * Checks whether a user account can be locked because of inactivity.
260     *
261     * @param cms the CMS context
262     * @param user the user to check
263     * @return true if the user may be locked after being inactive for too long
264     */
265    public boolean canLockBecauseOfInactivity(CmsObject cms, CmsUser user) {
266
267        return !user.isManaged()
268            && !user.isWebuser()
269            && !OpenCms.getDefaultUsers().isDefaultUser(user.getName())
270            && !OpenCms.getRoleManager().hasRole(cms, user.getName(), CmsRole.ROOT_ADMIN);
271    }
272
273    /**
274     * Checks whether the given user has been inactive for longer than the configured limit.<p>
275     *
276     * If no max inactivity time is configured, always returns false.
277     *
278     * @param user the user to check
279     * @return true if the user has been inactive for longer than the configured limit
280     */
281    public boolean checkInactive(CmsUser user) {
282
283        if (m_maxInactive == null) {
284            return false;
285        }
286
287        try {
288            long maxInactive = CmsStringUtil.parseDuration(m_maxInactive, Long.MAX_VALUE);
289            return (System.currentTimeMillis() - user.getLastlogin()) > maxInactive;
290        } catch (Exception e) {
291            LOG.warn(e.getLocalizedMessage(), e);
292            return false;
293        }
294    }
295
296    /**
297     * Checks if the threshold for the invalid logins has been reached for the given user.<p>
298     *
299     * In case the configured threshold is reached, an Exception is thrown.<p>
300     *
301     * @param userName the name of the user
302     * @param remoteAddress the remote address (IP) from which the login attempt was made
303     *
304     * @throws CmsAuthentificationException in case the threshold of invalid login attempts has been reached
305     */
306    public void checkInvalidLogins(String userName, String remoteAddress) throws CmsAuthentificationException {
307
308        if (m_maxBadAttempts < 0) {
309            // invalid login storage is disabled
310            return;
311        }
312
313        String key = createStorageKey(userName, remoteAddress);
314        // look up the user in the storage
315        CmsUserData userData = m_storage.get(key);
316        if ((userData != null) && (userData.isDisabled())) {
317            // threshold of invalid logins is reached
318            Set<CmsUserData> data = TEMP_DISABLED_USER.get(userName);
319            if (data == null) {
320                data = new HashSet<CmsUserData>();
321            }
322            data.add(userData);
323            TEMP_DISABLED_USER.put(userName, data);
324            throw new CmsUserDisabledException(
325                Messages.get().container(
326                    Messages.ERR_LOGIN_FAILED_TEMP_DISABLED_4,
327                    new Object[] {
328                        userName,
329                        remoteAddress,
330                        userData.getReleaseDate(),
331                        userData.getInvalidLoginCount()}));
332        }
333        if (TEMP_DISABLED_USER.containsKey(userName) & (userData != null)) {
334            //User war disabled, but time is over -> remove from list
335            if (TEMP_DISABLED_USER.get(userName).contains(userData)) {
336                TEMP_DISABLED_USER.get(userName).remove(userData);
337                if (TEMP_DISABLED_USER.get(userName).isEmpty()) {
338                    TEMP_DISABLED_USER.remove(userName);
339                }
340            }
341        }
342    }
343
344    /**
345     * Checks if a login is currently allowed.<p>
346     *
347     * In case no logins are allowed, an Exception is thrown.<p>
348     *
349     * @throws CmsAuthentificationException in case no logins are allowed
350     */
351    public void checkLoginAllowed() throws CmsAuthentificationException {
352
353        if ((m_loginMessage != null) && (m_loginMessage.isLoginCurrentlyForbidden())) {
354            // login message has been set and is active
355            throw new CmsAuthentificationException(
356                Messages.get().container(Messages.ERR_LOGIN_FAILED_WITH_MESSAGE_1, m_loginMessage.getMessage()));
357        }
358    }
359
360    /**
361     * Returns the current before login message that is displayed on the login form.<p>
362     *
363     * if <code>null</code> is returned, no login message has been currently set.<p>
364     *
365     * @return  the current login message that is displayed if a user logs in
366     */
367    public CmsLoginMessage getBeforeLoginMessage() {
368
369        return m_beforeLoginMessage;
370    }
371
372    /**
373     * Returns the minutes an account gets disabled after too many failed login attempts.<p>
374     *
375     * @return the minutes an account gets disabled after too many failed login attempts
376     */
377    public int getDisableMinutes() {
378
379        return m_disableMinutes;
380    }
381
382    /**
383     * Returns the current login message that is displayed if a user logs in.<p>
384     *
385     * if <code>null</code> is returned, no login message has been currently set.<p>
386     *
387     * @return  the current login message that is displayed if a user logs in
388     */
389    public CmsLoginMessage getLoginMessage() {
390
391        return m_loginMessage;
392    }
393
394    /**
395     * Returns the number of bad login attempts allowed before an account is temporarily disabled.<p>
396     *
397     * @return the number of bad login attempts allowed before an account is temporarily disabled
398     */
399    public int getMaxBadAttempts() {
400
401        return m_maxBadAttempts;
402    }
403
404    /**
405     * Gets the max inactivity time.<p>
406     *
407     * @return the max inactivity time
408     */
409    public String getMaxInactive() {
410
411        return m_maxInactive;
412    }
413
414    /**
415     * Gets the password change interval.<p>
416     *
417     * @return the password change interval
418     */
419    public long getPasswordChangeInterval() {
420
421        if (m_passwordChangeInterval == null) {
422            return Long.MAX_VALUE;
423        } else {
424            return CmsStringUtil.parseDuration(m_passwordChangeInterval, Long.MAX_VALUE);
425        }
426    }
427
428    /**
429     * Gets the raw password change interval string.<p>
430     *
431     * @return the configured string for the password change interval
432     */
433    public String getPasswordChangeIntervalStr() {
434
435        return m_passwordChangeInterval;
436    }
437
438    /**
439     * Gets the authorization token lifetime in milliseconds.<p>
440     *
441     * @return the authorization token lifetime in milliseconds
442     */
443    public long getTokenLifetime() {
444
445        if (m_tokenLifetimeStr == null) {
446            return DEFAULT_TOKEN_LIFETIME;
447        }
448        return CmsStringUtil.parseDuration(m_tokenLifetimeStr, DEFAULT_TOKEN_LIFETIME);
449    }
450
451    /**
452     * Gets the configured token lifetime as a string.<p>
453     *
454     * @return the configured token lifetime as a string
455     */
456    public String getTokenLifetimeStr() {
457
458        return m_tokenLifetimeStr;
459    }
460
461    /**
462     * Gets the user data check interval.<p>
463     *
464     * @return the user data check interval
465     */
466    public long getUserDataCheckInterval() {
467
468        if (m_userDateCheckInterval == null) {
469            return Long.MAX_VALUE;
470        } else {
471            return CmsStringUtil.parseDuration(m_userDateCheckInterval, Long.MAX_VALUE);
472        }
473    }
474
475    /**
476     * Gets the raw user data check interval string.<p>
477     *
478     * @return the configured string for the user data check interval
479     */
480    public String getUserDataCheckIntervalStr() {
481
482        return m_userDateCheckInterval;
483    }
484
485    /**
486     * Returns if the security option ahould be enabled on the login dialog.<p>
487     *
488     * @return <code>true</code> if the security option ahould be enabled on the login dialog, otherwise <code>false</code>
489     */
490    public boolean isEnableSecurity() {
491
492        return m_enableSecurity;
493    }
494
495    /**
496     * Checks if password has to be reset.<p>
497     *
498     * @param cms CmsObject
499     * @param user CmsUser
500     * @return true if password should be reset
501     */
502    public boolean isPasswordReset(CmsObject cms, CmsUser user) {
503
504        if (user.isManaged() || user.isWebuser() || OpenCms.getDefaultUsers().isDefaultUser(user.getName())) {
505            return false;
506        }
507        if (user.getAdditionalInfo().get(CmsUserSettings.ADDITIONAL_INFO_PASSWORD_RESET) != null) {
508            return true;
509        }
510        return false;
511    }
512
513    /**
514     * Checks if a user is locked due to too many failed logins.<p>
515     *
516     * @param user the user to check
517     *
518     * @return true if the user is locked
519     */
520    public boolean isUserLocked(CmsUser user) {
521
522        Set<String> keysForUser = getKeysForUser(user);
523        for (String key : keysForUser) {
524            CmsUserData data = m_storage.get(key);
525            if ((data != null) && data.isDisabled()) {
526                return true;
527            }
528        }
529        return false;
530    }
531
532    /**
533     * Checks if given user it temporarily locked.<p>
534     *
535     * @param username to check
536     * @return true if user is locked
537     */
538    public boolean isUserTempDisabled(String username) {
539
540        Set<CmsUserData> data = TEMP_DISABLED_USER.get(username);
541        if (data == null) {
542            return false;
543        }
544        for (CmsUserData userData : data) {
545            if (!userData.isDisabled()) {
546                data.remove(userData);
547            }
548        }
549        if (data.size() > 0) {
550            TEMP_DISABLED_USER.put(username, data);
551            return true;
552        } else {
553            TEMP_DISABLED_USER.remove(username);
554            return false;
555        }
556    }
557
558    /**
559     * Removes the current login message.<p>
560     *
561     * This operation requires that the current user has role permissions of <code>{@link CmsRole#ROOT_ADMIN}</code>.<p>
562     *
563     * @param cms the current OpenCms user context
564     *
565     * @throws CmsRoleViolationException in case the current user does not have the required role permissions
566     */
567    public void removeLoginMessage(CmsObject cms) throws CmsRoleViolationException {
568
569        OpenCms.getRoleManager().checkRole(cms, CmsRole.ROOT_ADMIN);
570        m_loginMessage = null;
571    }
572
573    /**
574     * Checks if a user is required to change his password now.<p>
575     *
576     * @param cms the current CMS context
577     * @param user the user to check
578     *
579     * @return true if the user should be asked to change his password
580     */
581    public boolean requiresPasswordChange(CmsObject cms, CmsUser user) {
582
583        if (user.isManaged()
584            || user.isWebuser()
585            || OpenCms.getDefaultUsers().isDefaultUser(user.getName())
586            || OpenCms.getRoleManager().hasRole(cms, user.getName(), CmsRole.ROOT_ADMIN)) {
587            return false;
588        }
589        String lastPasswordChangeStr = (String)user.getAdditionalInfo().get(
590            CmsUserSettings.ADDITIONAL_INFO_LAST_PASSWORD_CHANGE);
591        if (lastPasswordChangeStr == null) {
592            return false;
593        }
594        long lastPasswordChange = Long.parseLong(lastPasswordChangeStr);
595        if ((System.currentTimeMillis() - lastPasswordChange) > getPasswordChangeInterval()) {
596            return true;
597        }
598        return false;
599    }
600
601    /**
602     * Checks if a user is required to change his password now.<p>
603     *
604     * @param cms the current CMS context
605     * @param user the user to check
606     *
607     * @return true if the user should be asked to change his password
608     */
609    public boolean requiresUserDataCheck(CmsObject cms, CmsUser user) {
610
611        if (user.isManaged()
612            || user.isWebuser()
613            || OpenCms.getDefaultUsers().isDefaultUser(user.getName())
614            || OpenCms.getRoleManager().hasRole(cms, user.getName(), CmsRole.ROOT_ADMIN)) {
615            return false;
616        }
617        String lastCheckStr = (String)user.getAdditionalInfo().get(
618            CmsUserSettings.ADDITIONAL_INFO_LAST_USER_DATA_CHECK);
619        if (lastCheckStr == null) {
620            return !CmsStringUtil.isEmptyOrWhitespaceOnly(getUserDataCheckIntervalStr());
621        }
622        long lastCheck = Long.parseLong(lastCheckStr);
623        if ((System.currentTimeMillis() - lastCheck) > getUserDataCheckInterval()) {
624            return true;
625        }
626        return false;
627    }
628
629    /**
630     * Resets lock from user.<p>
631     *
632     * @param username to reset lock for
633     */
634    public void resetUserTempDisable(String username) {
635
636        Set<CmsUserData> data = TEMP_DISABLED_USER.get(username);
637        if (data == null) {
638            return;
639        }
640        for (CmsUserData userData : data) {
641            userData.reset();
642        }
643        TEMP_DISABLED_USER.remove(username);
644    }
645
646    /**
647     * Sets the before login message to display on the login form.<p>
648     *
649     * This operation requires that the current user has role permissions of <code>{@link CmsRole#ROOT_ADMIN}</code>.<p>
650     *
651     * @param cms the current OpenCms user context
652     * @param message the message to set
653     *
654     * @throws CmsRoleViolationException in case the current user does not have the required role permissions
655     */
656    public void setBeforeLoginMessage(CmsObject cms, CmsLoginMessage message) throws CmsRoleViolationException {
657
658        if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) {
659            // during configuration phase no permission check id required
660            OpenCms.getRoleManager().checkRole(cms, CmsRole.ROOT_ADMIN);
661        }
662        m_beforeLoginMessage = message;
663
664        if (m_beforeLoginMessage != null) {
665            m_beforeLoginMessage.setFrozen();
666        }
667    }
668
669    /**
670     * Sets the login message to display if a user logs in.<p>
671     *
672     * This operation requires that the current user has role permissions of <code>{@link CmsRole#ROOT_ADMIN}</code>.<p>
673     *
674     * @param cms the current OpenCms user context
675     * @param message the message to set
676     *
677     * @throws CmsRoleViolationException in case the current user does not have the required role permissions
678     */
679    public void setLoginMessage(CmsObject cms, CmsLoginMessage message) throws CmsRoleViolationException {
680
681        if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) {
682            // during configuration phase no permission check id required
683            OpenCms.getRoleManager().checkRole(cms, CmsRole.ROOT_ADMIN);
684        }
685        m_loginMessage = message;
686        if (m_loginMessage != null) {
687            m_loginMessage.setFrozen();
688        }
689    }
690
691    /**
692     * Unlocks a user who has exceeded his number of failed login attempts so that he can try to log in again.<p>
693     * This requires the "account manager" role.
694     *
695     * @param cms the current CMS context
696     * @param user the user to unlock
697     *
698     * @throws CmsRoleViolationException if the permission check fails
699     */
700    public void unlockUser(CmsObject cms, CmsUser user) throws CmsRoleViolationException {
701
702        OpenCms.getRoleManager().checkRole(cms, CmsRole.ACCOUNT_MANAGER.forOrgUnit(cms.getRequestContext().getOuFqn()));
703        Set<String> keysToRemove = getKeysForUser(user);
704        for (String keyToRemove : keysToRemove) {
705            m_storage.remove(keyToRemove);
706        }
707    }
708
709    /**
710     * Adds an invalid attempt to login for the given user / IP to the storage.<p>
711     *
712     * In case the configured threshold is reached, the user is disabled for the configured time.<p>
713     *
714     * @param userName the name of the user
715     * @param remoteAddress the remore address (IP) from which the login attempt was made
716     */
717    protected void addInvalidLogin(String userName, String remoteAddress) {
718
719        if (m_maxBadAttempts < 0) {
720            // invalid login storage is disabled
721            return;
722        }
723
724        String key = createStorageKey(userName, remoteAddress);
725        // look up the user in the storage
726        CmsUserData userData = m_storage.get(key);
727        if (userData != null) {
728            // user data already contained in storage
729            userData.increaseInvalidLoginCount();
730        } else {
731            // create an new data object for this user
732            userData = new CmsUserData();
733            m_storage.put(key, userData);
734        }
735    }
736
737    /**
738     * Removes all invalid attempts to login for the given user / IP.<p>
739     *
740     * @param userName the name of the user
741     * @param remoteAddress the remore address (IP) from which the login attempt was made
742     */
743    protected void removeInvalidLogins(String userName, String remoteAddress) {
744
745        if (m_maxBadAttempts < 0) {
746            // invalid login storage is disabled
747            return;
748        }
749
750        String key = createStorageKey(userName, remoteAddress);
751        // just remove the user from the storage
752        m_storage.remove(key);
753    }
754
755    /**
756     * Helper method to get all the storage keys that match a user's name.<p>
757     *
758     * @param user the user for which to get the storage keys
759     *
760     * @return the set of storage keys
761     */
762    private Set<String> getKeysForUser(CmsUser user) {
763
764        Set<String> keysToRemove = new HashSet<String>();
765        for (Map.Entry<String, CmsUserData> entry : m_storage.entrySet()) {
766            String key = entry.getKey();
767            int separatorPos = key.lastIndexOf(KEY_SEPARATOR);
768            String prefix = key.substring(0, separatorPos);
769            if (user.getName().equals(prefix)) {
770                keysToRemove.add(key);
771            }
772        }
773        return keysToRemove;
774    }
775}