GUACAMOLE-36: Merge password history tracking change.

This commit is contained in:
James Muehlner
2017-01-05 13:49:29 -08:00
17 changed files with 705 additions and 15 deletions

View File

@@ -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.SharingProfileParameterMapper;
import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileService; import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileService;
import org.apache.guacamole.auth.jdbc.tunnel.RestrictedGuacamoleTunnelService; 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.MyBatisModule;
import org.mybatis.guice.datasource.builtin.PooledDataSourceProvider; import org.mybatis.guice.datasource.builtin.PooledDataSourceProvider;
@@ -121,6 +122,7 @@ public class JDBCAuthenticationProviderModule extends MyBatisModule {
addMapperClass(ConnectionPermissionMapper.class); addMapperClass(ConnectionPermissionMapper.class);
addMapperClass(ConnectionRecordMapper.class); addMapperClass(ConnectionRecordMapper.class);
addMapperClass(ConnectionParameterMapper.class); addMapperClass(ConnectionParameterMapper.class);
addMapperClass(PasswordRecordMapper.class);
addMapperClass(SystemPermissionMapper.class); addMapperClass(SystemPermissionMapper.class);
addMapperClass(SharingProfileMapper.class); addMapperClass(SharingProfileMapper.class);
addMapperClass(SharingProfileParameterMapper.class); addMapperClass(SharingProfileParameterMapper.class);

View File

@@ -72,6 +72,20 @@ public interface PasswordPolicy {
*/ */
int getMaximumAge() throws GuacamoleException; 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 * Returns whether both uppercase and lowercase characters must be present
* in new passwords. If true, passwords which do not have at least one * in new passwords. If true, passwords which do not have at least one

View File

@@ -20,12 +20,16 @@
package org.apache.guacamole.auth.jdbc.security; package org.apache.guacamole.auth.jdbc.security;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
import org.apache.guacamole.auth.jdbc.user.ModeledUser; 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 * Service which verifies compliance with the password policy configured via
@@ -41,6 +45,18 @@ public class PasswordPolicyService {
@Inject @Inject
private JDBCEnvironment environment; 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 * Regular expression which matches only if the string contains at least one
* lowercase character. * 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<PasswordRecordModel> 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 * Verifies that the given new password complies with the password policy
* configured within guacamole.properties, throwing a GuacamoleException if * configured within guacamole.properties, throwing a GuacamoleException if
@@ -144,6 +203,12 @@ public class PasswordPolicyService {
throw new PasswordRequiresSymbolException( throw new PasswordRequiresSymbolException(
"Passwords must contain at least one non-alphanumeric character."); "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 // Password passes all defined restrictions
} }
@@ -161,9 +226,14 @@ public class PasswordPolicyService {
*/ */
private long getPasswordAge(ModeledUser user) { 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 // Pull both current time and the time the password was last reset
long currentTime = System.currentTimeMillis(); 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 // Calculate the number of days elapsed since the password was last reset
return TimeUnit.DAYS.convert(currentTime - lastResetTime, TimeUnit.MILLISECONDS); 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);
}
} }

View File

@@ -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)
));
}
}

View File

