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/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..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 @@ -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 those 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 a47c03894..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 @@ -20,12 +20,16 @@ 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; 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; /** * Service which verifies compliance with the password policy configured via @@ -41,6 +45,18 @@ public class PasswordPolicyService { @Inject private JDBCEnvironment environment; + /** + * Mapper for creating/retrieving previously-set passwords. + */ + @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. @@ -97,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 @@ -144,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 } @@ -161,9 +226,14 @@ public class PasswordPolicyService { */ private long getPasswordAge(ModeledUser user) { + // If no password was set, then no time has elapsed + 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 = user.getPreviousPasswordDate().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); @@ -229,4 +299,33 @@ 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 password should be recorded within the password + * history. + * + * @throws GuacamoleException + * If the password policy cannot be parsed. + */ + public void recordPassword(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.getPasswordRecord(), historySize); + + } + } 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/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..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 @@ -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 passwordRecord = 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.passwordRecord = 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 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 time and date that this user's password was previously set, or - * null if the user is new. + * The historical data associated with this user's password, or null if + * the user is new. */ - public Timestamp getPreviousPasswordDate() { - return previousPasswordDate; + 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/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-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; + } + +} 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..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 @@ -242,6 +242,9 @@ public class UserService extends ModeledDirectoryObjectService + + + + + + + + + + + + + + + + + + + + + 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} + ); + + DELETE FROM guacamole_user_password_history + WHERE password_history_id <= ( + SELECT password_history_id + FROM ( + SELECT password_history_id + FROM guacamole_user_password_history + WHERE user_id = #{record.userID,jdbcType=INTEGER} + ORDER BY password_date DESC + LIMIT 1 OFFSET #{maxHistorySize} + ) old_password_record + ); + + + + 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/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 new file mode 100644 index 000000000..41591fada --- /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,79 @@ + + + + + + + + + + + + + + + + + + + + + + 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} + ); + + 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} + ); + + + +