package cash.model;
import net.sf.hibernate.HibernateException;
import org.apache.log4j.Logger;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeSet;
import cash.config.ConfigManager;
import cash.util.Hex;
import cash.util.HibernateUtil;
import cash.util.UtcDate;
import cash.validator.PasswordFormatValidator;
/**
* Represents a User object. Clients of this class should instantiate a User object with the
* multi-arg constructor rather than using setters.
*
* @author Joel Hockey
* @version $Id: $
* @hibernate.class
* table="user"
* dynamic-update="true"
* optimistic-lock="version"
*/
public class User implements java.io.Serializable {
private static final Logger LOG = Logger.getLogger(User.class);
private static MessageDigest s_md5;
private static SecureRandom s_random;
private static final int MAX_LOGIN_FAILURE_COUNT = 20;
private static final boolean RESET_LOCKED_OUT_AFTER_TIME = true;
private static final long RESET_LOCKED_OUT_TIME = 1 * 60 * 60 * 1000; // 1 hour
private int m_id;
private int m_version;
private String m_username;
private String m_password;
private Date m_passwordChangeDate;
private String m_hashedPassword;
private SortedSet m_passwordHistory = new TreeSet();
private String m_salt;
private byte[] m_saltBytes;
private Date m_createDate;
private String m_email;
private Locale m_locale;
private TimeZone m_timeZone;
private String m_telephone;
private Date m_lastSuccessfulLogin;
private String m_lastSuccessfulLoginIp;
private Date m_lastFailedLogin;
private String m_lastFailedLoginIp;
private int m_loginFailureCount;
private int m_maxLoginFailureCount = MAX_LOGIN_FAILURE_COUNT;
private boolean m_resetLockedOutAfterTime = RESET_LOCKED_OUT_AFTER_TIME;
private long m_resetLockedOutTime = RESET_LOCKED_OUT_TIME;
private boolean m_lockedOut = false;
private boolean m_disabled = false;
private boolean m_isSuperUser = false;
private boolean m_passwordNeverExpires = false;
private Set m_privileges = new HashSet();
static {
try {
s_md5 = MessageDigest.getInstance("MD5");
s_random = SecureRandom.getInstance("SHA1PRNG");
} catch (GeneralSecurityException gse) {
// shouldn't happen
LOG.error("Error creating MD5 or SHA1PRNG", gse);
throw new RuntimeException("Error creating MD5 or SHA1PRNG");
}
}
/** default constructor for Hibernate */
public User() { }
/**
* Create a User.
*
* @param username The username for logging in
* @param password The user's password
* @param email The user's email
* @throws InvalidPasswordException if password is invalid.
*/
public User(String username, String password, String email) throws InvalidPasswordException {
m_username = username;
// password
initSalt();
if (!PasswordFormatValidator.checkPasswordFormat(password)) {
throw new InvalidPasswordException();
}
m_hashedPassword = hashPassword(password);
m_createDate = UtcDate.createUtcDate();
m_email = email;
m_locale = Locale.getDefault();
m_timeZone = TimeZone.getDefault();
}
/** @param id The id to set */
public void setId(int id) { m_id = id; }
/**
* @return unique id of this User. Generated by DB.
* @hibernate.id
* generator-class="native"
*/
public int getId() { return m_id; }
/** @param version The version of this object */
public void setVersion(int version) { m_version = version; }
/**
* @return version of this object
* @hibernate.version
*/
public int getVersion() { return m_version; }
/** @param username The username to set */
public void setUsername(String username) { m_username = username; }
/**
* @return username
* @hibernate.property
* length="32"
* unique="true"
* not-null="true"
*/
public String getUsername() { return m_username; }
/**
* Set's the user's password without updating history or checking validity.
* This should only be used at User creation time, and password validity
* should be checked externally to this method.
* Do not use to update password, see {@link #changePassword(String)}
* @param password user's password
*/
public void setPassword(String password) {
m_password = password;
if (m_salt == null) {
initSalt();
}
m_hashedPassword = hashPassword(password);
m_passwordChangeDate = UtcDate.createUtcDate();
}
/**
* This method is provided to help at User creation time. It will only return
* valid values if {@link #setPassword(String)} has already been called.
* @return plaintext password.
*/
public String getPassword() { return m_password; }
/** @param time Date (UTC) user last changed password. */
public void setPasswordChangeDate(Date time) { m_passwordChangeDate = time; }
/**
* @return UTC date of last password change
* @hibernate.property
* type="cash.model.TimestampType"
* length="23"
*/
public Date getPasswordChangeDate() { return m_passwordChangeDate; }
/**
* Sets the user's hashed password. This method is provided only for the use
* of hibernate. Users of this class should not call this method.
* Use the {@link #setPassword(String)} method to set the plaintext password.
* @param hash The hashed password to set
*/
public void setHashedPassword(String hash) {
m_hashedPassword = hash;
}
/**
* @return hashed password
* @hibernate.property
* column="pwd"
* length="32"
* not-null="true"
*/
public String getHashedPassword() { return m_hashedPassword; }
/**
* @param oldPasswords The last n passwords, where n
* is defined as noRepeatHistory in User configuration. Passwords are ordered
* in descending order of creation.
*/
public void setPasswordHistory(SortedSet oldPasswords) { m_passwordHistory = oldPasswords; }
/**
* @return Password history
* @hibernate.set
* lazy="true"
* sort="cash.model.PasswordHistory"
* inverse="true"
* cascade="all"
* @hibernate.collection-key
* column="userId"
* @hibernate.collection-one-to-many
* class="cash.model.PasswordHistory"
*/
public SortedSet getPasswordHistory() { return m_passwordHistory; }
/** @param random The random salt to be used with password */
public void setSalt(String random) {
m_salt = random;
m_saltBytes = Hex.fromString(random);
}
/**
* @return random salt used with password
* @hibernate.property
* length="32"
* not-null="true"
*/
public String getSalt() { return m_salt; }
/** @param time create date */
public void setCreateDate(Date time) { m_createDate = time; }
/**
* @return Date in UTC user was created.
* @hibernate.property
* update="false"
* not-null="true"
* type="cash.model.TimestampType"
* length="23"
*/
public Date getCreateDate() { return m_createDate; }
/** @param email User's email */
public void setEmail(String email) { m_email = email; }
/**
* @return User's email
* @hibernate.property
* length="255"
* not-null="true"
*/
public String getEmail() { return m_email; }
/** @param locale The User's locale. This should be a 2 character field. */
public void setLocale(Locale locale) { m_locale = locale; }
/**
* @return User's locale. Uses 2 character ISO-something value.
* @hibernate.property
* not-null="true"
*/
public Locale getLocale() { return m_locale; }
/** @param timeZone User's time zone */
public void setTimeZone(TimeZone timeZone) { m_timeZone = timeZone; }
/**
* @return User's timezone
* @hibernate.property
* not-null="true"
*/
public TimeZone getTimeZone() { return m_timeZone; }
/** @param telephone User's telephone */
public void setTelephone(String telephone) { m_telephone = telephone; }
/**
* @return Telephone of user
* @hibernate.property
* length="16"
*/
public String getTelephone() { return m_telephone; }
/** @param time user's last successful login date in UTC. */
public void setLastSuccessfulLogin(Date time) { m_lastSuccessfulLogin = time; }
/**
* @return UTC date of last successful login
* @hibernate.property
* type="cash.model.TimestampType"
* length="23"
*/
public Date getLastSuccessfulLogin() { return m_lastSuccessfulLogin; }
/** @param ip IP address used for user's last successful login. */
public void setLastSuccessfulLoginIp(String ip) { m_lastSuccessfulLoginIp = ip; }
/**
* @return IP address used for last successful login
* @hibernate.property
*/
public String getLastSuccessfulLoginIp() { return m_lastSuccessfulLoginIp; }
/** @param time user's last failed login date in UTC. */
public void setLastFailedLogin(Date time) { m_lastFailedLogin = time; }
/**
* @return UTC date of last failed login
* @hibernate.property
* type="cash.model.TimestampType"
* length="23"
*/
public Date getLastFailedLogin() { return m_lastFailedLogin; }
/** @param ip IP address used for user's last failed login. */
public void setLastFailedLoginIp(String ip) { m_lastFailedLoginIp = ip; }
/**
* @return IP address used for last failed login
* @hibernate.property
*/
public String getLastFailedLoginIp() { return m_lastFailedLoginIp; }
/**
* Sets the number of times that a user has failed when attempting to login.
* This value is reset when a user logs in successfully, or their account is reset.
* @param count the value to set.
*/
public void setLoginFailureCount(int count) { m_loginFailureCount = count; }
/**
* @return The number of times that a user has failed when attempting to login.
* This value is reset when a user logs on successfully, or their account is reset.
* @hibernate.property
*/
public int getLoginFailureCount() { return m_loginFailureCount; }
/**
* @param count The maximum number of times that a user may fail to login before
* their account is locked out
*/
public void setMaxLoginFailureCount(int count) { m_maxLoginFailureCount = count; }
/**
* @return The maximum number of times that a user may fail to login before their account
* is locked out.
* @hibernate.property
*/
public int getMaxLoginFailureCount() { return m_maxLoginFailureCount; }
/**
* @param reset Whether this user's account will be unlocked after a specified time when it is locked
* due to login failure.
* @see #setResetLockedOutAfterTime(boolean) setResetLockedOutAfterTime
*/
public void setResetLockedOutAfterTime(boolean reset) { m_resetLockedOutAfterTime = reset; }
/**
* @return Whether this user's account will be unlocked after a specified time when it
* is locked out due to login failure.
* @see #getResetLockedOutAfterTime getResetLockedOutAfterTime
* @hibernate.property
*/
public boolean getResetLockedOutAfterTime() { return m_resetLockedOutAfterTime; }
/**
* @param time The time in millis between login attempts before login failure count is reset. Login failure
* count will only be reset if the Reset Locked Out After Time boolean is set to true.
*/
public void setResetLockedOutTime(long time) { m_resetLockedOutTime = time; }
/**
* @return Time in milliseconds before account is auto-reset after login lockout.
* @hibernate.property
*/
public long getResetLockedOutTime() { return m_resetLockedOutTime; }
/** @param lockedOut User's locked out status. */
public void setLockedOut(boolean lockedOut) { m_lockedOut = lockedOut; }
/**
* @return Whether this user's account is locked out
* @hibernate.property
*/
public boolean isLockedOut() { return m_lockedOut; }
/** @param disabled User's disabled status. */
public void setDisabled(boolean disabled) { m_disabled = disabled; }
/**
* @return Whether this user's account disabled
* @hibernate.property
*/
public boolean isDisabled() { return m_disabled; }
/** @param superUser True if user is super user */
public void setSuperUser(boolean superUser) { m_isSuperUser = superUser; }
/**
* @return Whether this user is a super user
* @hibernate.property
*/
public boolean isSuperUser() { return m_isSuperUser; }
/** @param expires True if user's password never expires */
public void setPasswordNeverExpires(boolean expires) { m_passwordNeverExpires = expires; }
/**
* @return Whether this user's password ever expires
* @hibernate.property
*/
public boolean getPasswordNeverExpires() { return m_passwordNeverExpires; }
/** @param privs Set of privileges for this user */
public void setPrivileges(Set privs) { m_privileges = privs; }
/**
* @return Set of Privileges for this User.
* @hibernate.set
* table="user_priv"
* lazy="true"
* cascade="all"
* @hibernate.collection-key
* column="userId"
* @hibernate.collection-element
* column="priv"
* type="string"
*/
public Set getPrivileges() { return m_privileges; }
/** convenience method of OGNL */
public void setPriv(String[] privs) {
for (int i = 0; i < privs.length; i++) {
m_privileges.add(privs[i]);
}
}
// other methods
/**
* Changes the user's password. Password must meet criteria
* defined in configuration. The user's password will be appended to
* a random 20 byte salt and then hashed using MD5 to create the
* value that will be stored in the DB. The current Hibernate Session
* will be used to update pwd history.
*
* @param password The password to set
* @return true if password is changed, false if password was not changed
* because it did not meet password requirements.
* @throws HibernateException if error updating password history
*/
public boolean changePassword(String password) throws HibernateException {
// check format
if (!PasswordFormatValidator.checkPasswordFormat(password)) {
return false;
}
// check history
// first check current password
String hashedPwd = hashPassword(password);
LOG.debug("checking if password is same as current");
if (hashedPwd.equals(m_hashedPassword)) {
LOG.info("password is same as current password");
return false;
}
LOG.debug("checking if password exists in history. History size is " + m_passwordHistory.size());
for (Iterator i = getPasswordHistory().iterator(); i.hasNext(); ) {
PasswordHistory ph = (PasswordHistory)i.next();
if (hashedPwd.equals(ph.getHashedPassword())) {
LOG.info("password already used as one of last "
+ ConfigManager.getConfig().getUser().getNoRepeatHistory());
return false;
}
}
// add current pwd to history and truncate history if it is too long now
PasswordHistory ph = new PasswordHistory(this, m_hashedPassword);
m_passwordHistory.add(ph);
LOG.debug("saving old password into password history");
HibernateUtil.currentSession().save(ph);
// compare to (noRepeat - 1) because we are checking current as part of history
if (m_passwordHistory.size() > ConfigManager.getConfig().getUser().getNoRepeatHistory() - 1) {
PasswordHistory toRemove = (PasswordHistory)m_passwordHistory.first();
LOG.info("Removing password history object for user " + m_username
+ " created: " + toRemove.getCreateDate());
m_passwordHistory.remove(toRemove);
HibernateUtil.currentSession().delete(toRemove);
}
// now set password and date
m_hashedPassword = hashedPwd;
m_passwordChangeDate = UtcDate.createUtcDate();
return true;
}
/**
* Hashes input pwd to see if it equals stored pwd hash value.
* @param pwd Password to check
* @return true if passwords are equal.
*/
public boolean passwordEquals(String pwd) {
String hash = hashPassword(pwd);
return m_hashedPassword.equalsIgnoreCase(hash);
}
/**
* Hashes salt and password to produce hashed password.
* @param pwd Password to hash
* @return Hex encoding of MD5 hash of salt and pwd
*/
private String hashPassword(String pwd) {
byte[] pwdBytes = pwd.getBytes(); //TODO: should an encoding be specified here?
byte[] in = new byte[OS:m_saltBytes.length + pwdBytes.length];
System.arraycopy(m_saltBytes, 0, in, 0, m_saltBytes.length);
System.arraycopy(pwdBytes, 0, in, m_saltBytes.length, pwdBytes.length);
byte[] out = s_md5.digest(in);
return Hex.toString(out);
}
/** initialises salt */
private void initSalt() {
m_saltBytes = new byte[OS:16];
s_random.nextBytes(m_saltBytes);
m_salt = Hex.toString(m_saltBytes);
}
/** @return String representation of User */
public String toString() {
StringBuffer sb = new StringBuffer(500);
sb.append("[").append("ID:").append(m_id)
.append(",version:").append(m_version)
.append(",hashedPassword:").append(m_hashedPassword)
.append(",salt:").append(m_salt)
.append(",createDate:").append(m_createDate)
.append(",email:").append(m_email)
.append(",locale:").append(m_locale)
.append(",timeZone:").append(m_timeZone)
.append(",telephone:").append(m_telephone)
.append(",lastSuccessfulLogin:").append(m_lastSuccessfulLogin)
.append(",lastSuccessfulLoginIp:").append(m_lastSuccessfulLoginIp)
.append(",lastFailedLogin:").append(m_lastFailedLogin)
.append(",lastFailedLoginIp:").append(m_lastFailedLoginIp)
.append(",loginFailureCount:").append(m_loginFailureCount)
.append(",maxLoginFailureCount:").append(m_maxLoginFailureCount)
.append(",resetLockedOutAfterTime:").append(m_resetLockedOutAfterTime)
.append(",resetLockedOutTime:").append(m_resetLockedOutTime)
.append(",lockedOut:").append(m_lockedOut)
.append(",disabled:").append(m_disabled)
.append(",isSuperUser:").append(m_isSuperUser)
.append(",passwordNeverExpires:").append(m_passwordNeverExpires)
.append(",passwordChangeDate:").append(m_passwordChangeDate)
.append(",privs:").append(m_privileges);
return sb.toString();
}
/**
* Copies editable data from this object to User object provided. This is used
* in Edit actions. Not all fields are copied, only those that are editable
* @param user Object to copy to
*/
public void copy(User user) {
user.setUsername(m_username);
user.setEmail(m_email);
user.setLocale(m_locale);
user.setTimeZone(m_timeZone);
user.setTelephone(m_telephone);
user.setLockedOut(m_lockedOut);
user.setDisabled(m_disabled);
user.setPasswordNeverExpires(m_passwordNeverExpires);
// do some smarts for privs removal. Clear all if more than half are removed
if (m_privileges.size() <= user.getPrivileges().size() / 2) {
LOG.debug("detected that many privs are removed, clearing all");
user.setPrivileges(m_privileges);
} else {
// find which ones should be removed
List toRemove = new ArrayList();
for (Iterator i = user.getPrivileges().iterator(); i.hasNext(); ) {
String priv = (String)i.next();
if (!m_privileges.contains(priv)) {
toRemove.add(priv);
}
}
// remove them
for (int i = 0; i < toRemove.size(); i++) {
user.getPrivileges().remove(toRemove.get(i));
}
// add all new privs
for (Iterator i = m_privileges.iterator(); i.hasNext(); ) {
user.getPrivileges().add(i.next());
}
}
}
}