@@ -189,10 +189,10 @@ public class ModeledUser extends ModeledDirectoryObject<UserModel> implements Us
private String password = null; private String password = null;
/** /**
* The time and date that this user's password was previously set (prior to * The data associated with this user's password at the time this user was
* being queried). If the user is new, this will be null. * queried. If the user is new, this will be null.
*/ */
private Timestamp previousPasswordDate = null; private PasswordRecordModel passwordRecord = null;
/** /**
* Creates a new, empty ModeledUser. * Creates a new, empty ModeledUser.
@@ -202,8 +202,13 @@ public class ModeledUser extends ModeledDirectoryObject<UserModel> implements Us
@Override @Override
public void setModel(UserModel model) { public void setModel(UserModel model) {
super.setModel(model); super.setModel(model);
this.previousPasswordDate = model.getPasswordDate();
// Store previous password, if any
if (model.getPasswordHash() != null)
this.passwordRecord = new PasswordRecordModel(model);
} }
@Override @Override
@@ -240,19 +245,19 @@ public class ModeledUser extends ModeledDirectoryObject<UserModel> implements Us
} }
/** /**
* Returns the time and date that this user's password was previously set. * Returns the this user's current password record. If the user is new, this
* If the user is new, this will be null. Unlike getPasswordDate() of * will be null. Note that this may represent a different password than what
* UserModel (which is updated automatically along with the password salt * is returned by getPassword(): unlike the other password-related functions
* and hash whenever setPassword() is invoked), this value is unaffected by * of ModeledUser, the data returned by this function is historical and is
* calls to setPassword(), and will always be the value stored in the * unaffected by calls to setPassword(). It will always return the values
* database at the time this user was queried. * stored in the database at the time this user was queried.
* *
* @return * @return
* The time and date that this user's password was previously set, or * The historical data associated with this user's password, or null if
* null if the user is new. * the user is new.
*/ */
public Timestamp getPreviousPasswordDate() { public PasswordRecordModel getPasswordRecord() {
return previousPasswordDate; return passwordRecord;
} }
/** /**

View File

@@ -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<UserModel> {
/**
* 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<PasswordRecordModel> 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);
}

View File

@@ -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;
}
}

View File

@@ -242,6 +242,9 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
// Always verify password complexity // Always verify password complexity
passwordPolicyService.verifyPassword(object.getIdentifier(), object.getPassword()); passwordPolicyService.verifyPassword(object.getIdentifier(), object.getPassword());
// Store previous password in history
passwordPolicyService.recordPassword(object);
} }
} }

View File

@@ -60,6 +60,7 @@
"ERROR_REQUIRES_DIGIT" : "Passwords must contain at least one digit.", "ERROR_REQUIRES_DIGIT" : "Passwords must contain at least one digit.",
"ERROR_REQUIRES_MULTIPLE_CASE" : "Passwords must contain both uppercase and lowercase characters.", "ERROR_REQUIRES_MULTIPLE_CASE" : "Passwords must contain both uppercase and lowercase characters.",
"ERROR_REQUIRES_NON_ALNUM" : "Passwords must contain at least one symbol.", "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_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." "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."

View File

@@ -336,3 +336,25 @@ CREATE TABLE `guacamole_connection_history` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) 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;

View File

@@ -23,3 +23,26 @@
ALTER TABLE guacamole_user ALTER TABLE guacamole_user
ADD COLUMN password_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; 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;

View File

@@ -71,6 +71,19 @@ public class MySQLPasswordPolicy 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 "mysql-user-password-history-size"; }
};
/** /**
* The property which specifies whether all user passwords must have at * The property which specifies whether all user passwords must have at
* least one lowercase character and one uppercase character. By default, * least one lowercase character and one uppercase character. By default,
@@ -155,6 +168,11 @@ public class MySQLPasswordPolicy implements PasswordPolicy {
return environment.getProperty(MAX_AGE, 0); return environment.getProperty(MAX_AGE, 0);
} }
@Override
public int getHistorySize() throws GuacamoleException {
return environment.getProperty(HISTORY_SIZE, 0);
}
@Override @Override
public boolean isMultipleCaseRequired() throws GuacamoleException { public boolean isMultipleCaseRequired() throws GuacamoleException {
return environment.getProperty(REQUIRE_MULTIPLE_CASE, false); return environment.getProperty(REQUIRE_MULTIPLE_CASE, false);

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!--
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.
-->
<mapper namespace="org.apache.guacamole.auth.jdbc.user.PasswordRecordMapper" >
<!-- Result mapper for system permissions -->
<resultMap id="PasswordRecordResultMap" type="org.apache.guacamole.auth.jdbc.user.PasswordRecordModel">
<result column="user_id" property="userID" jdbcType="INTEGER"/>
<result column="password_hash" property="passwordHash" jdbcType="BINARY"/>
<result column="password_salt" property="passwordSalt" jdbcType="BINARY"/>
<result column="password_date" property="passwordDate" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- Select all password records for a given user -->
<select id="select" resultMap="PasswordRecordResultMap">
SELECT
guacamole_user_password_history.user_id,
guacamole_user_password_history.password_hash,
guacamole_user_password_history.password_salt,
guacamole_user_password_history.password_date
FROM guacamole_user_password_history
JOIN guacamole_user ON guacamole_user_password_history.user_id = guacamole_user.user_id
WHERE
guacamole_user.username = #{username,jdbcType=VARCHAR}
ORDER BY
guacamole_user_password_history.password_date DESC
LIMIT #{maxHistorySize}
</select>
<!-- Insert the given password record -->
<insert id="insert" parameterType="org.apache.guacamole.auth.jdbc.user.PasswordRecordModel">
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
);
</insert>
</mapper>

View File

@@ -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(sharing_profile_id);
CREATE INDEX ON guacamole_connection_history(start_date); CREATE INDEX ON guacamole_connection_history(start_date);
CREATE INDEX ON guacamole_connection_history(end_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);

View File

@@ -23,3 +23,27 @@
ALTER TABLE guacamole_user ALTER TABLE guacamole_user
ADD COLUMN password_date timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP; 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);

View File

@@ -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 * The property which specifies whether all user passwords must have at
* least one lowercase character and one uppercase character. By default, * 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); return environment.getProperty(MAX_AGE, 0);
} }
@Override
public int getHistorySize() throws GuacamoleException {
return environment.getProperty(HISTORY_SIZE, 0);
}
@Override @Override
public boolean isMultipleCaseRequired() throws GuacamoleException { public boolean isMultipleCaseRequired() throws GuacamoleException {
return environment.getProperty(REQUIRE_MULTIPLE_CASE, false); return environment.getProperty(REQUIRE_MULTIPLE_CASE, false);

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!--
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.
-->
<mapper namespace="org.apache.guacamole.auth.jdbc.user.PasswordRecordMapper" >
<!-- Result mapper for historical passwords -->
<resultMap id="PasswordRecordResultMap" type="org.apache.guacamole.auth.jdbc.user.PasswordRecordModel">
<result column="user_id" property="userID" jdbcType="INTEGER"/>
<result column="password_hash" property="passwordHash" jdbcType="BINARY"/>
<result column="password_salt" property="passwordSalt" jdbcType="BINARY"/>
<result column="password_date" property="passwordDate" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- Select all password records for a given user -->
<select id="select" resultMap="PasswordRecordResultMap">
SELECT
guacamole_user_password_history.user_id,
guacamole_user_password_history.password_hash,
guacamole_user_password_history.password_salt,
guacamole_user_password_history.password_date
FROM guacamole_user_password_history
JOIN guacamole_user ON guacamole_user_password_history.user_id = guacamole_user.user_id
WHERE
guacamole_user.username = #{username,jdbcType=VARCHAR}
ORDER BY
guacamole_user_password_history.password_date DESC
LIMIT #{maxHistorySize}
</select>
<!-- Insert the given password record -->
<insert id="insert" parameterType="org.apache.guacamole.auth.jdbc.user.PasswordRecordModel">
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}
);
</insert>
</mapper>