mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-36: Merge password history tracking change.
This commit is contained in:
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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<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
|
||||
* 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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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)
|
||||
));
|
||||
}
|
||||
|
||||
}
|
@@ -189,10 +189,10 @@ public class ModeledUser extends ModeledDirectoryObject<UserModel> 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<UserModel> 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<UserModel> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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);
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -242,6 +242,9 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
|
||||
// Always verify password complexity
|
||||
passwordPolicyService.verifyPassword(object.getIdentifier(), object.getPassword());
|
||||
|
||||
// Store previous password in history
|
||||
passwordPolicyService.recordPassword(object);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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."
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
* 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);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
@@ -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>
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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>
|
Reference in New Issue
Block a user