Repository: ambari Updated Branches: refs/heads/branch-feature-AMBARI-20859 317905e40 -> 3cefb74cd
AMBARI-21680. Prevent users from authenticating if they exceed a configured number of login failures (amagyar) Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/3cefb74c Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/3cefb74c Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/3cefb74c Branch: refs/heads/branch-feature-AMBARI-20859 Commit: 3cefb74cdae3a836ee1896a30dca713e44b95f98 Parents: 317905e Author: Attila Magyar <amag...@hortonworks.com> Authored: Thu Aug 10 11:37:00 2017 +0200 Committer: Attila Magyar <amag...@hortonworks.com> Committed: Mon Aug 21 13:37:28 2017 +0200 ---------------------------------------------------------------------- .../scripts/controllers/users/UsersShowCtrl.js | 6 ++++- .../ui/admin-web/app/scripts/i18n.config.js | 2 ++ .../ui/admin-web/app/scripts/services/User.js | 9 +++++++ .../ui/admin-web/app/views/users/show.html | 13 ++++++++++ ambari-server/docs/configuration/index.md | 1 + .../server/configuration/Configuration.java | 13 ++++++++++ .../ambari/server/controller/AmbariServer.java | 5 ++-- .../ambari/server/controller/UserRequest.java | 10 ++++++++ .../internal/UserResourceProvider.java | 11 ++++++++ .../AmbariAuthenticationEventHandlerImpl.java | 2 +- .../TooManyLoginFailuresException.java | 27 ++++++++++++++++++++ .../authorization/AmbariLocalUserProvider.java | 12 +++++++-- .../AmbariLocalUserProviderTest.java | 18 +++++++++++++ 13 files changed, 123 insertions(+), 6 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/users/UsersShowCtrl.js ---------------------------------------------------------------------- diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/users/UsersShowCtrl.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/users/UsersShowCtrl.js index 200872e..014703d 100644 --- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/users/UsersShowCtrl.js +++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/users/UsersShowCtrl.js @@ -243,7 +243,11 @@ angular.module('ambariAdminConsole') }); }); }; - + $scope.resetLoginFailures = function() { + User.resetLoginFailures($scope.user.user_name).then(function() { + $scope.user.consecutive_failures = 0; + }); + }; // Load privileges function loadPrivileges(){ User.getPrivileges($routeParams.id).then(function(data) { http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-admin/src/main/resources/ui/admin-web/app/scripts/i18n.config.js ---------------------------------------------------------------------- diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/i18n.config.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/i18n.config.js index 43b32da..f83a8b3 100644 --- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/i18n.config.js +++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/i18n.config.js @@ -302,6 +302,8 @@ angular.module('ambariAdminConsole') 'users.inactive': 'Inactive', 'users.status': 'Status', 'users.password': 'Password', + 'users.loginFailures': 'Login failures', + 'users.resetLoginFailures': 'Reset', 'users.passwordConfirmation': 'Password Ñonfirmation', 'users.userIsAdmin': 'This user is an Ambari Admin and has all privileges.', 'users.showAll': 'Show all users', http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/User.js ---------------------------------------------------------------------- diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/User.js b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/User.js index ac50653..1393362 100644 --- a/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/User.js +++ b/ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/User.js @@ -85,6 +85,15 @@ angular.module('ambariAdminConsole') } }); }, + resetLoginFailures: function(userId) { + return $http({ + method: 'PUT', + url: Settings.baseUrl + '/users/' + userId, + data: { + 'Users/consecutive_failures': 0 + } + }); + }, /** * Generate user info to display by response data from API. * Generally this is a single point to manage all required and useful data http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-admin/src/main/resources/ui/admin-web/app/views/users/show.html ---------------------------------------------------------------------- diff --git a/ambari-admin/src/main/resources/ui/admin-web/app/views/users/show.html b/ambari-admin/src/main/resources/ui/admin-web/app/views/users/show.html index f965c5d..de4f14a 100644 --- a/ambari-admin/src/main/resources/ui/admin-web/app/views/users/show.html +++ b/ambari-admin/src/main/resources/ui/admin-web/app/views/users/show.html @@ -60,6 +60,19 @@ </div> </div> <div class="form-group"> + <label for="" class="col-sm-2 control-label">{{'users.loginFailures' | translate}}</label> + <label class="col-sm-10 form-control-static">{{user.consecutive_failures}}</label> + </div> + <div class="form-group"> + <div for="" class="col-sm-2"></div> + <div class="col-sm-10"> + <div ng-switch="user.user_type != 'LOCAL'"> + <button class="btn deleteuser-btn disabled btn-default" ng-switch-when="true" tooltip="{{'users.resetLoginFailures' | translate}}">{{'users.resetLoginFailures' | translate}}</button> + <a href ng-click="resetLoginFailures()" ng-switch-when="false" class="btn btn-default changepassword">{{'users.resetLoginFailures' | translate}}</a> + </div> + </div> + </div> + <div class="form-group"> <label for="groups" class="col-sm-2 control-label">{{getUserMembership(user.user_type)}}</label> <div class="col-sm-10"> <editable-list items-source="editingGroupsList" resource-type="Group" editable="user.user_type == 'LOCAL'"></editable-list> http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-server/docs/configuration/index.md ---------------------------------------------------------------------- diff --git a/ambari-server/docs/configuration/index.md b/ambari-server/docs/configuration/index.md index 9dbe9c4..395687d 100644 --- a/ambari-server/docs/configuration/index.md +++ b/ambari-server/docs/configuration/index.md @@ -109,6 +109,7 @@ The following are the properties which can be used to configure Ambari. | authentication.ldap.userSearchFilter | A filter used to lookup a user in LDAP based on the Ambari user name<br/><br/>The following are examples of valid values:<ul><li>`(&({usernameAttribute}={0})(objectClass={userObjectClass}))`</ul> |`(&({usernameAttribute}={0})(objectClass={userObjectClass}))` | | authentication.ldap.username.forceLowercase | Declares whether to force the ldap user name to be lowercase or leave as-is. This is useful when local user names are expected to be lowercase but the LDAP user names are not. |`false` | | authentication.ldap.usernameAttribute | The attribute used for determining the user name, such as `uid`. |`uid` | +| authentication.local.max.failures | The maximum number of authentication attempts permitted to a local user. Once the number of failures reaches this limit the user will be locked out. 0 indicates unlimited failures. |`10` | | authorization.ldap.adminGroupMappingRules | A comma-separate list of groups which would give a user administrative access to Ambari when syncing from LDAP. This is only used when `authorization.ldap.groupSearchFilter` is blank.<br/><br/>The following are examples of valid values:<ul><li>`administrators`<li>`Hadoop Admins,Hadoop Admins.*,DC Admins,.*Hadoop Operators`</ul> |`Ambari Administrators` | | authorization.ldap.groupSearchFilter | The DN to use when searching for LDAP groups. | | | auto.group.creation | The auto group creation by Ambari |`false` | http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java b/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java index 4f787c6..fa6e9f9 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java @@ -2765,6 +2765,15 @@ public class Configuration { "notification.dispatch.alert.script.directory",AmbariPath.getPath("/var/lib/ambari-server/resources/scripts")); + /** + * The maximum number of authentication attempts permitted to a local user. Once the number of failures reaches this limit the user will be locked out. 0 indicates unlimited failures + */ + @Markdown( + description = "The maximum number of authentication attempts permitted to a local user. Once the number of failures reaches this limit the user will be locked out. 0 indicates unlimited failures.") + public static final ConfigurationProperty<Integer> MAX_LOCAL_AUTHENTICATION_FAILURES = new ConfigurationProperty<>( + "authentication.local.max.failures", 10); + + private static final Logger LOG = LoggerFactory.getLogger( Configuration.class); @@ -6189,4 +6198,8 @@ public class Configuration { public String getAutoGroupCreation() { return getProperty(AUTO_GROUP_CREATION); } + + public int getMaxAuthenticationFailures() { + return Integer.parseInt(getProperty(MAX_LOCAL_AUTHENTICATION_FAILURES)); + } } http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java index b52e2b1..21ab757 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java @@ -339,8 +339,9 @@ public class AmbariServer { injector.getInstance(PermissionHelper.class)); factory.registerSingleton("ambariLdapAuthenticationProvider", injector.getInstance(AmbariLdapAuthenticationProvider.class)); - factory.registerSingleton("ambariLocalAuthenticationProvider", - injector.getInstance(AmbariLocalUserProvider.class)); + AmbariLocalUserProvider ambariLocalUserProvider = injector.getInstance(AmbariLocalUserProvider.class); + ambariLocalUserProvider.setMaxConsecutiveFailures(configs.getMaxAuthenticationFailures()); + factory.registerSingleton("ambariLocalAuthenticationProvider", ambariLocalUserProvider); factory.registerSingleton("ambariLdapDataPopulator", injector.getInstance(AmbariLdapDataPopulator.class)); factory.registerSingleton("ambariUserAuthorizationFilter", http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-server/src/main/java/org/apache/ambari/server/controller/UserRequest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/UserRequest.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/UserRequest.java index d0836a9..2f155b6 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/controller/UserRequest.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/UserRequest.java @@ -35,6 +35,7 @@ public class UserRequest { private String displayName; private String localUserName; + private Integer consecutiveFailures; public UserRequest(String name) { this.userName = name; @@ -99,6 +100,15 @@ public class UserRequest { this.localUserName = localUserName; } + @ApiModelProperty(name = UserResourceProvider.CONSECUTIVE_FAILURES_PROPERTY_ID) + public Integer getConsecutiveFailures() { + return consecutiveFailures; + } + + public void setConsecutiveFailures(Integer consecutiveFailures) { + this.consecutiveFailures = consecutiveFailures; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/UserResourceProvider.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/UserResourceProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/UserResourceProvider.java index a2d9917..99f88ca 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/UserResourceProvider.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/UserResourceProvider.java @@ -333,6 +333,10 @@ public class UserResourceProvider extends AbstractControllerResourceProvider imp request.setAdmin(Boolean.valueOf(properties.get(USER_ADMIN_PROPERTY_ID).toString())); } + if (null != properties.get(USER_CONSECUTIVE_FAILURES_PROPERTY_ID)) { + request.setConsecutiveFailures(Integer.parseInt(properties.get(USER_CONSECUTIVE_FAILURES_PROPERTY_ID).toString())); + } + return request; } @@ -476,6 +480,13 @@ public class UserResourceProvider extends AbstractControllerResourceProvider imp if (request.getPassword() != null) { addOrUpdateLocalAuthenticationSource(asUserAdministrator, userEntity, request.getPassword(), request.getOldPassword()); } + + if (request.getConsecutiveFailures() != null) { + if (!asUserAdministrator) { + throw new AuthorizationException("The authenticated user is not authorized to update the requested resource property"); + } + users.safelyUpdateUserEntity(userEntity, user -> user.setConsecutiveFailures(request.getConsecutiveFailures())); + } } } http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandlerImpl.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandlerImpl.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandlerImpl.java index 3a5a66b..2a89437 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandlerImpl.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandlerImpl.java @@ -125,7 +125,7 @@ public class AmbariAuthenticationEventHandlerImpl implements AmbariAuthenticatio AuditEvent loginFailedAuditEvent = LoginAuditEvent.builder() .withRemoteIp(RequestUtils.getRemoteAddress(servletRequest)) .withTimestamp(System.currentTimeMillis()) - .withReasonOfFailure("Invalid username/password combination") + .withReasonOfFailure(message) .withConsecutiveFailures(consecutiveFailures) .withUserName(username) .build(); http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/TooManyLoginFailuresException.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/TooManyLoginFailuresException.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/TooManyLoginFailuresException.java new file mode 100644 index 0000000..b172079 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/TooManyLoginFailuresException.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ambari.server.security.authentication; + +/** + * Thrown when the consecutive authentication failures exceed the limit + */ +public class TooManyLoginFailuresException extends AmbariAuthenticationException { + public TooManyLoginFailuresException(String username) { + super(username, "Too many authentication failures"); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProvider.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProvider.java index 2c8bf12..2a2e397 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProvider.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProvider.java @@ -23,6 +23,7 @@ import org.apache.ambari.server.orm.dao.UserDAO; import org.apache.ambari.server.orm.entities.UserAuthenticationEntity; import org.apache.ambari.server.orm.entities.UserEntity; import org.apache.ambari.server.security.authentication.InvalidUsernamePasswordCombinationException; +import org.apache.ambari.server.security.authentication.TooManyLoginFailuresException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -36,11 +37,10 @@ import com.google.inject.Inject; public class AmbariLocalUserProvider extends AbstractUserDetailsAuthenticationProvider { private static final Logger LOG = LoggerFactory.getLogger(AmbariLocalUserProvider.class); - private UserDAO userDAO; private Users users; private PasswordEncoder passwordEncoder; - + private int maxConsecutiveFailures = 0; @Inject public AmbariLocalUserProvider(UserDAO userDAO, Users users, PasswordEncoder passwordEncoder) { @@ -75,6 +75,10 @@ public class AmbariLocalUserProvider extends AbstractUserDetailsAuthenticationPr throw new InvalidUsernamePasswordCombinationException(userName); } + if (maxConsecutiveFailures > 0 && userEntity.getConsecutiveFailures() >= maxConsecutiveFailures) { + throw new TooManyLoginFailuresException(userName); + } + if (authentication.getCredentials() == null) { LOG.debug("Authentication failed: no credentials provided"); throw new InvalidUsernamePasswordCombinationException(userName); @@ -111,4 +115,8 @@ public class AmbariLocalUserProvider extends AbstractUserDetailsAuthenticationPr public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } + + public void setMaxConsecutiveFailures(int maxConsecutiveFailures) { + this.maxConsecutiveFailures = maxConsecutiveFailures; + } } http://git-wip-us.apache.org/repos/asf/ambari/blob/3cefb74c/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProviderTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProviderTest.java b/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProviderTest.java index 133fc9f..fb4ebf9 100644 --- a/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProviderTest.java +++ b/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProviderTest.java @@ -36,6 +36,7 @@ import org.apache.ambari.server.orm.entities.PrincipalEntity; import org.apache.ambari.server.orm.entities.UserAuthenticationEntity; import org.apache.ambari.server.orm.entities.UserEntity; import org.apache.ambari.server.security.authentication.InvalidUsernamePasswordCombinationException; +import org.apache.ambari.server.security.authentication.TooManyLoginFailuresException; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -153,7 +154,24 @@ public class AmbariLocalUserProviderTest { ambariLocalUserProvider.authenticate(authentication); } + @Test(expected = TooManyLoginFailuresException.class) + public void testUserIsLockedOutAfterConsecutiveFailures() { + Users users = createMock(Users.class); + UserDAO userDAO = createMock(UserDAO.class); + Authentication authentication = createMock(Authentication.class); + UserEntity userEntity = combineUserEntity(); + userEntity.setConsecutiveFailures(3); + expect(authentication.getName()).andReturn(TEST_USER_NAME).anyTimes(); + expect(authentication.getCredentials()).andReturn(TEST_USER_PASS).anyTimes(); + expect(userDAO.findUserByName(TEST_USER_NAME)).andReturn(userEntity).anyTimes(); + expect(users.getUserAuthorities(userEntity)).andReturn(null); + + replay(users, userDAO, authentication); + AmbariLocalUserProvider ambariLocalUserProvider = new AmbariLocalUserProvider(userDAO, users, passwordEncoder); + ambariLocalUserProvider.setMaxConsecutiveFailures(3); + ambariLocalUserProvider.authenticate(authentication); + } private UserEntity combineUserEntity() { PrincipalEntity principalEntity = new PrincipalEntity();