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();

Reply via email to