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]