diff --git a/core/src/com/cloud/user/UserAccountVO.java b/core/src/com/cloud/user/UserAccountVO.java index 5e7c018568a..1236061475b 100644 --- a/core/src/com/cloud/user/UserAccountVO.java +++ b/core/src/com/cloud/user/UserAccountVO.java @@ -83,6 +83,9 @@ public class UserAccountVO implements UserAccount { @Column(name="is_registered") boolean registered; + @Column (name="incorrect_login_attempts") + int loginAttempts; + @Column(name="account_name", table="account", insertable=false, updatable=false) private String accountName = null; @@ -269,4 +272,12 @@ public class UserAccountVO implements UserAccount { public void setRegistered(boolean registered) { this.registered = registered; } -} \ No newline at end of file + + public void setLoginAttempts(int loginAttempts) { + this.loginAttempts = loginAttempts; + } + + public int getLoginAttempts() { + return loginAttempts; + } +} diff --git a/server/src/com/cloud/configuration/Config.java b/server/src/com/cloud/configuration/Config.java index 85548362c24..764e5ee9377 100755 --- a/server/src/com/cloud/configuration/Config.java +++ b/server/src/com/cloud/configuration/Config.java @@ -229,7 +229,7 @@ public enum Config { EnableEC2API("Advanced", ManagementServer.class, Boolean.class, "enable.ec2.api", "false", "enable EC2 API on CloudStack", null), EnableS3API("Advanced", ManagementServer.class, Boolean.class, "enable.s3.api", "false", "enable Amazon S3 API on CloudStack", null), RecreateSystemVmEnabled("Advanced", ManagementServer.class, Boolean.class, "recreate.systemvm.enabled", "false", "If true, will recreate system vm root disk whenever starting system vm", "true,false"), - + IncorrectLoginAttemptsAllowed("Advanced", ManagementServer.class, Integer.class, "incorrect.login.attempts.allowed", "5", "Incorrect login attempts allowed before the user is disabled", null), // Ovm OvmPublicNetwork("Hidden", ManagementServer.class, String.class, "ovm.public.network.device", null, "Specify the public bridge on host for public network", null), OvmPrivateNetwork("Hidden", ManagementServer.class, String.class, "ovm.private.network.device", null, "Specify the private bridge on host for private network", null), diff --git a/server/src/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/com/cloud/configuration/ConfigurationManagerImpl.java index f8deb159f7b..ef940e89f61 100755 --- a/server/src/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/com/cloud/configuration/ConfigurationManagerImpl.java @@ -274,6 +274,7 @@ public class ConfigurationManagerImpl implements ConfigurationManager, Configura configValuesForValidation.add("storage.cleanup.interval"); configValuesForValidation.add("wait"); configValuesForValidation.add("xen.heartbeat.interval"); + configValuesForValidation.add("incorrect.login.attempts.allowed"); } @Override diff --git a/server/src/com/cloud/user/AccountManagerImpl.java b/server/src/com/cloud/user/AccountManagerImpl.java index 38153f30618..fa9fafb617e 100755 --- a/server/src/com/cloud/user/AccountManagerImpl.java +++ b/server/src/com/cloud/user/AccountManagerImpl.java @@ -224,6 +224,8 @@ public class AccountManagerImpl implements AccountManager, AccountService, Manag private final ScheduledExecutorService _executor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("AccountChecker")); + int _allowedLoginAttempts; + UserVO _systemUser; AccountVO _systemAccount; @Inject(adapter = SecurityChecker.class) @@ -248,6 +250,9 @@ public class AccountManagerImpl implements AccountManager, AccountService, Manag ConfigurationDao configDao = locator.getDao(ConfigurationDao.class); Map configs = configDao.getConfiguration(params); + String loginAttempts = configs.get(Config.IncorrectLoginAttemptsAllowed.key()); + _allowedLoginAttempts = NumbersUtil.parseInt(loginAttempts, 5); + String value = configs.get(Config.AccountCleanupInterval.key()); _cleanupInterval = NumbersUtil.parseInt(value, 60 * 60 * 24); // 1 day. @@ -302,6 +307,13 @@ public class AccountManagerImpl implements AccountManager, AccountService, Manag return (accountType == Account.ACCOUNT_TYPE_RESOURCE_DOMAIN_ADMIN); } + public boolean isInternalAccount(short accountType) { + if (isRootAdmin(accountType) || (accountType == Account.ACCOUNT_ID_SYSTEM)) { + return true; + } + return false; + } + @Override public void checkAccess(Account caller, Domain domain) throws PermissionDeniedException { for (SecurityChecker checker : _securityCheckers) { @@ -420,6 +432,25 @@ public class AccountManagerImpl implements AccountManager, AccountService, Manag } + @DB + public void updateLoginAttempts(Long id, int attempts, boolean toDisable) { + Transaction txn = Transaction.currentTxn(); + txn.start(); + try { + UserAccountVO user = null; + user = _userAccountDao.lockRow(id, true); + user.setLoginAttempts(attempts); + if(toDisable) { + user.setState(State.disabled.toString()); + } + _userAccountDao.update(id, user); + txn.commit(); + } catch (Exception e) { + s_logger.error("Failed to update login attempts for user with id " + id ); + } + txn.close(); + } + private boolean doSetUserStatus(long userId, State state) { UserVO userForUpdate = _userDao.createForUpdate(); userForUpdate.setState(state); @@ -732,7 +763,7 @@ public class AccountManagerImpl implements AccountManager, AccountService, Manag if (domainId == null) { domainId = DomainVO.ROOT_DOMAIN; } - + if (userName.isEmpty()) { throw new InvalidParameterValueException("Username is empty"); } @@ -740,7 +771,7 @@ public class AccountManagerImpl implements AccountManager, AccountService, Manag if (firstName.isEmpty()) { throw new InvalidParameterValueException("Firstname is empty"); } - + if (lastName.isEmpty()) { throw new InvalidParameterValueException("Lastname is empty"); } @@ -1002,6 +1033,8 @@ public class AccountManagerImpl implements AccountManager, AccountService, Manag txn.commit(); if (success) { + // whenever the user is successfully enabled, reset the login attempts to zero + updateLoginAttempts(userId, 0, false); return _userAccountDao.findById(userId); } else { throw new CloudRuntimeException("Unable to enable user " + userId); @@ -1818,11 +1851,36 @@ public class AccountManagerImpl implements AccountManager, AccountService, Manag throw new CloudAuthenticationException("User " + username + " in domain " + domainName + " is disabled/locked (or account is disabled/locked)"); // return null; } + // Whenever the user is able to log in successfully, reset the login attempts to zero + if(!isInternalAccount(userAccount.getType())) + updateLoginAttempts(userAccount.getId(), 0, false); + return userAccount; } else { if (s_logger.isDebugEnabled()) { s_logger.debug("Unable to authenticate user with username " + username + " in domain " + domainId); } + + UserAccount userAccount = _userAccountDao.getUserAccount(username, domainId); + UserAccountVO user = _userAccountDao.findById(userAccount.getId()); + if (user != null) { + if ((user.getState().toString()).equals("enabled")) { + if (!isInternalAccount(user.getType())) { + //Internal accounts are not disabled + int attemptsMade = user.getLoginAttempts() + 1; + if (attemptsMade < _allowedLoginAttempts) { + updateLoginAttempts(userAccount.getId(), attemptsMade, false); + s_logger.warn("Login attempt failed. You have " + ( _allowedLoginAttempts - attemptsMade ) + " attempt(s) remaining"); + } else { + updateLoginAttempts(userAccount.getId(), _allowedLoginAttempts, true); + s_logger.warn("User " + user.getUsername() + " has been disabled due to multiple failed login attempts." + + " Please contact admin."); + } + } + } else { + s_logger.info("User " + user.getUsername() + " is disabled/locked"); + } + } return null; } } diff --git a/setup/db/create-schema.sql b/setup/db/create-schema.sql index bb6dc5dd576..5b6dc0447c7 100755 --- a/setup/db/create-schema.sql +++ b/setup/db/create-schema.sql @@ -890,6 +890,7 @@ CREATE TABLE `cloud`.`user` ( `timezone` varchar(30) default NULL, `registration_token` varchar(255) default NULL, `is_registered` tinyint NOT NULL DEFAULT 0 COMMENT '1: yes, 0: no', + `incorrect_login_attempts` integer unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`id`), INDEX `i_user__removed`(`removed`), INDEX `i_user__secret_key_removed`(`secret_key`, `removed`), diff --git a/setup/db/db/schema-302to40.sql b/setup/db/db/schema-302to40.sql index d70697b440c..091615624a2 100644 --- a/setup/db/db/schema-302to40.sql +++ b/setup/db/db/schema-302to40.sql @@ -472,3 +472,6 @@ INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Network', 'DEFAULT', 'manage INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Network', 'DEFAULT', 'management-server', 'site2site.vpn.customergateway.subnets.limit', '10', 'The maximum number of subnets per customer gateway'); INSERT IGNORE INTO `cloud`.`guest_os_category` VALUES ('11','None',NULL); +ALTER TABLE `cloud`.`user` ADD COLUMN `incorrect_login_attempts` integer unsigned NOT NULL DEFAULT '0'; +INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Advanced', 'DEFAULT', 'management-server', 'incorrect.login.attempts.allowed', '5', 'Incorrect login attempts allowed before the user is disabled'); +