From 3c718f27bffb3f28f83b3f632b7b7288c9a13302 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2016 15:43:52 -0700 Subject: [PATCH 1/6] GUACAMOLE-36: Define historical password records. Use password records for password age comparisons. --- .../jdbc/security/PasswordPolicyService.java | 8 +- .../guacamole/auth/jdbc/user/ModeledUser.java | 33 ++-- .../auth/jdbc/user/PasswordRecordModel.java | 156 ++++++++++++++++++ 3 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/PasswordRecordModel.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java index a47c03894..dfa980c0b 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java @@ -26,6 +26,7 @@ import java.util.regex.Pattern; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.apache.guacamole.auth.jdbc.user.ModeledUser; +import org.apache.guacamole.auth.jdbc.user.PasswordRecordModel; /** * Service which verifies compliance with the password policy configured via @@ -161,9 +162,14 @@ public class PasswordPolicyService { */ private long getPasswordAge(ModeledUser user) { + // If no password was set, then no time has elapsed + PasswordRecordModel previousPassword = user.getPreviousPassword(); + if (previousPassword == null) + return 0; + // Pull both current time and the time the password was last reset long currentTime = System.currentTimeMillis(); - long lastResetTime = user.getPreviousPasswordDate().getTime(); + long lastResetTime = previousPassword.getPasswordDate().getTime(); // Calculate the number of days elapsed since the password was last reset return TimeUnit.DAYS.convert(currentTime - lastResetTime, TimeUnit.MILLISECONDS); diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java index 2f1e58382..18a13eccb 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java @@ -189,10 +189,10 @@ public class ModeledUser extends ModeledDirectoryObject implements Us private String password = null; /** - * The time and date that this user's password was previously set (prior to - * being queried). If the user is new, this will be null. + * The data associated with this user's password at the time this user was + * queried. If the user is new, this will be null. */ - private Timestamp previousPasswordDate = null; + private PasswordRecordModel previousPassword = null; /** * Creates a new, empty ModeledUser. @@ -202,8 +202,13 @@ public class ModeledUser extends ModeledDirectoryObject implements Us @Override public void setModel(UserModel model) { + super.setModel(model); - this.previousPasswordDate = model.getPasswordDate(); + + // Store previous password, if any + if (model.getPasswordHash() != null) + this.previousPassword = new PasswordRecordModel(model); + } @Override @@ -240,19 +245,19 @@ public class ModeledUser extends ModeledDirectoryObject implements Us } /** - * Returns the time and date that this user's password was previously set. - * If the user is new, this will be null. Unlike getPasswordDate() of - * UserModel (which is updated automatically along with the password salt - * and hash whenever setPassword() is invoked), this value is unaffected by - * calls to setPassword(), and will always be the value stored in the - * database at the time this user was queried. + * Returns the data associated with this user's previous password as a + * password record. If the user is new, this will be null. Unlike the other + * password-related functions of UserModel, this data returned by this + * function is historical and is unaffected by calls to setPassword(). It + * will always return the values stored in the database at the time this + * user was queried. * * @return - * The time and date that this user's password was previously set, or - * null if the user is new. + * The data associated with this user's previous password, or null if + * the user is new. */ - public Timestamp getPreviousPasswordDate() { - return previousPasswordDate; + public PasswordRecordModel getPreviousPassword() { + return previousPassword; } /** diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/PasswordRecordModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/PasswordRecordModel.java new file mode 100644 index 000000000..4b34f8038 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/PasswordRecordModel.java @@ -0,0 +1,156 @@ +/* + * 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.guacamole.auth.jdbc.user; + +import java.sql.Timestamp; + +/** + * A single password record representing a previous password of a particular + * user, along with the time/date that password was set. + * + * @author Michael Jumper + */ +public class PasswordRecordModel { + + /** + * The database ID of the user associated with this password record. + */ + private Integer userID; + + /** + * The hash of the password and salt. + */ + private byte[] passwordHash; + + /** + * The random salt that was appended to the password prior to hashing. + */ + private byte[] passwordSalt; + + /** + * The date and time when this password was first set for the associated + * user. + */ + private Timestamp passwordDate; + + /** + * Creates a new, empty PasswordRecordModel. + */ + public PasswordRecordModel() { + } + + /** + * Creates a new PasswordRecordModel associated with the given user and + * populated with that user's password hash and salt. + * + * @param user + * The user to associate with this PasswordRecordModel. + */ + public PasswordRecordModel(UserModel user) { + this.userID = user.getObjectID(); + this.passwordHash = user.getPasswordHash(); + this.passwordSalt = user.getPasswordSalt(); + this.passwordDate = user.getPasswordDate(); + } + + /** + * Returns the database ID of the user associated with this password + * record. + * + * @return + * The database ID of the user associated with this password record. + */ + public Integer getUserID() { + return userID; + } + + /** + * Sets the database ID of the user associated with this password record. + * + * @param userID + * The database ID of the user to associate with this password + * record. + */ + public void setUserID(Integer userID) { + this.userID = userID; + } + + /** + * Returns the hash of the password and password salt. + * + * @return + * The hash of the password and password salt. + */ + public byte[] getPasswordHash() { + return passwordHash; + } + + /** + * Sets the hash of the password and password salt. + * + * @param passwordHash + * The hash of the password and password salt. + */ + public void setPasswordHash(byte[] passwordHash) { + this.passwordHash = passwordHash; + } + + /** + * Returns the random salt that was used when generating the password hash. + * + * @return + * The random salt that was used when generating the password hash. + */ + public byte[] getPasswordSalt() { + return passwordSalt; + } + + /** + * Sets the random salt that was used when generating the password hash. + * + * @param passwordSalt + * The random salt used when generating the password hash. + */ + public void setPasswordSalt(byte[] passwordSalt) { + this.passwordSalt = passwordSalt; + } + + /** + * Returns the date that this password was first set for the associated + * user. + * + * @return + * The date that this password was first set for the associated user. + */ + public Timestamp getPasswordDate() { + return passwordDate; + } + + /** + * Sets the date that this password was first set for the associated user. + * + * @param passwordDate + * The date that this password was first set for the associated user. + */ + public void setPasswordDate(Timestamp passwordDate) { + this.passwordDate = passwordDate; + } + +} From ae695ef17b2272fefa4941c1a69b1816ffd9b8e9 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2016 17:24:38 -0700 Subject: [PATCH 2/6] GUACAMOLE-36: Define and map historical password record table. --- .../JDBCAuthenticationProviderModule.java | 2 + .../auth/jdbc/user/PasswordRecordMapper.java | 68 ++++++++++++++++++ .../schema/001-create-schema.sql | 22 ++++++ .../schema/upgrade/upgrade-pre-0.9.11.sql | 23 ++++++ .../auth/jdbc/user/PasswordRecordMapper.xml | 70 +++++++++++++++++++ .../schema/001-create-schema.sql | 24 +++++++ .../schema/upgrade/upgrade-pre-0.9.11.sql | 24 +++++++ .../auth/jdbc/user/PasswordRecordMapper.xml | 70 +++++++++++++++++++ 8 files changed, 303 insertions(+) create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.java create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.xml create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.xml diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java index e52ca5a1d..475282f9e 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java @@ -75,6 +75,7 @@ import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper; import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileParameterMapper; import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileService; import org.apache.guacamole.auth.jdbc.tunnel.RestrictedGuacamoleTunnelService; +import org.apache.guacamole.auth.jdbc.user.PasswordRecordMapper; import org.mybatis.guice.MyBatisModule; import org.mybatis.guice.datasource.builtin.PooledDataSourceProvider; @@ -121,6 +122,7 @@ public class JDBCAuthenticationProviderModule extends MyBatisModule { addMapperClass(ConnectionPermissionMapper.class); addMapperClass(ConnectionRecordMapper.class); addMapperClass(ConnectionParameterMapper.class); + addMapperClass(PasswordRecordMapper.class); addMapperClass(SystemPermissionMapper.class); addMapperClass(SharingProfileMapper.class); addMapperClass(SharingProfileParameterMapper.class); diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.java new file mode 100644 index 000000000..21d36a87b --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.java @@ -0,0 +1,68 @@ +/* + * 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.guacamole.auth.jdbc.user; + +import java.util.List; +import org.apache.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper; +import org.apache.ibatis.annotations.Param; + +/** + * Mapper for historical password records (users' prior passwords, along with + * the dates they were set). + * + * @author Michael Jumper + */ +public interface PasswordRecordMapper extends ModeledDirectoryObjectMapper { + + /** + * Returns a collection of all password records associated with the user + * having the given username. + * + * @param username + * The username of the user whose password records are to be retrieved. + * + * @param maxHistorySize + * The maximum number of records to maintain for each user. + * + * @return + * A collection of all password records associated with the user having + * the given username. This collection will be empty if no such user + * exists. + */ + List select(@Param("username") String username, + @Param("maxHistorySize") int maxHistorySize); + + /** + * Inserts the given password record. Old records exceeding the maximum + * history size will be automatically deleted. + * + * @param record + * The password record to insert. + * + * @param maxHistorySize + * The maximum number of records to maintain for each user. + * + * @return + * The number of rows inserted. + */ + int insert(@Param("record") PasswordRecordModel record, + @Param("maxHistorySize") int maxHistorySize); + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/001-create-schema.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/001-create-schema.sql index cb5604785..ebf5b7d2a 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/001-create-schema.sql +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/001-create-schema.sql @@ -336,3 +336,25 @@ CREATE TABLE `guacamole_connection_history` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- +-- User password history +-- + +CREATE TABLE guacamole_user_password_history ( + + `password_history_id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + + -- Salted password + `password_hash` binary(32) NOT NULL, + `password_salt` binary(32), + `password_date` datetime NOT NULL, + + PRIMARY KEY (`password_history_id`), + KEY `user_id` (`user_id`), + + CONSTRAINT `guacamole_user_password_history_ibfk_1` + FOREIGN KEY (`user_id`) + REFERENCES `guacamole_user` (`user_id`) ON DELETE CASCADE + +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.11.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.11.sql index 3acc2a40b..ecfde8871 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.11.sql +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.11.sql @@ -23,3 +23,26 @@ ALTER TABLE guacamole_user ADD COLUMN password_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- +-- User password history +-- + +CREATE TABLE guacamole_user_password_history ( + + `password_history_id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + + -- Salted password + `password_hash` binary(32) NOT NULL, + `password_salt` binary(32), + `password_date` datetime NOT NULL, + + PRIMARY KEY (`password_history_id`), + KEY `user_id` (`user_id`), + + CONSTRAINT `guacamole_user_password_history_ibfk_1` + FOREIGN KEY (`user_id`) + REFERENCES `guacamole_user` (`user_id`) ON DELETE CASCADE + +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.xml new file mode 100644 index 000000000..2b5ff233e --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + INSERT INTO guacamole_user_password_history ( + user_id, + password_hash, + password_salt, + password_date + ) + VALUES ( + #{record.userID,jdbcType=INTEGER}, + #{record.passwordHash,jdbcType=BINARY}, + #{record.passwordSalt,jdbcType=BINARY}, + #{record.passwordDate,jdbcType=TIMESTAMP} + ) + + + + diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql index e308dce11..f2ad2c487 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sql @@ -385,3 +385,27 @@ CREATE INDEX ON guacamole_connection_history(connection_id); CREATE INDEX ON guacamole_connection_history(sharing_profile_id); CREATE INDEX ON guacamole_connection_history(start_date); CREATE INDEX ON guacamole_connection_history(end_date); + +-- +-- User password history +-- + +CREATE TABLE guacamole_user_password_history ( + + password_history_id serial NOT NULL, + user_id integer NOT NULL, + + -- Salted password + password_hash bytea NOT NULL, + password_salt bytea, + password_date timestamptz NOT NULL, + + PRIMARY KEY (password_history_id), + + CONSTRAINT guacamole_user_password_history_ibfk_1 + FOREIGN KEY (user_id) + REFERENCES guacamole_user (user_id) ON DELETE CASCADE + +); + +CREATE INDEX ON guacamole_user_password_history(user_id); diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.11.sql b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.11.sql index d2f430c60..c047a8fcb 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.11.sql +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.11.sql @@ -23,3 +23,27 @@ ALTER TABLE guacamole_user ADD COLUMN password_date timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- +-- User password history +-- + +CREATE TABLE guacamole_user_password_history ( + + password_history_id serial NOT NULL, + user_id integer NOT NULL, + + -- Salted password + password_hash bytea NOT NULL, + password_salt bytea, + password_date timestamptz NOT NULL, + + PRIMARY KEY (password_history_id), + + CONSTRAINT guacamole_user_password_history_ibfk_1 + FOREIGN KEY (user_id) + REFERENCES guacamole_user (user_id) ON DELETE CASCADE + +); + +CREATE INDEX ON guacamole_user_password_history(user_id); diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.xml new file mode 100644 index 000000000..a119f4c3c --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + INSERT INTO guacamole_user_password_history ( + user_id, + password_hash, + password_salt, + password_date + ) + VALUES ( + #{record.userID,jdbcType=INTEGER}, + #{record.passwordHash,jdbcType=BINARY}, + #{record.passwordSalt,jdbcType=BINARY}, + #{record.passwordDate,jdbcType=TIMESTAMP} + ) + + + + From a943077d4053804bced5d138164a78ef692a09df Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2016 22:06:44 -0700 Subject: [PATCH 3/6] GUACAMOLE-36: Record and maintain password history. --- .../auth/jdbc/security/PasswordPolicy.java | 14 ++++++++ .../jdbc/security/PasswordPolicyService.java | 35 +++++++++++++++++++ .../guacamole/auth/jdbc/user/UserService.java | 3 ++ .../auth/mysql/MySQLPasswordPolicy.java | 18 ++++++++++ .../auth/jdbc/user/PasswordRecordMapper.xml | 14 +++++++- .../postgresql/PostgreSQLPasswordPolicy.java | 18 ++++++++++ .../auth/jdbc/user/PasswordRecordMapper.xml | 11 +++++- 7 files changed, 111 insertions(+), 2 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicy.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicy.java index 8413b6a8d..7684487c5 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicy.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicy.java @@ -72,6 +72,20 @@ public interface PasswordPolicy { */ int getMaximumAge() throws GuacamoleException; + /** + * Returns the number of previous passwords remembered for each user. If + * greater than zero, users will be prohibited from reusing their past + * passwords. + * + * @return + * The number of previous passwords remembered for each user. + * + * @throws GuacamoleException + * If the password history size cannot be parsed from + * guacamole.properties. + */ + int getHistorySize() throws GuacamoleException; + /** * Returns whether both uppercase and lowercase characters must be present * in new passwords. If true, passwords which do not have at least one diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java index dfa980c0b..2b6c74cce 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java @@ -26,6 +26,7 @@ import java.util.regex.Pattern; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.apache.guacamole.auth.jdbc.user.ModeledUser; +import org.apache.guacamole.auth.jdbc.user.PasswordRecordMapper; import org.apache.guacamole.auth.jdbc.user.PasswordRecordModel; /** @@ -42,6 +43,12 @@ public class PasswordPolicyService { @Inject private JDBCEnvironment environment; + /** + * Mapper for creating/retrieving previously-set passwords. + */ + @Inject + private PasswordRecordMapper passwordRecordMapper; + /** * Regular expression which matches only if the string contains at least one * lowercase character. @@ -235,4 +242,32 @@ public class PasswordPolicyService { } + /** + * Records the password that was associated with the given user at the time + * the user was queried, such that future attempts to set that same password + * for that user will be denied. The number of passwords remembered for each + * user is limited by the password policy. + * + * @param user + * The user whose previous password should be recorded. + * + * @throws GuacamoleException + * If the password policy cannot be parsed. + */ + public void recordPreviousPassword(ModeledUser user) + throws GuacamoleException { + + // Retrieve password policy from environment + PasswordPolicy policy = environment.getPasswordPolicy(); + + // Nothing to do if history is not being recorded + int historySize = policy.getHistorySize(); + if (historySize <= 0) + return; + + // Store previous password in history + passwordRecordMapper.insert(user.getPreviousPassword(), historySize); + + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java index 5bfd665ad..74503b5b8 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java @@ -242,6 +242,9 @@ public class UserService extends ModeledDirectoryObjectService diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/PostgreSQLPasswordPolicy.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/PostgreSQLPasswordPolicy.java index 002c96b85..1e46f9aaf 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/PostgreSQLPasswordPolicy.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/PostgreSQLPasswordPolicy.java @@ -71,6 +71,19 @@ public class PostgreSQLPasswordPolicy implements PasswordPolicy { }; + /** + * The property which specifies the number of previous passwords remembered + * for each user. If set to zero, the default, then this restriction does + * not apply. + */ + private static final IntegerGuacamoleProperty HISTORY_SIZE = + new IntegerGuacamoleProperty() { + + @Override + public String getName() { return "postgresql-user-password-history-size"; } + + }; + /** * The property which specifies whether all user passwords must have at * least one lowercase character and one uppercase character. By default, @@ -155,6 +168,11 @@ public class PostgreSQLPasswordPolicy implements PasswordPolicy { return environment.getProperty(MAX_AGE, 0); } + @Override + public int getHistorySize() throws GuacamoleException { + return environment.getProperty(HISTORY_SIZE, 0); + } + @Override public boolean isMultipleCaseRequired() throws GuacamoleException { return environment.getProperty(REQUIRE_MULTIPLE_CASE, false); diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.xml index a119f4c3c..41591fada 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/PasswordRecordMapper.xml @@ -63,7 +63,16 @@ #{record.passwordHash,jdbcType=BINARY}, #{record.passwordSalt,jdbcType=BINARY}, #{record.passwordDate,jdbcType=TIMESTAMP} - ) + ); + + DELETE FROM guacamole_user_password_history + WHERE password_history_id IN ( + SELECT password_history_id + FROM guacamole_user_password_history + WHERE user_id = #{record.userID,jdbcType=INTEGER} + ORDER BY password_date DESC + OFFSET #{maxHistorySize} + ); From 4a1ae7f29235f6db38befa01e7dac4c5cccfce06 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2016 23:11:48 -0700 Subject: [PATCH 4/6] GUACAMOLE-36: Verify new passwords against history. --- .../jdbc/security/PasswordPolicyService.java | 57 +++++++++++++++++++ .../security/PasswordReusedException.java | 52 +++++++++++++++++ .../src/main/resources/translations/en.json | 1 + 3 files changed, 110 insertions(+) create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordReusedException.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java index 2b6c74cce..d6a9fe575 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java @@ -20,6 +20,8 @@ package org.apache.guacamole.auth.jdbc.security; import com.google.inject.Inject; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -49,6 +51,12 @@ public class PasswordPolicyService { @Inject private PasswordRecordMapper passwordRecordMapper; + /** + * Service for hashing passwords. + */ + @Inject + private PasswordEncryptionService encryptionService; + /** * Regular expression which matches only if the string contains at least one * lowercase character. @@ -105,6 +113,49 @@ public class PasswordPolicyService { } + /** + * Returns whether the given password matches any of the user's previous + * passwords. Regardless of the value specified here, the maximum number of + * passwords involved in this check depends on how many previous passwords + * were actually recorded, which depends on the password policy. + * + * @param password + * The password to check. + * + * @param username + * The username of the user whose history should be compared against + * the given password. + * + * @param historySize + * The maximum number of history records to compare the password + * against. + * + * @return + * true if the given password matches any of the user's previous + * passwords, up to the specified limit, false otherwise. + */ + private boolean matchesPreviousPasswords(String password, String username, + int historySize) { + + // No need to compare if no history is relevant + if (historySize <= 0) + return false; + + // Check password against all recorded hashes + List history = passwordRecordMapper.select(username, historySize); + for (PasswordRecordModel record : history) { + + byte[] hash = encryptionService.createPasswordHash(password, record.getPasswordSalt()); + if (Arrays.equals(hash, record.getPasswordHash())) + return true; + + } + + // No passwords match + return false; + + } + /** * Verifies that the given new password complies with the password policy * configured within guacamole.properties, throwing a GuacamoleException if @@ -152,6 +203,12 @@ public class PasswordPolicyService { throw new PasswordRequiresSymbolException( "Passwords must contain at least one non-alphanumeric character."); + // Prohibit password reuse + int historySize = policy.getHistorySize(); + if (matchesPreviousPasswords(password, username, historySize)) + throw new PasswordReusedException( + "Password matches a previously-used password.", historySize); + // Password passes all defined restrictions } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordReusedException.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordReusedException.java new file mode 100644 index 000000000..3e3ad9b1e --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordReusedException.java @@ -0,0 +1,52 @@ +/* + * 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.guacamole.auth.jdbc.security; + +import java.util.Collections; +import org.apache.guacamole.language.TranslatableMessage; + +/** + * Thrown when an attempt is made to reuse a previous password, in violation of + * the defined password policy. + * + * @author Michael Jumper + */ +public class PasswordReusedException extends PasswordPolicyException { + + /** + * Creates a new PasswordReusedException with the given human-readable + * message. The translatable message is already defined. + * + * @param message + * A human-readable message describing the password policy violation + * that occurred. + * + * @param historySize + * The number of previous passwords which are remembered for each user, + * and must not be reused. + */ + public PasswordReusedException(String message, int historySize) { + super(message, new TranslatableMessage( + "PASSWORD_POLICY.ERROR_REUSED", + Collections.singletonMap("HISTORY_SIZE", historySize) + )); + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.json index 95bb59b58..78728c3c6 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.json +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.json @@ -60,6 +60,7 @@ "ERROR_REQUIRES_DIGIT" : "Passwords must contain at least one digit.", "ERROR_REQUIRES_MULTIPLE_CASE" : "Passwords must contain both uppercase and lowercase characters.", "ERROR_REQUIRES_NON_ALNUM" : "Passwords must contain at least one symbol.", + "ERROR_REUSED" : "This password has already been used. Please do not reuse any of the previous {HISTORY_SIZE} {HISTORY_SIZE, plural, one{password} other{passwords}}.", "ERROR_TOO_SHORT" : "Passwords must be at least {LENGTH} {LENGTH, plural, one{character} other{characters}} long.", "ERROR_TOO_YOUNG" : "The password for this account has already been reset. Please wait at least {WAIT} more {WAIT, plural, one{day} other{days}} before changing the password again." From 52dda6b55f4538c40195abbef3d954dbd655eb3c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 5 Jan 2017 13:36:58 -0800 Subject: [PATCH 5/6] GUACAMOLE-36: Clarify function naming regarding a user's current (at time of query) password record. --- .../jdbc/security/PasswordPolicyService.java | 13 ++++++----- .../guacamole/auth/jdbc/user/ModeledUser.java | 22 +++++++++---------- .../guacamole/auth/jdbc/user/UserService.java | 2 +- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java index d6a9fe575..a9fbcf35b 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicyService.java @@ -227,13 +227,13 @@ public class PasswordPolicyService { private long getPasswordAge(ModeledUser user) { // If no password was set, then no time has elapsed - PasswordRecordModel previousPassword = user.getPreviousPassword(); - if (previousPassword == null) + PasswordRecordModel passwordRecord = user.getPasswordRecord(); + if (passwordRecord == null) return 0; // Pull both current time and the time the password was last reset long currentTime = System.currentTimeMillis(); - long lastResetTime = previousPassword.getPasswordDate().getTime(); + long lastResetTime = passwordRecord.getPasswordDate().getTime(); // Calculate the number of days elapsed since the password was last reset return TimeUnit.DAYS.convert(currentTime - lastResetTime, TimeUnit.MILLISECONDS); @@ -306,12 +306,13 @@ public class PasswordPolicyService { * user is limited by the password policy. * * @param user - * The user whose previous password should be recorded. + * The user whose password should be recorded within the password + * history. * * @throws GuacamoleException * If the password policy cannot be parsed. */ - public void recordPreviousPassword(ModeledUser user) + public void recordPassword(ModeledUser user) throws GuacamoleException { // Retrieve password policy from environment @@ -323,7 +324,7 @@ public class PasswordPolicyService { return; // Store previous password in history - passwordRecordMapper.insert(user.getPreviousPassword(), historySize); + passwordRecordMapper.insert(user.getPasswordRecord(), historySize); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java index 18a13eccb..d3570030a 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java @@ -192,7 +192,7 @@ public class ModeledUser extends ModeledDirectoryObject implements Us * The data associated with this user's password at the time this user was * queried. If the user is new, this will be null. */ - private PasswordRecordModel previousPassword = null; + private PasswordRecordModel passwordRecord = null; /** * Creates a new, empty ModeledUser. @@ -207,7 +207,7 @@ public class ModeledUser extends ModeledDirectoryObject implements Us // Store previous password, if any if (model.getPasswordHash() != null) - this.previousPassword = new PasswordRecordModel(model); + this.passwordRecord = new PasswordRecordModel(model); } @@ -245,19 +245,19 @@ public class ModeledUser extends ModeledDirectoryObject implements Us } /** - * Returns the data associated with this user's previous password as a - * password record. If the user is new, this will be null. Unlike the other - * password-related functions of UserModel, this data returned by this - * function is historical and is unaffected by calls to setPassword(). It - * will always return the values stored in the database at the time this - * user was queried. + * Returns the this user's current password record. If the user is new, this + * will be null. Note that this may represent a different password than what + * is returned by getPassword(): unlike the other password-related functions + * of ModeledUser, the data returned by this function is historical and is + * unaffected by calls to setPassword(). It will always return the values + * stored in the database at the time this user was queried. * * @return - * The data associated with this user's previous password, or null if + * The historical data associated with this user's password, or null if * the user is new. */ - public PasswordRecordModel getPreviousPassword() { - return previousPassword; + public PasswordRecordModel getPasswordRecord() { + return passwordRecord; } /** diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java index 74503b5b8..5939b041c 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java @@ -243,7 +243,7 @@ public class UserService extends ModeledDirectoryObjectService Date: Thu, 5 Jan 2017 13:44:16 -0800 Subject: [PATCH 6/6] GUACAMOLE-36: Clarify semantics of password policy history size. --- .../apache/guacamole/auth/jdbc/security/PasswordPolicy.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicy.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicy.java index 7684487c5..3ee122a6c 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicy.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/PasswordPolicy.java @@ -74,8 +74,8 @@ public interface PasswordPolicy { /** * Returns the number of previous passwords remembered for each user. If - * greater than zero, users will be prohibited from reusing their past - * passwords. + * greater than zero, users will be prohibited from reusing those passwords. + * * * @return * The number of previous passwords remembered for each user.