org.opencms.db.CmsLoginManager Maven / Gradle / Ivy
Show all versions of opencms-core Show documentation
/*
* This library is part of OpenCms -
* the Open Source Content Management System
*
* Copyright (c) Alkacon Software GmbH & Co. KG (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 & Co. KG, 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.file.CmsUser;
import org.opencms.main.CmsLog;
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 org.opencms.util.CmsStringUtil;
import java.util.Date;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
/**
* Provides functions used to check the validity of a user login.
*
* Stores invalid login attempts and disables a user account temporarily in case
* the configured threshold of invalid logins is reached.
*
* 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.
*
* Also allows to temporarily disallow logins (for example in case of maintenance work on the system).
*
* @since 6.0.0
*/
public class CmsLoginManager {
/**
* Contains the data stored for each user in the storage for invalid login attempts.
*/
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.
*/
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.
*
* @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.
*
* @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.
*/
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 true
in case this user has been temporarily disabled.
*
* @return true
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;
}
/**
* Reset disable time.
*/
protected void reset() {
m_disableTimeStart = 0;
m_invalidLoginCount = 0;
}
}
/** Default token lifetime. */
public static final long DEFAULT_TOKEN_LIFETIME = 3600 * 24 * 1000;
/** 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;
/** Separator used for storage keys. */
public static final String KEY_SEPARATOR = "_";
/** Default for bad login attempts. */
public static final int MAX_BAD_ATTEMPTS_DEFAULT = 3;
/** The logger instance for this class. */
private static final Log LOG = CmsLog.getLog(CmsLoginManager.class);
/**Map holding usernames and userdata for user which are currently locked.*/
protected static Map> TEMP_DISABLED_USER;
/** 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 m_storage;
/** The token lifetime. */
protected String m_tokenLifetimeStr;
/** The login message, setting this may also disable logins for non-Admin users. */
private CmsLoginMessage m_loginMessage;
/** The before login message. */
private CmsLoginMessage m_beforeLoginMessage;
/** Max inactivity time. */
private String m_maxInactive;
/** Password change interval. */
private String m_passwordChangeInterval;
/** User data check interval. */
private String m_userDateCheckInterval;
/** Option which determines whether the login dialog should require an organizational unit. */
private boolean m_requireOrgUnit;
/** The logout URI. */
private String m_logoutUri;
/**
* Creates a new storage for invalid logins.
*
* @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
* @param tokenLifetime the lifetime of authorization tokens, i.e. the time for which they are valid
* @param maxInactive maximum inactivity time
* @param passwordChangeInterval the password change interval
* @param userDataCheckInterval the user data check interval
* @param requireOrgUnit if true, should require organizational unit selection on login
* @param logoutUri the alternative logout handler URI
*/
public CmsLoginManager(
int disableMinutes,
int maxBadAttempts,
boolean enableSecurity,
String tokenLifetime,
String maxInactive,
String passwordChangeInterval,
String userDataCheckInterval,
boolean requireOrgUnit,
String logoutUri) {
m_maxBadAttempts = maxBadAttempts;
if (TEMP_DISABLED_USER == null) {
TEMP_DISABLED_USER = new Hashtable>();
}
if (m_maxBadAttempts >= 0) {
// otherwise the invalid login storage is sisabled
m_disableMinutes = disableMinutes;
m_disableMillis = disableMinutes * 60 * 1000;
m_storage = new Hashtable();
}
m_enableSecurity = enableSecurity;
m_tokenLifetimeStr = tokenLifetime;
m_maxInactive = maxInactive;
m_passwordChangeInterval = passwordChangeInterval;
m_userDateCheckInterval = userDataCheckInterval;
m_requireOrgUnit = requireOrgUnit;
m_logoutUri = logoutUri;
}
/**
* Returns the key to use for looking up the user in the invalid login storage.
*
* @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(KEY_SEPARATOR);
result.append(remoteAddress);
return result.toString();
}
/**
* Checks whether a user account can be locked because of inactivity.
*
* @param cms the CMS context
* @param user the user to check
* @return true if the user may be locked after being inactive for too long
*/
public boolean canLockBecauseOfInactivity(CmsObject cms, CmsUser user) {
return !user.isManaged()
&& !user.isWebuser()
&& !OpenCms.getDefaultUsers().isDefaultUser(user.getName())
&& !OpenCms.getRoleManager().hasRole(cms, user.getName(), CmsRole.ROOT_ADMIN);
}
/**
* Checks whether the given user has been inactive for longer than the configured limit.
*
* If no max inactivity time is configured, always returns false.
*
* @param user the user to check
* @return true if the user has been inactive for longer than the configured limit
*/
public boolean checkInactive(CmsUser user) {
if (m_maxInactive == null) {
return false;
}
try {
if (user.getLastlogin() == 0) {
return false;
}
long maxInactive = CmsStringUtil.parseDuration(m_maxInactive, Long.MAX_VALUE);
return (System.currentTimeMillis() - user.getLastlogin()) > maxInactive;
} catch (Exception e) {
LOG.warn(e.getLocalizedMessage(), e);
return false;
}
}
/**
* Checks if the threshold for the invalid logins has been reached for the given user.
*
* In case the configured threshold is reached, an Exception is thrown.
*
* @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
Set data = TEMP_DISABLED_USER.get(userName);
if (data == null) {
data = new HashSet();
}
data.add(userData);
TEMP_DISABLED_USER.put(userName, data);
throw new CmsUserDisabledException(
Messages.get().container(
Messages.ERR_LOGIN_FAILED_TEMP_DISABLED_4,
new Object[] {
userName,
remoteAddress,
userData.getReleaseDate(),
userData.getInvalidLoginCount()}));
}
if (TEMP_DISABLED_USER.containsKey(userName) & (userData != null)) {
//User war disabled, but time is over -> remove from list
if (TEMP_DISABLED_USER.get(userName).contains(userData)) {
TEMP_DISABLED_USER.get(userName).remove(userData);
if (TEMP_DISABLED_USER.get(userName).isEmpty()) {
TEMP_DISABLED_USER.remove(userName);
}
}
}
}
/**
* Checks if a login is currently allowed.
*
* In case no logins are allowed, an Exception is thrown.
*
* @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 current before login message that is displayed on the login form.
*
* if null
is returned, no login message has been currently set.
*
* @return the current login message that is displayed if a user logs in
*/
public CmsLoginMessage getBeforeLoginMessage() {
return m_beforeLoginMessage;
}
/**
* Returns the minutes an account gets disabled after too many failed login attempts.
*
* @return the minutes an account gets disabled after too many failed login attempts
*/
public int getDisableMinutes() {
return m_disableMinutes;
}
/**
* Returns the current login message that is displayed if a user logs in.
*
* if null
is returned, no login message has been currently set.
*
* @return the current login message that is displayed if a user logs in
*/
public CmsLoginMessage getLoginMessage() {
return m_loginMessage;
}
/**
* Gets the logout URI.
*
* If this is not null, users will be redirected to this JSP when logging out from the workplace
* or page editor. The JSP is responsible for invalidating the user's session.
*
* @return the logout URI
*/
public String getLogoutUri() {
return m_logoutUri;
}
/**
* Returns the number of bad login attempts allowed before an account is temporarily disabled.
*
* @return the number of bad login attempts allowed before an account is temporarily disabled
*/
public int getMaxBadAttempts() {
return m_maxBadAttempts;
}
/**
* Gets the max inactivity time.
*
* @return the max inactivity time
*/
public String getMaxInactive() {
return m_maxInactive;
}
/**
* Gets the password change interval.
*
* @return the password change interval
*/
public long getPasswordChangeInterval() {
if (m_passwordChangeInterval == null) {
return Long.MAX_VALUE;
} else {
return CmsStringUtil.parseDuration(m_passwordChangeInterval, Long.MAX_VALUE);
}
}
/**
* Gets the raw password change interval string.
*
* @return the configured string for the password change interval
*/
public String getPasswordChangeIntervalStr() {
return m_passwordChangeInterval;
}
/**
* Gets the authorization token lifetime in milliseconds.
*
* @return the authorization token lifetime in milliseconds
*/
public long getTokenLifetime() {
if (m_tokenLifetimeStr == null) {
return DEFAULT_TOKEN_LIFETIME;
}
return CmsStringUtil.parseDuration(m_tokenLifetimeStr, DEFAULT_TOKEN_LIFETIME);
}
/**
* Gets the configured token lifetime as a string.
*
* @return the configured token lifetime as a string
*/
public String getTokenLifetimeStr() {
return m_tokenLifetimeStr;
}
/**
* Gets the user data check interval.
*
* @return the user data check interval
*/
public long getUserDataCheckInterval() {
if (m_userDateCheckInterval == null) {
return Long.MAX_VALUE;
} else {
return CmsStringUtil.parseDuration(m_userDateCheckInterval, Long.MAX_VALUE);
}
}
/**
* Gets the raw user data check interval string.
*
* @return the configured string for the user data check interval
*/
public String getUserDataCheckIntervalStr() {
return m_userDateCheckInterval;
}
/**
* Returns if the security option ahould be enabled on the login dialog.
*
* @return true
if the security option ahould be enabled on the login dialog, otherwise false
*/
public boolean isEnableSecurity() {
return m_enableSecurity;
}
/**
* Returns true if organizational unit selection should be required on login.
*
* @return true if org unit selection should be required
*/
public boolean isOrgUnitRequired() {
return m_requireOrgUnit;
}
/**
* Checks if password has to be reset.
*
* @param cms CmsObject
* @param user CmsUser
* @return true if password should be reset
*/
public boolean isPasswordReset(CmsObject cms, CmsUser user) {
if (user.isManaged() || user.isWebuser() || OpenCms.getDefaultUsers().isDefaultUser(user.getName())) {
return false;
}
if (user.getAdditionalInfo().get(CmsUserSettings.ADDITIONAL_INFO_PASSWORD_RESET) != null) {
return true;
}
return false;
}
/**
* Checks if a user is locked due to too many failed logins.
*
* @param user the user to check
*
* @return true if the user is locked
*/
public boolean isUserLocked(CmsUser user) {
Set keysForUser = getKeysForUser(user);
for (String key : keysForUser) {
CmsUserData data = m_storage.get(key);
if ((data != null) && data.isDisabled()) {
return true;
}
}
return false;
}
/**
* Checks if given user it temporarily locked.
*
* @param username to check
* @return true if user is locked
*/
public boolean isUserTempDisabled(String username) {
Set data = TEMP_DISABLED_USER.get(username);
if (data == null) {
return false;
}
for (CmsUserData userData : data) {
if (!userData.isDisabled()) {
data.remove(userData);
}
}
if (data.size() > 0) {
TEMP_DISABLED_USER.put(username, data);
return true;
} else {
TEMP_DISABLED_USER.remove(username);
return false;
}
}
/**
* Removes the current login message.
*
* This operation requires that the current user has role permissions of {@link CmsRole#ROOT_ADMIN}
.
*
* @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;
}
/**
* Checks if a user is required to change his password now.
*
* @param cms the current CMS context
* @param user the user to check
*
* @return true if the user should be asked to change his password
*/
public boolean requiresPasswordChange(CmsObject cms, CmsUser user) {
if (user.isManaged()
|| user.isWebuser()
|| OpenCms.getDefaultUsers().isDefaultUser(user.getName())
|| OpenCms.getRoleManager().hasRole(cms, user.getName(), CmsRole.ROOT_ADMIN)) {
return false;
}
String lastPasswordChangeStr = (String)user.getAdditionalInfo().get(
CmsUserSettings.ADDITIONAL_INFO_LAST_PASSWORD_CHANGE);
if (lastPasswordChangeStr == null) {
return false;
}
long lastPasswordChange = Long.parseLong(lastPasswordChangeStr);
if ((System.currentTimeMillis() - lastPasswordChange) > getPasswordChangeInterval()) {
return true;
}
return false;
}
/**
* Checks if a user is required to change his password now.
*
* @param cms the current CMS context
* @param user the user to check
*
* @return true if the user should be asked to change his password
*/
public boolean requiresUserDataCheck(CmsObject cms, CmsUser user) {
if (user.isManaged()
|| user.isWebuser()
|| OpenCms.getDefaultUsers().isDefaultUser(user.getName())
|| OpenCms.getRoleManager().hasRole(cms, user.getName(), CmsRole.ROOT_ADMIN)) {
return false;
}
String lastCheckStr = (String)user.getAdditionalInfo().get(
CmsUserSettings.ADDITIONAL_INFO_LAST_USER_DATA_CHECK);
if (lastCheckStr == null) {
return !CmsStringUtil.isEmptyOrWhitespaceOnly(getUserDataCheckIntervalStr());
}
long lastCheck = Long.parseLong(lastCheckStr);
if ((System.currentTimeMillis() - lastCheck) > getUserDataCheckInterval()) {
return true;
}
return false;
}
/**
* Resets lock from user.
*
* @param username to reset lock for
*/
public void resetUserTempDisable(String username) {
Set data = TEMP_DISABLED_USER.get(username);
if (data == null) {
return;
}
for (CmsUserData userData : data) {
userData.reset();
}
TEMP_DISABLED_USER.remove(username);
}
/**
* Sets the before login message to display on the login form.
*
* This operation requires that the current user has role permissions of {@link CmsRole#ROOT_ADMIN}
.
*
* @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 setBeforeLoginMessage(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_beforeLoginMessage = message;
if (m_beforeLoginMessage != null) {
m_beforeLoginMessage.setFrozen();
}
}
/**
* Sets the login message to display if a user logs in.
*
* This operation requires that the current user has role permissions of {@link CmsRole#ROOT_ADMIN}
.
*
* @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();
}
}
/**
* Unlocks a user who has exceeded his number of failed login attempts so that he can try to log in again.
* This requires the "account manager" role.
*
* @param cms the current CMS context
* @param user the user to unlock
*
* @throws CmsRoleViolationException if the permission check fails
*/
public void unlockUser(CmsObject cms, CmsUser user) throws CmsRoleViolationException {
OpenCms.getRoleManager().checkRole(cms, CmsRole.ACCOUNT_MANAGER.forOrgUnit(cms.getRequestContext().getOuFqn()));
Set keysToRemove = getKeysForUser(user);
for (String keyToRemove : keysToRemove) {
m_storage.remove(keyToRemove);
}
}
/**
* Adds an invalid attempt to login for the given user / IP to the storage.
*
* In case the configured threshold is reached, the user is disabled for the configured time.
*
* @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.
*
* @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);
}
/**
* Helper method to get all the storage keys that match a user's name.
*
* @param user the user for which to get the storage keys
*
* @return the set of storage keys
*/
private Set getKeysForUser(CmsUser user) {
Set keysToRemove = new HashSet();
for (Map.Entry entry : m_storage.entrySet()) {
String key = entry.getKey();
int separatorPos = key.lastIndexOf(KEY_SEPARATOR);
String prefix = key.substring(0, separatorPos);
if (user.getName().equals(prefix)) {
keysToRemove.add(key);
}
}
return keysToRemove;
}
}