This is an automated email from the ASF dual-hosted git repository.

vjasani pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 71f9f01564 AMBARI-26061 : Add password validation criteria for ambari 
local users (#3776)
71f9f01564 is described below

commit 71f9f01564fedb81d46733e8291c5e5d62557cc1
Author: Himanshu Maurya <[email protected]>
AuthorDate: Thu Jun 27 06:40:35 2024 +0530

    AMBARI-26061 : Add password validation criteria for ambari local users 
(#3776)
---
 ambari-server/docs/configuration/index.md          |  1 +
 .../ambari/server/configuration/Configuration.java | 34 +++++++++++++++
 .../controller/internal/UserResourceProvider.java  |  2 +-
 .../orm/entities/UserAuthenticationEntity.java     | 26 +++++++++++
 .../server/security/authorization/Users.java       | 51 ++++++++++++++++++++--
 .../server/security/authorization/UsersTest.java   | 47 +++++++++++++++++++-
 6 files changed, 154 insertions(+), 7 deletions(-)

diff --git a/ambari-server/docs/configuration/index.md 
b/ambari-server/docs/configuration/index.md
index 4229a3db77..7a1b7184f6 100644
--- a/ambari-server/docs/configuration/index.md
+++ b/ambari-server/docs/configuration/index.md
@@ -168,6 +168,7 @@ The following are the properties which can be used to 
configure Ambari.
 | security.master.keystore.location | The location on the Ambari Server of the 
master keystore file. | | 
 | security.passwords.encryption.enabled | Whether security password encryption 
is enabled or not. In case it is we store passwords in their own file(s); 
otherwise we store passwords in the Ambari credential store. |`false` | 
 | security.server.cert_chain_name | The name of the file located in the 
`security.server.keys_dir` directory containing the CA certificate chain used 
to verify certificates during 2-way SSL communications. |`ca_chain.pem` | 
+| security.password.policy.history.count | Password policy to mandate that new 
password should be different from previous passwords, this would be based on 
the history count configured by the user. Valid values are 1 to 10. | `1` |
 | security.server.cert_name | The name of the file located in the 
`security.server.keys_dir` directory where certificates will be generated when 
Ambari uses the `openssl ca` command. |`ca.crt` | 
 | security.server.crt_pass | The password for the keystores, truststores, and 
certificates. If not specified, then `security.server.crt_pass_file` should be 
used | | 
 | security.server.crt_pass.len | The length of the randomly generated password 
for keystores and truststores.  |`50` | 
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 85920f6c60..83a4655cc7 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
@@ -325,6 +325,16 @@ public class Configuration {
   // Ambari server log4j file name
   public static final String AMBARI_LOG_FILE = "log4j.properties";
 
+  /**
+   * The maximum value of password history count which can be configured by 
the user.
+   */
+  public static final int MAXIMUM_PASSWORD_HISTORY_LIMIT = 10;
+
+  /**
+   * The default and minimum value of password history count which can be 
configured by the user.
+   */
+  public static final int MINIMUM_PASSWORD_HISTORY_LIMIT = 1;
+
   @Markdown(
           description = "Interval for heartbeat presence checks.",
           examples = {"60000","600000"} )
@@ -530,6 +540,14 @@ public class Configuration {
   public static final ConfigurationProperty<String> 
PASSWORD_POLICY_DESCRIPTION = new ConfigurationProperty<>(
       "security.password.policy.description", "");
 
+  /**
+   * Password history count to validate how many previous passwords should be 
checked before updating user password.
+   */
+  @Markdown(
+          description = "Password policy to mandate that new password should 
be different from previous passwords, this would be based on the history count 
configured by the user. Valid values are 1 to 10.")
+  public static final ConfigurationProperty<String> 
PASSWORD_POLICY_HISTORY_COUNT = new ConfigurationProperty<>(
+          "security.password.policy.history.count", "1");
+
   /**
    * Determines whether the Ambari Agent host names should be validated against
    * a regular expression to ensure that they are well-formed.
@@ -4138,6 +4156,22 @@ public class Configuration {
     return getProperty(PASSWORD_POLICY_DESCRIPTION);
   }
 
+  /**
+   * @return Password policy history count
+   */
+  public int getPasswordPolicyHistoryCount() {
+    int historyCount = 
Integer.parseInt(getProperty(PASSWORD_POLICY_HISTORY_COUNT));
+    if(historyCount < MINIMUM_PASSWORD_HISTORY_LIMIT){
+      historyCount = MINIMUM_PASSWORD_HISTORY_LIMIT;
+      LOG.debug("Invalid password history count detected. The count has been 
automatically set to the minimum permissible value of 
{}.",MINIMUM_PASSWORD_HISTORY_LIMIT);
+    }
+    if(historyCount > MAXIMUM_PASSWORD_HISTORY_LIMIT){
+      historyCount = MAXIMUM_PASSWORD_HISTORY_LIMIT;
+      LOG.debug("Invalid password history count detected. The count has been 
automatically set to the maximum permissible value of 
{}.",MAXIMUM_PASSWORD_HISTORY_LIMIT);
+    }
+    return historyCount;
+  }
+
   public JPATableGenerationStrategy getJPATableGenerationStrategy() {
     return JPATableGenerationStrategy.fromString(
         System.getProperty(SERVER_JDBC_GENERATE_TABLES.getKey()));
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 2fa99cf2a9..df1a924434 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
@@ -384,7 +384,7 @@ public class UserResourceProvider extends 
AbstractControllerResourceProvider imp
       // Setting a user's the password here is to allow for backward 
compatibility with pre-Ambari-3.0
       // versions so that the contract for REST API V1 is maintained.
       if (!StringUtils.isEmpty(password)) {
-        users.validatePassword(password);
+        users.validatePassword(password, username);
       }
 
       UserEntity userEntity = users.createUser(username, localUserName, 
displayName, request.isActive());
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/UserAuthenticationEntity.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/UserAuthenticationEntity.java
index e90949481d..a2324ea588 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/UserAuthenticationEntity.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/UserAuthenticationEntity.java
@@ -17,6 +17,10 @@
  */
 package org.apache.ambari.server.orm.entities;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 import javax.persistence.Basic;
 import javax.persistence.Column;
 import javax.persistence.Entity;
@@ -101,6 +105,14 @@ public class UserAuthenticationEntity {
   }
 
   public String getAuthenticationKey() {
+    int firstCommaIndex = authenticationKey.indexOf(",");
+    if (firstCommaIndex != -1) {
+      return authenticationKey.substring(0, firstCommaIndex);
+    }
+    return authenticationKey;
+  }
+
+  public String getFullAuthenticationKey() {
     return authenticationKey;
   }
 
@@ -108,6 +120,20 @@ public class UserAuthenticationEntity {
     this.authenticationKey = authenticationKey;
   }
 
+  public void updateAuthenticationKey(String newAuthenticationKey, int 
historyCount) {
+    if (this.authenticationKey == null || this.authenticationKey.isEmpty()) {
+        this.authenticationKey = newAuthenticationKey;
+    } else {
+        String[] keys = this.authenticationKey.split(",");
+        List<String> keyList = new ArrayList<>(Arrays.asList(keys));
+        if (keyList.size() >= historyCount) {
+            keyList = keyList.subList(0, historyCount - 1);
+        }
+        keyList.add(0, newAuthenticationKey);
+        this.authenticationKey = String.join(",", keyList);
+    }
+}
+
   public long getCreateTime() {
     return createTime;
   }
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java
index d8d36c3e0a..69ad595e61 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java
@@ -1259,14 +1259,14 @@ public class Users {
           throw new AmbariException("Wrong current password provided");
         }
 
-        validatePassword(newKey);
+        validatePassword(newKey, 
userAuthenticationEntity.getUser().getUserName(), 
userAuthenticationEntity.getFullAuthenticationKey());
 
         // If we get here the authenticated user is authorized to change the 
password for the subject
         // user and the correct current password was supplied (if required).
-        
userAuthenticationEntity.setAuthenticationKey(passwordEncoder.encode(newKey));
+        
userAuthenticationEntity.updateAuthenticationKey(passwordEncoder.encode(newKey),
 configuration.getPasswordPolicyHistoryCount());
       } else {
         // If we get here the authenticated user is authorized to change the 
key for the subject.
-        userAuthenticationEntity.setAuthenticationKey(newKey);
+        userAuthenticationEntity.updateAuthenticationKey(newKey, 
configuration.getPasswordPolicyHistoryCount());
       }
 
       userAuthenticationDAO.merge(userAuthenticationEntity);
@@ -1458,7 +1458,7 @@ public class Users {
   public void addLocalAuthentication(UserEntity userEntity, String password, 
boolean persist) throws AmbariException {
 
     // Ensure the password meets configured minimal requirements, if any
-    validatePassword(password);
+    validatePassword(password, userEntity.getUserName());
 
     // Encode the password..
     String encodedPassword = passwordEncoder.encode(password);
@@ -1772,6 +1772,49 @@ public class Users {
     }
   }
 
+  /**
+   * Validates the password meets configured requirements according 
ambari.properties and it does not contain the username.
+   *
+   * <p>
+   *
+   * @param password the password
+   * @param userName the username
+   * @throws  IllegalArgumentException if password does not meet the password 
policy requirements
+   */
+  public void validatePassword(String password, String userName) {
+    validatePassword(password);
+    if (StringUtils.isNotEmpty(userName) && 
Pattern.compile(Pattern.quote(userName), 
Pattern.CASE_INSENSITIVE).matcher(password).find()) {
+      final String msg = "The password does not meet the Ambari user password 
policy : password cannot contain the username";
+      throw new IllegalArgumentException(msg);
+    }
+  }
+
+  /**
+   * Validates the password meets configured requirements according 
ambari.properties, does not contain the username and different from previous 
passwords
+   *
+   * <p>
+   *
+   * @param password the password
+   * @param userName the username
+   * @param authenticationKey the authenticationKey associated with previous 
passwords
+   * @throws  IllegalArgumentException if password does not meet the password 
policy requirements
+   */
+  public void validatePassword(String password, String userName, String 
authenticationKey) {
+    validatePassword(password, userName);
+    boolean isMatched = false;
+    String[] previousPasswords = StringUtils.split(authenticationKey, ",");
+    for (String previousPassword : previousPasswords) {
+      if (passwordEncoder.matches(password, previousPassword)) {
+        isMatched = true;
+        break;
+      }
+    }
+    if (isMatched) {
+      final String msg = "The password does not meet the Ambari user password 
policy : new password can not be same as previous " + 
configuration.getPasswordPolicyHistoryCount() + " passwords.";
+      throw new IllegalArgumentException(msg);
+    }
+  }
+
   /**
    * Validator is an interface to be implemented by authentication type 
specific validators to ensure
    * new user authentication records meet the specific requirements for the 
relative authentication
diff --git 
a/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/UsersTest.java
 
b/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/UsersTest.java
index 9a3d237a4d..c6f2d924d7 100644
--- 
a/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/UsersTest.java
+++ 
b/ambari-server/src/test/java/org/apache/ambari/server/security/authorization/UsersTest.java
@@ -230,6 +230,41 @@ public class UsersTest extends EasyMockSupport {
     users.modifyAuthentication(entity, "wrong password", "world", false);
   }
 
+  @Test(expected = IllegalArgumentException.class)
+  public void modifyAuthentication_local_byAdminUser_usernameInPassword() 
throws AmbariException {
+    // given
+    UserAuthenticationEntity entity = initForModifyAuthentication();
+
+    // when
+    Users users = injector.getInstance(Users.class);
+    users.modifyAuthentication(entity, "admin1234", "User123", false);
+  }
+
+  @Test
+  public void modifyAuthentication_local_byAdminUser_usernameNotInPassword() 
throws AmbariException {
+    // given
+    UserAuthenticationEntity entity = initForModifyAuthentication();
+
+    // when
+    Users users = injector.getInstance(Users.class);
+    users.modifyAuthentication(entity, "admin1234", "User213", false);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void 
modifyAuthentication_local_byAdminUser_newPasswordSameAsPreviousPassword() 
throws AmbariException {
+    // given
+    UserAuthenticationEntity entity = initForModifyAuthentication();
+
+    // when
+    Users users = injector.getInstance(Users.class);
+
+    // 1st time update for user1 from hello to hello1
+    users.modifyAuthentication(entity, "admin1234", "hello1", false);
+
+    // 2nd time update for user1 from hello1 to hello
+    users.modifyAuthentication(entity, "admin1234", "hello", false);
+  }
+
   private void initForCreateUser(@Nullable UserEntity existingUser) {
     UserDAO userDao = createStrictMock(UserDAO.class);
     expect(userDao.findUserByName(anyString())).andReturn(existingUser);
@@ -242,10 +277,14 @@ public class UsersTest extends EasyMockSupport {
   }
 
   private UserAuthenticationEntity initForModifyAuthentication() {
+    UserEntity user1 = new UserEntity();
+    user1.setUserId(-1);
+    user1.setUserName("user1");
     UserAuthenticationEntity userEntity = new UserAuthenticationEntity();
     userEntity.setAuthenticationKey("hello");
     userEntity.setAuthenticationType(UserAuthenticationType.LOCAL);
-
+    userEntity.setUser(user1);
+    user1.setAuthenticationEntities(ImmutableList.of(userEntity));
     EntityManager manager = mock(EntityManager.class);
     expect(manager.merge(userEntity)).andReturn(userEntity).once();
 
@@ -287,7 +326,6 @@ public class UsersTest extends EasyMockSupport {
         bind(HookService.class).toInstance(createMock(HookService.class));
         
bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
         bind(PrincipalDAO.class).toInstance(createMock(PrincipalDAO.class));
-        
bind(Configuration.class).toInstance(createNiceMock(Configuration.class));
         bind(new TypeLiteral<Encryptor<AmbariServerConfiguration>>() 
{}).annotatedWith(Names.named("AmbariServerConfigurationEncryptor")).toInstance(Encryptor.NONE);
         
bind(AmbariLdapConfigurationProvider.class).toInstance(createMock(AmbariLdapConfigurationProvider.class));
 
@@ -297,6 +335,11 @@ public class UsersTest extends EasyMockSupport {
         expect(nopEncoder.encode(anyString())).andAnswer(
                 () -> (String)EasyMock.getCurrentArguments()[0]).anyTimes();
         bind(PasswordEncoder.class).toInstance(nopEncoder);
+
+        Configuration noConfig = createMock(Configuration.class);
+        
expect(noConfig.getPasswordPolicyHistoryCount()).andReturn(3).anyTimes();
+        expect(noConfig.getPasswordPolicyRegexp()).andReturn(".*").anyTimes();
+        bind(Configuration.class).toInstance(noConfig);
       }
     });
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to