/* * This library is part of OpenCms - * the Open Source Content Management System * * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * For further information about Alkacon Software GmbH, please see the * company website: http://www.alkacon.com * * For further information about OpenCms, please see the * project website: http://www.opencms.org * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.opencms.db; import org.opencms.file.CmsObject; import org.opencms.main.OpenCms; import org.opencms.security.CmsAuthentificationException; import org.opencms.security.CmsRole; import org.opencms.security.CmsRoleViolationException; import org.opencms.security.CmsUserDisabledException; import org.opencms.security.Messages; import java.util.Date; import java.util.Hashtable; import java.util.Map; /** * Provides functions used to check the validity of a user login.<p> * * Stores invalid login attempts and disables a user account temporarily in case * the configured threshold of invalid logins is reached.<p> * * The invalid login attempt storage operates on a combination of user name, login remote IP address and * user type. This means that a user can be disabled for one remote IP, but still be enabled for * another remote IP.<p> * * Also allows to temporarily disallow logins (for example in case of maintenance work on the system).<p> * * @since 6.0.0 */ public class CmsLoginManager { /** * Contains the data stored for each user in the storage for invalid login attempts.<p> */ private class CmsUserData { /** The start time this account was disabled. */ private long m_disableTimeStart; /** The count of the failed attempts. */ private int m_invalidLoginCount; /** * Creates a new user data instance.<p> */ protected CmsUserData() { // a new instance is creted only if there already was one failed attempt m_invalidLoginCount = 1; } /** * Returns the bad attempt count for this user.<p> * * @return the bad attempt count for this user */ protected Integer getInvalidLoginCount() { return new Integer(m_invalidLoginCount); } /** * Returns the date this disabled user is released again.<p> * * @return the date this disabled user is released again */ protected Date getReleaseDate() { return new Date(m_disableTimeStart + m_disableMillis + 1); } /** * Increases the bad attempt count, disables the data in case the * configured threshold is reached.<p> */ protected void increaseInvalidLoginCount() { m_invalidLoginCount++; if (m_invalidLoginCount >= m_maxBadAttempts) { // threshold for bad login attempts has been reached for this user if (m_disableTimeStart == 0) { // only disable in case this user has not already been disabled m_disableTimeStart = System.currentTimeMillis(); } } } /** * Returns <code>true</code> in case this user has been temporarily disabled.<p> * * @return <code>true</code> in case this user has been temporarily disabled */ protected boolean isDisabled() { if (m_disableTimeStart > 0) { // check if the disable time is already over long currentTime = System.currentTimeMillis(); if ((currentTime - m_disableTimeStart) > m_disableMillis) { // disable time is over m_disableTimeStart = 0; } } return m_disableTimeStart > 0; } } /** Default lock time if treshold for bad login attempts is reached. */ public static final int DISABLE_MINUTES_DEFAULT = 15; /** Default setting for the security option. */ public static final boolean ENABLE_SECURITY_DEFAULT = false; /** Default for bad login attempts. */ public static final int MAX_BAD_ATTEMPTS_DEFAULT = 3; /** The milliseconds to disable an account if the threshold is reached. */ protected int m_disableMillis; /** The minutes to disable an account if the threshold is reached. */ protected int m_disableMinutes; /** The flag to determine if the security option ahould be enabled on the login dialog. */ protected boolean m_enableSecurity; /** The number of bad login attempts allowed before an account is temporarily disabled. */ protected int m_maxBadAttempts; /** The storage for the bad login attempts. */ protected Map<String, CmsUserData> m_storage; /** The login message, setting this may also disable logins for non-Admin users. */ private CmsLoginMessage m_loginMessage; /** * Creates a new storage for invalid logins.<p> * * @param disableMinutes the minutes to disable an account if the threshold is reached * @param maxBadAttempts the number of bad login attempts allowed before an account is temporarily disabled * @param enableSecurity flag to determine if the security option should be enabled on the login dialog */ public CmsLoginManager(int disableMinutes, int maxBadAttempts, boolean enableSecurity) { m_maxBadAttempts = maxBadAttempts; if (m_maxBadAttempts >= 0) { // otherwise the invalid login storage is sisabled m_disableMinutes = disableMinutes; m_disableMillis = disableMinutes * 60 * 1000; m_storage = new Hashtable<String, CmsUserData>(); } m_enableSecurity = enableSecurity; } /** * Returns the key to use for looking up the user in the invalid login storage.<p> * * @param userName the name of the user * @param remoteAddress the remore address (IP) from which the login attempt was made * * @return the key to use for looking up the user in the invalid login storage */ private static String createStorageKey(String userName, String remoteAddress) { StringBuffer result = new StringBuffer(); result.append(userName); result.append('_'); result.append(remoteAddress); return result.toString(); } /** * Checks if the threshold for the invalid logins has been reached for the given user.<p> * * In case the configured threshold is reached, an Exception is thrown.<p> * * @param userName the name of the user * @param remoteAddress the remote address (IP) from which the login attempt was made * * @throws CmsAuthentificationException in case the threshold of invalid login attempts has been reached */ public void checkInvalidLogins(String userName, String remoteAddress) throws CmsAuthentificationException { if (m_maxBadAttempts < 0) { // invalid login storage is disabled return; } String key = createStorageKey(userName, remoteAddress); // look up the user in the storage CmsUserData userData = m_storage.get(key); if ((userData != null) && (userData.isDisabled())) { // threshold of invalid logins is reached throw new CmsUserDisabledException(Messages.get().container( Messages.ERR_LOGIN_FAILED_TEMP_DISABLED_4, new Object[] {userName, remoteAddress, userData.getReleaseDate(), userData.getInvalidLoginCount()})); } } /** * Checks if a login is currently allowed.<p> * * In case no logins are allowed, an Exception is thrown.<p> * * @throws CmsAuthentificationException in case no logins are allowed */ public void checkLoginAllowed() throws CmsAuthentificationException { if ((m_loginMessage != null) && (m_loginMessage.isLoginCurrentlyForbidden())) { // login message has been set and is active throw new CmsAuthentificationException(Messages.get().container( Messages.ERR_LOGIN_FAILED_WITH_MESSAGE_1, m_loginMessage.getMessage())); } } /** * Returns the minutes an account gets disabled after too many failed login attempts.<p> * * @return the minutes an account gets disabled after too many failed login attempts */ public int getDisableMinutes() { return m_disableMinutes; } /** * Returns if the security option ahould be enabled on the login dialog.<p> * * @return <code>true</code> if the security option ahould be enabled on the login dialog, otherwise <code>false</code> */ public boolean isEnableSecurity() { return m_enableSecurity; } /** * Returns the current login message that is displayed if a user logs in.<p> * * if <code>null</code> is returned, no login message has been currently set.<p> * * @return the current login message that is displayed if a user logs in */ public CmsLoginMessage getLoginMessage() { return m_loginMessage; } /** * Returns the number of bad login attempts allowed before an account is temporarily disabled.<p> * * @return the number of bad login attempts allowed before an account is temporarily disabled */ public int getMaxBadAttempts() { return m_maxBadAttempts; } /** * Removes the current login message.<p> * * This operation requires that the current user has role permissions of <code>{@link CmsRole#ROOT_ADMIN}</code>.<p> * * @param cms the current OpenCms user context * * @throws CmsRoleViolationException in case the current user does not have the required role permissions */ public void removeLoginMessage(CmsObject cms) throws CmsRoleViolationException { OpenCms.getRoleManager().checkRole(cms, CmsRole.ROOT_ADMIN); m_loginMessage = null; } /** * Sets the login message to display if a user logs in.<p> * * This operation requires that the current user has role permissions of <code>{@link CmsRole#ROOT_ADMIN}</code>.<p> * * @param cms the current OpenCms user context * @param message the message to set * * @throws CmsRoleViolationException in case the current user does not have the required role permissions */ public void setLoginMessage(CmsObject cms, CmsLoginMessage message) throws CmsRoleViolationException { if (OpenCms.getRunLevel() >= OpenCms.RUNLEVEL_3_SHELL_ACCESS) { // during configuration phase no permission check id required OpenCms.getRoleManager().checkRole(cms, CmsRole.ROOT_ADMIN); } m_loginMessage = message; if (m_loginMessage != null) { m_loginMessage.setFrozen(); } } /** * Adds an invalid attempt to login for the given user / IP to the storage.<p> * * In case the configured threshold is reached, the user is disabled for the configured time.<p> * * @param userName the name of the user * @param remoteAddress the remore address (IP) from which the login attempt was made */ protected void addInvalidLogin(String userName, String remoteAddress) { if (m_maxBadAttempts < 0) { // invalid login storage is disabled return; } String key = createStorageKey(userName, remoteAddress); // look up the user in the storage CmsUserData userData = m_storage.get(key); if (userData != null) { // user data already contained in storage userData.increaseInvalidLoginCount(); } else { // create an new data object for this user userData = new CmsUserData(); m_storage.put(key, userData); } } /** * Removes all invalid attempts to login for the given user / IP.<p> * * @param userName the name of the user * @param remoteAddress the remore address (IP) from which the login attempt was made */ protected void removeInvalidLogins(String userName, String remoteAddress) { if (m_maxBadAttempts < 0) { // invalid login storage is disabled return; } String key = createStorageKey(userName, remoteAddress); // just remove the user from the storage m_storage.remove(key); } }