diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/DefaultUserManager.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/DefaultUserManager.java index 512196a189..23e04ebfed 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/DefaultUserManager.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/DefaultUserManager.java @@ -71,6 +71,7 @@ Licensed to the Apache Software Foundation (ASF) under one import java.util.WeakHashMap; + /** * Default implementation for {@link UserManager}. * @@ -79,7 +80,7 @@ Licensed to the Apache Software Foundation (ASF) under one public class DefaultUserManager implements UserManager { private static final String USERDATABASE_PACKAGE = "org.apache.wiki.auth.user"; - private static final String SESSION_MESSAGES = "profile"; + public static final String SESSION_MESSAGES = "profile"; private static final String PARAM_EMAIL = "email"; private static final String PARAM_FULLNAME = "fullname"; private static final String PARAM_PASSWORD = "password"; @@ -344,25 +345,40 @@ public void validateProfile( final Context context, final UserProfile profile ) // passwords must match and can't be null //this is the new password - final String password = profile.getPassword(); - if( password == null ) { + final String newpassword = profile.getPassword(); + if( newpassword == null ) { session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.blankpassword" ) ); } else { final HttpServletRequest request = context.getHttpRequest(); //the existing password - final String password0 = ( request == null ) ? null : request.getParameter( "password0" ); + final String existingPassword = ( request == null ) ? null : request.getParameter( "password0" ); //the new password confirmation - final String password2 = ( request == null ) ? null : request.getParameter( "password2" ); - if( !password.equals( password2 ) ) { + final String passwordConfirmation = ( request == null ) ? null : request.getParameter( "password2" ); + if (!newpassword.equals(passwordConfirmation)) { + //password confirmation does not match session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.passwordnomatch" ) ); } - if( !profile.isNew() && !getUserDatabase().validatePassword( profile.getLoginName(), password0 ) ) { + if( !profile.isNew() && (existingPassword==null || existingPassword.equals( newpassword ) ) ) { + //existing account and the existing password matches the new password + session.addMessage( SESSION_MESSAGES, "existing password matches the proposed new one" ); + } + if( !profile.isNew() && !getUserDatabase().validatePassword( profile.getLoginName(), existingPassword ) ) { + //existing account and the provided password does not match what we currently have session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.passwordnomatch" ) ); } - List msg = PasswordComplexityVeriffier.validate(password2, password0, context); + List msg = PasswordComplexityVeriffier.validate(passwordConfirmation, existingPassword, context); for (String s : msg) { session.addMessage( SESSION_MESSAGES, s ); } + int reuseCount = Integer.parseInt(m_engine.getWikiProperties().getProperty("jspwiki.credentials.reuseCount", "-1")); + if (reuseCount > 0) { + //if it's set to 0 or less, we don't store it so we can skip this check + if (!m_database.validatePasswordReuse(profile.getLoginName(), passwordConfirmation)) { + //password reuse detected + session.addMessage(SESSION_MESSAGES, + MessageFormat.format(rb.getString("security.error.passwordReuseError"), reuseCount)); + } + } } } @@ -373,7 +389,7 @@ public void validateProfile( final Context context, final UserProfile profile ) // It's illegal to use as a full name someone else's login name try { - otherProfile = getUserDatabase().find( fullName ); + otherProfile = getUserDatabase().findByFullName(fullName ); if( otherProfile != null && !profile.equals( otherProfile ) && !fullName.equals( otherProfile.getFullname() ) ) { final Object[] args = { fullName }; session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalfullname" ), args ) ); @@ -382,7 +398,7 @@ public void validateProfile( final Context context, final UserProfile profile ) // It's illegal to use as a login name someone else's full name try { - otherProfile = getUserDatabase().find( loginName ); + otherProfile = getUserDatabase().findByLoginName(loginName ); if( otherProfile != null && !profile.equals( otherProfile ) && !loginName.equals( otherProfile.getLoginName() ) ) { final Object[] args = { loginName }; session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalloginname" ), args ) ); diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/PasswordComplexityVeriffier.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/PasswordComplexityVeriffier.java index 10240aec79..7724e30ec2 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/PasswordComplexityVeriffier.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/PasswordComplexityVeriffier.java @@ -63,6 +63,10 @@ public static List validate(String pwd, String previousPwd, Context cont //perhaps a regex pattern can be added in the future List problems = new ArrayList<>(); + if (pwd == null) { + problems.add(MessageFormat.format(rb.getString("pwdcheck.tooshort"), minLength)); + return problems; + } if (pwd.length() > maxLength) { problems.add(MessageFormat.format(rb.getString("pwdcheck.toolong"), maxLength)); } diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/UserManager.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/UserManager.java index 5bd8cd8a67..f5e4800d7e 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/UserManager.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/UserManager.java @@ -74,9 +74,9 @@ public interface UserManager extends Initializable { /** *

- * Saves the {@link org.apache.wiki.auth.user.UserProfile} for the user in a wiki session. This method verifies that a user profile to - * be saved doesn't collide with existing profiles; that is, the login name or full name is already used by another profile. If the - * profile collides, a DuplicateUserException is thrown. After saving the profile, the user database changes are committed, + * Saves the {@link org.apache.wiki.auth.user.UserProfile} for the user in a wiki session.This method verifies that a user profile to + be saved doesn't collide with existing profiles; that is, the login name or full name is already used by another profile. If the + profile collides, a DuplicateUserException is thrown. After saving the profile, the user database changes are committed, * and the user's credential set is refreshed; if custom authentication is used, this means the user will be automatically be logged in. *

*

diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/user/AbstractUserDatabase.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/AbstractUserDatabase.java index 554cbe2c99..098469e626 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/user/AbstractUserDatabase.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/AbstractUserDatabase.java @@ -222,6 +222,41 @@ public boolean validatePassword( final String loginName, final String password ) } return false; } + + @Override + public boolean validatePasswordReuse( final String loginName, final String password ) { + try { + final UserProfile profile = findByLoginName( loginName ); + + // If the password is stored as SHA-256 or SSHA, verify the hash + + for (String storedPassword : profile.getPreviousHashedCredentials()) { + if (storedPassword.startsWith(SHA256_PREFIX) || storedPassword.startsWith(SSHA_PREFIX)) { + boolean match = CryptoUtil.verifySaltedPassword(password.getBytes(StandardCharsets.UTF_8), storedPassword); + if (match) { + return false; + } + } + if (storedPassword.startsWith(SHA_PREFIX)) { + String fragment = storedPassword.substring(SHA_PREFIX.length()); + String hashedPassword = getShaHash(password); + boolean match = hashedPassword.equals(fragment); + if (match) { + return false; + } + } + } + + return true; + } catch( final NoSuchPrincipalException e ) { + LOG.debug(e.getMessage(), e); + } catch( final NoSuchAlgorithmException e ) { + LOG.error( "Unsupported algorithm: " + e.getMessage() ); + } catch( final WikiSecurityException e ) { + LOG.error( "Could not upgrade SHA password to SSHA because profile could not be saved. Reason: " + e.getMessage(), e ); + } + return true; + } /** * Generates a new random user identifier (uid) that is guaranteed to be unique. diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/user/DefaultUserProfile.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/DefaultUserProfile.java index 3589c31441..8b4f7aa204 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/user/DefaultUserProfile.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/DefaultUserProfile.java @@ -22,8 +22,10 @@ Licensed to the Apache Software Foundation (ASF) under one import jakarta.servlet.http.HttpServletRequest; import java.io.Serializable; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; @@ -48,6 +50,12 @@ public final class DefaultUserProfile implements UserProfile { private String password; private String uid; private String wikiname; + //oldest should be in position zero, size bounded via configuration + private List previousHashedCredentials = new ArrayList<>(); + + public List getPreviousHashedCredentials() { + return previousHashedCredentials; + } /** * Package constructor to allow direct instantiation only from package related classes (i.e., AbstractUserDatabase). diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/user/JDBCUserDatabase.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/JDBCUserDatabase.java index a06655732b..1c7476ef9e 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/user/JDBCUserDatabase.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/JDBCUserDatabase.java @@ -45,6 +45,7 @@ Licensed to the Apache Software Foundation (ASF) under one import java.util.Map; import java.util.Properties; import java.util.Set; +import org.apache.wiki.WikiEngine; /** *

@@ -192,6 +193,8 @@ public class JDBCUserDatabase extends AbstractUserDatabase { private static final String NOTHING = ""; public static final String DEFAULT_DB_ATTRIBUTES = "attributes"; + + public static final String DEFAULT_DB_OLD_HASHES = "oldhashes"; public static final String DEFAULT_DB_CREATED = "created"; @@ -220,6 +223,8 @@ public class JDBCUserDatabase extends AbstractUserDatabase { public static final String DEFAULT_DB_WIKI_NAME = "wiki_name"; public static final String PROP_DB_ATTRIBUTES = "jspwiki.userdatabase.attributes"; + + public static final String PROP_DB_OLD_HASHES = "jspwiki.userdatabase.oldhashes"; public static final String PROP_DB_CREATED = "jspwiki.userdatabase.created"; @@ -298,6 +303,10 @@ public class JDBCUserDatabase extends AbstractUserDatabase { private String m_modified; private boolean m_supportsCommits; + + private String m_oldPasswords; + + private int m_passwordReusedCount = 0; /** * Looks up and deletes the first {@link UserProfile} in the user database @@ -433,6 +442,8 @@ public void initialize( final Engine engine, final Properties props ) throws NoR m_created = props.getProperty( PROP_DB_CREATED, DEFAULT_DB_CREATED ); m_modified = props.getProperty( PROP_DB_MODIFIED, DEFAULT_DB_MODIFIED ); m_attributes = props.getProperty( PROP_DB_ATTRIBUTES, DEFAULT_DB_ATTRIBUTES ); + m_oldPasswords = props.getProperty( PROP_DB_OLD_HASHES, DEFAULT_DB_OLD_HASHES ); + m_passwordReusedCount = Integer.parseInt(props.getProperty("jspwiki.credentials.reuseCount", "-1")); m_findAll = "SELECT * FROM " + userTable; m_findByEmail = "SELECT * FROM " + userTable + " WHERE " + m_email + "=?"; @@ -451,8 +462,9 @@ public void initialize( final Engine engine, final Properties props ) throws NoR + m_modified + "," + m_loginName + "," + m_attributes + "," - + m_created - + ") VALUES (?,?,?,?,?,?,?,?,?)"; + + m_created + "," + + m_oldPasswords + + ") VALUES (?,?,?,?,?,?,?,?,?,?)"; // The user update SQL prepared statement m_updateProfile = "UPDATE " + userTable + " SET " @@ -464,7 +476,8 @@ public void initialize( final Engine engine, final Properties props ) throws NoR + m_modified + "=?," + m_loginName + "=?," + m_attributes + "=?," - + m_lockExpiry + "=? " + + m_lockExpiry + "=?," + + m_oldPasswords + "=? " + "WHERE " + m_loginName + "=?"; // Prepare the role insert SQL @@ -562,6 +575,8 @@ public void rename( final String loginName, final String newName ) throws Duplic } /** + * @param profile + * @throws org.apache.wiki.auth.WikiSecurityException * @see org.apache.wiki.auth.user.UserDatabase#save(org.apache.wiki.auth.user.UserProfile) */ @Override @@ -592,6 +607,13 @@ public void save( final UserProfile profile ) throws WikiSecurityException { // If password changed, hash it before we save if( !Strings.CS.equals( password, existingPassword ) ) { password = getHash( password ); + //add the hashed password + profile.getPreviousHashedCredentials().add(password); + while (!profile.getPreviousHashedCredentials().isEmpty() && + profile.getPreviousHashedCredentials().size() > m_passwordReusedCount) { + profile.getPreviousHashedCredentials().remove(0); + } + } try( final Connection conn = m_ds.getConnection(); @@ -620,7 +642,9 @@ public void save( final UserProfile profile ) throws WikiSecurityException { } catch ( final IOException e ) { throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e ); } + ps1.setTimestamp( 9, ts ); + ps1.setString(10, StringUtils.join(profile.getPreviousHashedCredentials(), "|")); ps1.execute(); // Insert new role record @@ -655,7 +679,9 @@ public void save( final UserProfile profile ) throws WikiSecurityException { throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e ); } ps4.setDate( 9, lockExpiry ); - ps4.setString( 10, profile.getLoginName() ); + ps4.setString(10, StringUtils.join(profile.getPreviousHashedCredentials(), "|")); + ps4.setString( 11, profile.getLoginName() ); + ps4.execute(); } // Set the profile mod time @@ -731,6 +757,13 @@ private UserProfile findByPreparedStatement( final String sql, final Object inde LOG.error( "Could not parse user profile attributes!", e ); } } + String oldhashes = rs.getString(m_oldPasswords); + if (oldhashes != null && oldhashes.length() > 0) { + String[] parts = oldhashes.split("\\|"); + for (String s : parts) { + profile.getPreviousHashedCredentials().add(s); + } + } found = true; } } diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/user/UserDatabase.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/UserDatabase.java index f604e50244..9af0850e30 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/user/UserDatabase.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/UserDatabase.java @@ -68,6 +68,7 @@ public interface UserDatabase { * contain any profiles, this method will return a zero-length array. * * @return the WikiNames + * @throws org.apache.wiki.auth.WikiSecurityException */ Principal[] getWikiNames() throws WikiSecurityException; @@ -77,6 +78,8 @@ public interface UserDatabase { * that supplied the name is unknown. * * @param index the login name, full name, or wiki name + * @return User profile + * @throws org.apache.wiki.auth.NoSuchPrincipalException */ UserProfile find( String index ) throws NoSuchPrincipalException; @@ -86,6 +89,7 @@ public interface UserDatabase { * * @param index the e-mail address of the desired user profile * @return the user profile + * @throws org.apache.wiki.auth.NoSuchPrincipalException */ UserProfile findByEmail( String index ) throws NoSuchPrincipalException; @@ -95,6 +99,7 @@ public interface UserDatabase { * * @param index the login name of the desired user profile * @return the user profile + * @throws org.apache.wiki.auth.NoSuchPrincipalException */ UserProfile findByLoginName( String index ) throws NoSuchPrincipalException; @@ -104,6 +109,7 @@ public interface UserDatabase { * * @param uid the unique identifier of the desired user profile * @return the user profile + * @throws org.apache.wiki.auth.NoSuchPrincipalException * @since 2.8 */ UserProfile findByUid( String uid ) throws NoSuchPrincipalException; @@ -114,6 +120,7 @@ public interface UserDatabase { * * @param index the wiki name of the desired user profile * @return the user profile + * @throws org.apache.wiki.auth.NoSuchPrincipalException */ UserProfile findByWikiName( String index ) throws NoSuchPrincipalException; @@ -123,15 +130,21 @@ public interface UserDatabase { * * @param index the fill name of the desired user profile * @return the user profile + * @throws org.apache.wiki.auth.NoSuchPrincipalException */ UserProfile findByFullName( String index ) throws NoSuchPrincipalException; - /** Initializes the user database based on values from a Properties object. */ + /** Initializes the user database based on values from a Properties object. + * @param engine + * @param props + * @throws org.apache.wiki.api.exceptions.NoRequiredPropertyException + * @throws org.apache.wiki.auth.WikiSecurityException */ void initialize( Engine engine, Properties props ) throws NoRequiredPropertyException, WikiSecurityException; /** * Factory method that instantiates a new user profile. The {@link UserProfile#isNew()} method of profiles created using * this method should return true. + * @return user profile */ UserProfile newProfile(); @@ -181,4 +194,12 @@ public interface UserDatabase { */ boolean validatePassword( String loginName, String password ); + /** + * validates that the proposed password has not been recently used. + * @param loginName + * @param password + * @return false if the password has been recently used, true otherwise + * @since 3.0.0 + */ + boolean validatePasswordReuse( final String loginName, final String password ); } diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/user/UserProfile.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/UserProfile.java index 374646b095..0939428e1d 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/user/UserProfile.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/UserProfile.java @@ -20,6 +20,7 @@ Licensed to the Apache Software Foundation (ASF) under one import java.io.Serializable; import java.util.Date; +import java.util.List; import java.util.Map; /** @@ -194,4 +195,11 @@ public interface UserProfile extends Serializable */ @Override String toString(); + + /** + * List of recently used passwords in hashed format. may be empty + * @since 3.0.0 + * @return non null list + */ + List getPreviousHashedCredentials(); } diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/user/XMLUserDatabase.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/XMLUserDatabase.java index 5e622b98e7..fcfb20859c 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/user/XMLUserDatabase.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/XMLUserDatabase.java @@ -78,6 +78,7 @@ public class XMLUserDatabase extends AbstractUserDatabase { public static final String PROP_USERDATABASE = "jspwiki.xmlUserDatabaseFile"; private static final String DEFAULT_USERDATABASE = "userdatabase.xml"; private static final String ATTRIBUTES_TAG = "attributes"; + private static final String OLD_HASHES_TAG = "oldhashes"; private static final String CREATED = "created"; private static final String EMAIL = "email"; private static final String FULL_NAME = "fullName"; @@ -91,6 +92,7 @@ public class XMLUserDatabase extends AbstractUserDatabase { private static final String DATE_FORMAT = "yyyy.MM.dd 'at' HH:mm:ss:SSS z"; private Document c_dom; private File c_file; + private int m_passwordReusedCount = -1; /** {@inheritDoc} */ @Override @@ -193,7 +195,7 @@ public void initialize( final Engine engine, final Properties props ) throws NoR } LOG.info( "XML user database at " + c_file.getAbsolutePath() ); - + m_passwordReusedCount = Integer.parseInt(props.getProperty("jspwiki.credentials.reuseCount", "-1")); buildDOM(); sanitizeDOM(); } @@ -270,6 +272,8 @@ private void saveDOM() throws WikiSecurityException { io.write( "=\"" + user.getAttribute( LAST_MODIFIED ) + "\" " ); io.write( LOCK_EXPIRY ); io.write( "=\"" + user.getAttribute( LOCK_EXPIRY ) + "\" " ); + io.write( OLD_HASHES_TAG ); + io.write( "=\"" + user.getAttribute( OLD_HASHES_TAG ) + "\" " ); io.write( ">" ); final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG ); for( int j = 0; j < attributes.getLength(); j++ ) { @@ -412,7 +416,15 @@ public synchronized void save( final UserProfile profile ) throws WikiSecurityEx if( newPassword != null && !newPassword.equals( "" ) ) { final String oldPassword = user.getAttribute( PASSWORD ); if( !oldPassword.equals( newPassword ) ) { - setAttribute( user, PASSWORD, getHash( newPassword ) ); + String newhash = getHash( newPassword ); + setAttribute( user, PASSWORD, newhash ); + + profile.getPreviousHashedCredentials().add(newhash); + while (!profile.getPreviousHashedCredentials().isEmpty() && + profile.getPreviousHashedCredentials().size() > m_passwordReusedCount) { + profile.getPreviousHashedCredentials().remove(0); + } + } } @@ -428,6 +440,9 @@ public synchronized void save( final UserProfile profile ) throws WikiSecurityEx throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e ); } } + if (!profile.getPreviousHashedCredentials().isEmpty()) { + setAttribute( user, OLD_HASHES_TAG, StringUtils.join(profile.getPreviousHashedCredentials(), "|")); + } // Set the profile timestamps if( isNew ) { @@ -494,6 +509,13 @@ private UserProfile findByAttribute( final String matchAttribute, String index ) } else { profile.setLockExpiry( new Date( Long.parseLong( lockExpiry ) ) ); } + final String oldHahes = user.getAttribute(OLD_HASHES_TAG); + if (oldHahes != null && oldHahes.length() > 0) { + String[] parts = oldHahes.split("\\|"); + for (String s : parts) { + profile.getPreviousHashedCredentials().add(s); + } + } // Extract all the user's attributes (should only be one attributes tag, but you never know!) final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG ); diff --git a/jspwiki-main/src/main/java/org/apache/wiki/preferences/Preferences.java b/jspwiki-main/src/main/java/org/apache/wiki/preferences/Preferences.java index a440a1f52c..4240318394 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/preferences/Preferences.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/preferences/Preferences.java @@ -218,10 +218,14 @@ public static Locale getLocale( final Context context ) { // see if default locale is set server side if( loc == null ) { final String locale = context.getEngine().getWikiProperties().getProperty( "jspwiki.preferences.default-locale" ); - try { - loc = LocaleUtils.toLocale( locale ); - } catch( final IllegalArgumentException iae ) { - LOG.error( iae.getMessage() ); + if (locale != null) { + //this can be null under unit test/mock contexts but normally is + //not null under normal operating circumstances. + try { + loc = LocaleUtils.toLocale( locale ); + } catch( final IllegalArgumentException iae ) { + LOG.error( iae.getMessage() ); + } } } @@ -230,6 +234,9 @@ public static Locale getLocale( final Context context ) { final HttpServletRequest request = context.getHttpRequest(); loc = ( request != null ) ? request.getLocale() : Locale.getDefault(); } + if ( loc == null) { + loc = Locale.getDefault(); + } LOG.debug( "using locale " + loc.toString() ); return loc; diff --git a/jspwiki-main/src/main/resources/CoreResources.properties b/jspwiki-main/src/main/resources/CoreResources.properties index d2116a2ffe..dc2b581c2b 100644 --- a/jspwiki-main/src/main/resources/CoreResources.properties +++ b/jspwiki-main/src/main/resources/CoreResources.properties @@ -54,7 +54,7 @@ security.error.wrongip=Attempt to post from a different IP address than where th security.error.createprofilebeforelogin=You must log in before creating a profile. security.error.blankpassword=Password cannot be blank -security.error.passwordnomatch=Passwords don't match +security.error.passwordnomatch=Passwords do not match security.error.illegalfullname=Full name ''{0}'' fails validation checks security.error.illegalloginname=Login name ''{0}'' fails validation checks @@ -236,4 +236,6 @@ pwdcheck.minDigits=Not enough digits (0-9), min={0} pwdcheck.minOther=Not enough symbols or other characters, min={0} pwdcheck.tooshort=Password too short, min={0} pwdcheck.toolong=Password too long, max={0} +security.error.passwordReuseError=This password has been used within the last {0} password changes and cannot be reused. +pwdcheck.toolong=Password too long, max={0} pwdcheck.minchanged=Not enough characters changed, min={0} \ No newline at end of file diff --git a/jspwiki-main/src/main/resources/CoreResources_de.properties b/jspwiki-main/src/main/resources/CoreResources_de.properties index fa6eef5ed7..c0ea9d361e 100644 --- a/jspwiki-main/src/main/resources/CoreResources_de.properties +++ b/jspwiki-main/src/main/resources/CoreResources_de.properties @@ -253,4 +253,5 @@ pwdcheck.minDigits=Nicht gen\u00fcgend Ziffern (0-9), min={0} pwdcheck.minOther=Nicht gen\u00fcgend Symbole oder andere Zeichen, min={0} pwdcheck.tooshort=Passwort zu kurz, min={0} pwdcheck.toolong=Passwort zu lang, max={0} +security.error.passwordReuseError=Dieses Passwort wurde bei den letzten {0} Passwort\u00e4nderungen verwendet und kann nicht wiederverwendet werden. pwdcheck.minchanged=Nicht gen\u00fcgend Zeichen ge\u00e4ndert, min={0} diff --git a/jspwiki-main/src/main/resources/CoreResources_es.properties b/jspwiki-main/src/main/resources/CoreResources_es.properties index b9fac12a8c..9328a620dd 100644 --- a/jspwiki-main/src/main/resources/CoreResources_es.properties +++ b/jspwiki-main/src/main/resources/CoreResources_es.properties @@ -220,4 +220,5 @@ pwdcheck.minDigits=No hay suficientes d\u00edgitos (0-9), min={0} pwdcheck.minOther=No hay suficientes s\u00edmbolos u otros caracteres, min={0} pwdcheck.tooshort=Contrase\u00f1a demasiado corta, min={0} pwdcheck.toolong=Contrase\u00f1a demasiado larga, m\u00e1ximo={0} +security.error.passwordReuseError=Esta contrase\u00f1a se ha utilizado en los \u00faltimos {0} cambios de contrase\u00f1a y no se puede reutilizar. pwdcheck.minchanged=No se cambiaron suficientes caracteres, min={0} diff --git a/jspwiki-main/src/main/resources/CoreResources_fi.properties b/jspwiki-main/src/main/resources/CoreResources_fi.properties index 0f9c536b3f..341c547103 100644 --- a/jspwiki-main/src/main/resources/CoreResources_fi.properties +++ b/jspwiki-main/src/main/resources/CoreResources_fi.properties @@ -204,4 +204,5 @@ pwdcheck.minDigits=Ei tarpeeksi numeroita (0-9), min={0} pwdcheck.minOther=Ei tarpeeksi symboleja tai muita merkkej\u00e4, min={0} pwdcheck.tooshort=Salasana liian lyhyt, min={0} pwdcheck.toolong=Salasana liian pitk\u00e4, max={0} +security.error.passwordReuseError=T\u00e4t\u00e4 salasanaa on k\u00e4ytetty viimeisimpien {0} salasanan vaihdon yhteydess\u00e4, eik\u00e4 sit\u00e4 voi k\u00e4ytt\u00e4\u00e4 uudelleen. pwdcheck.minchanged=Pas assez de caract\u00e8res modifi\u00e9s, min={0} diff --git a/jspwiki-main/src/main/resources/CoreResources_fr.properties b/jspwiki-main/src/main/resources/CoreResources_fr.properties index db2a475b5d..8dcad9d064 100644 --- a/jspwiki-main/src/main/resources/CoreResources_fr.properties +++ b/jspwiki-main/src/main/resources/CoreResources_fr.properties @@ -254,4 +254,5 @@ pwdcheck.minDigits=Nombre de chiffres insuffisant (0-9), min={0} pwdcheck.minOther=Pas assez de symboles ou d'autres caract\u00e8res, min={0} pwdcheck.tooshort=Mot de passe trop court, min={0} pwdcheck.toolong=Mot de passe trop long, max={0} +security.error.passwordReuseError=Ce mot de passe a \u00e9t\u00e9 utilis\u00e9 lors des {0} derni\u00e8res modifications de mot de passe et ne peut pas \u00eatre r\u00e9utilis\u00e9. pwdcheck.minchanged=Pas assez de caract\u00e8res modifi\u00e9s, min={0} diff --git a/jspwiki-main/src/main/resources/CoreResources_it.properties b/jspwiki-main/src/main/resources/CoreResources_it.properties index 88fa21e0d8..9b8fdddb3e 100644 --- a/jspwiki-main/src/main/resources/CoreResources_it.properties +++ b/jspwiki-main/src/main/resources/CoreResources_it.properties @@ -241,4 +241,5 @@ pwdcheck.minDigits=Cifre insufficienti (0-9), min={0} pwdcheck.minOther=Simboli o altri caratteri non sufficienti, min={0} pwdcheck.tooshort=Password troppo corta, min={0} pwdcheck.toolong=Password troppo lunga, max={0} +security.error.passwordReuseError=Questa password \u00e8 stata utilizzata nelle ultime {0} modifiche della password e non pu\u00f2 essere riutilizzata. pwdcheck.minchanged=Non sono stati modificati abbastanza caratteri, min={0} diff --git a/jspwiki-main/src/main/resources/CoreResources_nl.properties b/jspwiki-main/src/main/resources/CoreResources_nl.properties index d4cadc7573..7ad9467939 100644 --- a/jspwiki-main/src/main/resources/CoreResources_nl.properties +++ b/jspwiki-main/src/main/resources/CoreResources_nl.properties @@ -240,3 +240,4 @@ pwdcheck.minOther=Niet genoeg symbolen of andere tekens, min={0} pwdcheck.tooshort=Wachtwoord te kort, min={0} pwdcheck.toolong=Wachtwoord te lang, max={0} pwdcheck.minchanged=Er zijn niet genoeg tekens gewijzigd, min={0} +security.error.passwordReuseError=Dit wachtwoord is gebruikt binnen de laatste {0} wachtwoordwijzigingen en kan niet opnieuw worden gebruikt. diff --git a/jspwiki-main/src/main/resources/CoreResources_pt_BR.properties b/jspwiki-main/src/main/resources/CoreResources_pt_BR.properties index e1a955013b..d45738eb46 100644 --- a/jspwiki-main/src/main/resources/CoreResources_pt_BR.properties +++ b/jspwiki-main/src/main/resources/CoreResources_pt_BR.properties @@ -242,4 +242,5 @@ pwdcheck.minDigits=D\u00edgitos insuficientes (0-9), m\u00ednimo={0} pwdcheck.minOther=N\u00e3o h\u00e1 s\u00edmbolos ou outros caracteres suficientes, min={0} pwdcheck.tooshort=Senha muito curta, min={0} pwdcheck.toolong=Senha muito longa, m\u00e1ximo={0} +security.error.passwordReuseError=Esta senha foi usada nas \u00faltimas {0} altera\u00e7\u00f5es de senha e n\u00e3o pode ser reutilizada. pwdcheck.minchanged=N\u00e3o foram alterados caracteres suficientes, min={0} diff --git a/jspwiki-main/src/main/resources/CoreResources_ru.properties b/jspwiki-main/src/main/resources/CoreResources_ru.properties index 3b7914c332..0aae6cd86c 100644 --- a/jspwiki-main/src/main/resources/CoreResources_ru.properties +++ b/jspwiki-main/src/main/resources/CoreResources_ru.properties @@ -247,4 +247,5 @@ pwdcheck.minDigits=\u041d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\ pwdcheck.minOther=\u041d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432 \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u0438\u0445 \u0437\u043d\u0430\u043a\u043e\u0432, min={0} pwdcheck.tooshort=\u041f\u0430\u0440\u043e\u043b\u044c \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439, \u043c\u0438\u043d={0} pwdcheck.toolong=\u041f\u0430\u0440\u043e\u043b\u044c \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0434\u043b\u0438\u043d\u043d\u044b\u0439, \u043c\u0430\u043a\u0441.={0} +security.error.passwordReuseError=\u042d\u0442\u043e\u0442 \u043f\u0430\u0440\u043e\u043b\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b\u0441\u044f \u043f\u0440\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0445 {0} \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u0445 \u043f\u0430\u0440\u043e\u043b\u044f \u0438 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e. pwdcheck.minchanged=\u041d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u043e, \u043c\u0438\u043d={0} diff --git a/jspwiki-main/src/main/resources/CoreResources_zh_CN.properties b/jspwiki-main/src/main/resources/CoreResources_zh_CN.properties index 970ffba66e..a55d1497e0 100644 --- a/jspwiki-main/src/main/resources/CoreResources_zh_CN.properties +++ b/jspwiki-main/src/main/resources/CoreResources_zh_CN.properties @@ -246,4 +246,5 @@ pwdcheck.minDigits=\u6570\u5b57\u4e0d\u8db3\uff080-9\uff09\uff0c\u6700\u5c0f\u50 pwdcheck.minOther=\u7b26\u53f7\u6216\u5176\u4ed6\u5b57\u7b26\u4e0d\u8db3\uff0c\u6700\u5c0f\u503c\u4e3a{0} pwdcheck.tooshort=\u5bc6\u7801\u8fc7\u77ed\uff0c\u6700\u5c0f\u503c\u4e3a{0} pwdcheck.toolong=\u5bc6\u7801\u8fc7\u957f\uff0c\u6700\u5927\u9650\u5236\u4e3a{0} +security.error.passwordReuseError=\u6b64\u5bc6\u7801\u5df2\u5728\u6700\u8fd1 {0} \u6b21\u5bc6\u7801\u66f4\u6539\u4e2d\u4f7f\u7528\u8fc7\uff0c\u4e0d\u80fd\u518d\u6b21\u4f7f\u7528\u3002 pwdcheck.minchanged=\u66f4\u6539\u7684\u5b57\u7b26\u6570\u4e0d\u8db3\uff0c\u6700\u5c0f\u503c\u4e3a0\u3002 diff --git a/jspwiki-main/src/main/resources/ini/jspwiki.properties b/jspwiki-main/src/main/resources/ini/jspwiki.properties index 261b82c37b..54d389f44a 100644 --- a/jspwiki-main/src/main/resources/ini/jspwiki.properties +++ b/jspwiki-main/src/main/resources/ini/jspwiki.properties @@ -1137,6 +1137,10 @@ jspwiki.credentials.repeatingCharacters=1 # when changing a password, at least this number of characters must be different jspwiki.credentials.minChanged=1 +# maximum quantity of password hashes to remember to prevent password reuse +# 0 or less to disable password tracking and to reenable password reuse. +# usually the value of 5 is recommended +jspwiki.credentials.reuseCount=-1 # Added in v3.0.0 Audit Logging alerting # true to enable the audit logger, false otherwise audit.enabled=true diff --git a/jspwiki-main/src/test/config/hsql-userdb-setup.ddl b/jspwiki-main/src/test/config/hsql-userdb-setup.ddl index c335f4a611..45d09ad779 100644 --- a/jspwiki-main/src/test/config/hsql-userdb-setup.ddl +++ b/jspwiki-main/src/test/config/hsql-userdb-setup.ddl @@ -29,6 +29,7 @@ create table users ( modified timestamp, lock_expiry timestamp, attributes longvarchar, + oldhashes longvarchar, constraint users primary key (uid) ); diff --git a/jspwiki-main/src/test/java/org/apache/wiki/HsqlDbUtils.java b/jspwiki-main/src/test/java/org/apache/wiki/HsqlDbUtils.java index 1a58b34ff2..6172705218 100644 --- a/jspwiki-main/src/test/java/org/apache/wiki/HsqlDbUtils.java +++ b/jspwiki-main/src/test/java/org/apache/wiki/HsqlDbUtils.java @@ -107,7 +107,15 @@ int findFreeTcpPort() throws Exception { void startHsqlServer() throws Exception { // start Hypersonic server final Properties hProps = loadPropertiesFrom( "target/test-classes/jspwiki-custom.properties" ); - + + String path = hProps.getProperty("server.database.0"); + path = path.replace("file:", ""); + File db = new File(path) ; + if (db.exists()) { + if (!db.delete()) { + LOG.warn("failed to remove existing hsql database, start up scripts may fail"); + } + } hsqlServer = new Server(); hsqlServer.setSilent( true ); // be quiet during junit tests hsqlServer.setLogWriter( null ); // and even more quiet @@ -186,7 +194,7 @@ public String getDriverUrl() throws IOException{ * @return {@link Properties} holding {@code fileLocation} properties. * @throws IOException if {@code fileLocation} cannot be readed. */ - Properties loadPropertiesFrom( final String fileLocation ) throws IOException { + public Properties loadPropertiesFrom( final String fileLocation ) throws IOException { final Properties p = new Properties(); final InputStream inStream = new BufferedInputStream( new FileInputStream( fileLocation ) ); p.load( inStream ); diff --git a/jspwiki-main/src/test/java/org/apache/wiki/TestJDBCDataSource.java b/jspwiki-main/src/test/java/org/apache/wiki/TestJDBCDataSource.java index 244a6ab252..79d43edba1 100644 --- a/jspwiki-main/src/test/java/org/apache/wiki/TestJDBCDataSource.java +++ b/jspwiki-main/src/test/java/org/apache/wiki/TestJDBCDataSource.java @@ -85,6 +85,8 @@ public TestJDBCDataSource( final File file, final String url ) throws Exception m_jdbcURL = url; initializeJDBC( file ); } + + /** * Returns a JDBC connection using the specified username and password. @@ -175,14 +177,18 @@ public Logger getParentLogger() * @param file the file containing the JDBC properties * @throws Exception error loading class or properties */ - protected void initializeJDBC( final File file ) throws Exception - { - // Load the properties JDBC properties file + protected void initializeJDBC(final File file) throws Exception { final Properties properties; properties = new Properties(); - final FileInputStream is = new FileInputStream( file ); - properties.load( is ); + final FileInputStream is = new FileInputStream(file); + properties.load(is); is.close(); + initializeJDBC(properties); + } + + protected void initializeJDBC( final Properties properties) throws Exception { + // Load the properties JDBC properties file + if( m_jdbcURL == null ) { m_jdbcURL = properties.getProperty( PROPERTY_DRIVER_URL ); } diff --git a/jspwiki-main/src/test/java/org/apache/wiki/auth/AbstractPasswordReuseTest.java b/jspwiki-main/src/test/java/org/apache/wiki/auth/AbstractPasswordReuseTest.java new file mode 100644 index 0000000000..cb38117aed --- /dev/null +++ b/jspwiki-main/src/test/java/org/apache/wiki/auth/AbstractPasswordReuseTest.java @@ -0,0 +1,241 @@ +/* + * Copyright 2025 The Apache Software Foundation. + * + * Licensed 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.wiki.auth; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; +import org.apache.wiki.TestEngine; +import org.apache.wiki.WikiSessionTest; +import org.apache.wiki.api.core.Context; +import org.apache.wiki.api.core.Session; +import org.apache.wiki.auth.user.UserProfile; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * + */ +public abstract class AbstractPasswordReuseTest { + + public abstract Properties getTestProps() throws Exception; + + @Test + public void verifyPasswordReusePolicies() throws Exception { + Properties props = getTestProps(); + + final HttpSession httpSession = mock(HttpSession.class); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameter("loginname")).thenReturn("unitTestBob"); + when(request.getParameter("password")).thenReturn("myP@5sw0rd"); + when(request.getParameter("password0")).thenReturn("myP@5sw0rd"); + when(request.getParameter("password2")).thenReturn("myP@5sw0rd"); + when(request.getParameter("fullname")).thenReturn("unitTestBob" + " Smith"); + when(request.getParameter("email")).thenReturn("unitTestBob" + "@apache.org"); + when(request.getSession()).thenReturn(httpSession); + props.put("jspwiki.credentials.reuseCount", "2"); + // props.put("jspwiki.userdatabase", "org.apache.wiki.auth.user.XMLUserDatabase"); + + final TestEngine engine = TestEngine.build(props); + final DefaultUserManager userManager = (DefaultUserManager) engine.getManager(UserManager.class); + + final Session wikiSession = WikiSessionTest.anonymousSession(engine); + // Mock Context + Context context = mock(Context.class); + when(context.getHttpRequest()).thenReturn(request); + when(context.getEngine()).thenReturn(engine); + when(context.getWikiSession()).thenReturn(wikiSession); + + // Call parseProfile + UserProfile profile = userManager.parseProfile(context); + profile.setCreated(null); + profile.setLastModified(null); + userManager.validateProfile(context, profile); + Assertions.assertEquals(0, + wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, + userManager.getUserDatabase().getClass().getName() + " " + + StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); + + //this should save the profile + userManager.setUserProfile(context, profile); + Assertions.assertEquals(0, + wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, + StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); + + //change the password + //note that the first password is not stored as a hash because + //it's under a unit test context + request = mock(HttpServletRequest.class); + when(request.getParameter("loginname")).thenReturn("unitTestBob"); + when(request.getParameter("fullname")).thenReturn("unitTestBob" + " Smith"); + when(request.getParameter("email")).thenReturn("unitTestBob" + "@apache.org"); + when(request.getParameter("password")).thenReturn("myP@5sw0rd"); + + when(request.getSession()).thenReturn(httpSession); + context = mock(Context.class); + when(context.getHttpRequest()).thenReturn(request); + when(context.getEngine()).thenReturn(engine); + when(context.getWikiSession()).thenReturn(wikiSession); + when(request.getParameter("password")).thenReturn("passwordA2!"); + + //the proposed password + profile.setPassword("passwordA2!"); + //old one + when(request.getParameter("password0")).thenReturn("myP@5sw0rd"); + //new confirm + when(request.getParameter("password2")).thenReturn("passwordA2!"); + + userManager.validateProfile(context, profile); + Assertions.assertEquals(0, + wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, + StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); + //this should save the profile, changing the password + userManager.setUserProfile(context, profile); + Assertions.assertEquals(0, wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, StringUtils.join(wikiSession.getMessages())); + + //change it again + request = mock(HttpServletRequest.class); + when(request.getParameter("loginname")).thenReturn("unitTestBob"); + when(request.getParameter("fullname")).thenReturn("unitTestBob" + " Smith"); + when(request.getParameter("email")).thenReturn("unitTestBob" + "@apache.org"); + when(request.getParameter("password")).thenReturn("passwordA2!"); + + when(request.getSession()).thenReturn(httpSession); + context = mock(Context.class); + when(context.getHttpRequest()).thenReturn(request); + when(context.getEngine()).thenReturn(engine); + when(context.getWikiSession()).thenReturn(wikiSession); + profile = userManager.parseProfile(context); + //proposed + profile.setPassword("passwordA3!"); + //existing + when(request.getParameter("password0")).thenReturn("passwordA2!"); + //new pass + when(request.getParameter("password2")).thenReturn("passwordA3!"); + userManager.validateProfile(context, profile); + Assertions.assertEquals(0, + wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, + StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); + userManager.setUserProfile(context, profile); + Assertions.assertEquals(0, + wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, + StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); + + //change it back + request = mock(HttpServletRequest.class); + when(request.getParameter("loginname")).thenReturn("unitTestBob"); + when(request.getParameter("fullname")).thenReturn("unitTestBob" + " Smith"); + when(request.getParameter("email")).thenReturn("unitTestBob" + "@apache.org"); + when(request.getParameter("password0")).thenReturn("passwordA3!"); + when(request.getParameter("password")).thenReturn("passwordA3!"); + when(request.getParameter("password2")).thenReturn("passwordA2!"); + when(request.getSession()).thenReturn(httpSession); + context = mock(Context.class); + when(context.getHttpRequest()).thenReturn(request); + when(context.getEngine()).thenReturn(engine); + when(context.getWikiSession()).thenReturn(wikiSession); + profile = userManager.parseProfile(context); + profile.setPassword("passwordA2!"); + + userManager.validateProfile(context, profile); + System.out.println(StringUtils.join(wikiSession.getMessages())); + Assertions.assertEquals(1, + wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, + StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); + + } + + @Test + public void verifyPasswordReusePoliciesWithItOff() throws Exception { + + Properties props = getTestProps(); + + final HttpSession httpSession = mock(HttpSession.class); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameter("loginname")).thenReturn("verifyPasswordReusePoliciesWithItOff"); + when(request.getParameter("password")).thenReturn("myP@5sw0rd"); + when(request.getParameter("password0")).thenReturn("myP@5sw0rd"); + when(request.getParameter("password2")).thenReturn("myP@5sw0rd"); + when(request.getParameter("fullname")).thenReturn("verifyPasswordReusePoliciesWithItOff" + " Smith"); + when(request.getParameter("email")).thenReturn("verifyPasswordReusePoliciesWithItOff" + "@apache.org"); + when(request.getSession()).thenReturn(httpSession); + props.put("jspwiki.credentials.reuseCount", "-1"); + //props.put("jspwiki.userdatabase", "org.apache.wiki.auth.user.XMLUserDatabase"); + + final TestEngine engine = TestEngine.build(props); + final DefaultUserManager userManager = (DefaultUserManager) engine.getManager(UserManager.class); + + final Session wikiSession = WikiSessionTest.anonymousSession(engine); + //engine, "verifyPasswordReusePoliciesWithItOff", "myP@5sw0rd"); + // Mock Context + Context context = mock(Context.class); + when(context.getHttpRequest()).thenReturn(request); + when(context.getEngine()).thenReturn(engine); + when(context.getWikiSession()).thenReturn(wikiSession); + + // Call parseProfile + UserProfile profile = userManager.parseProfile(context); + profile.setCreated(null); + profile.setLastModified(null); + userManager.validateProfile(context, profile); + Assertions.assertEquals(0, + wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, + StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); + + //this should save the profile + userManager.setUserProfile(context, profile); + Assertions.assertEquals(0, + wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, + StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); + + //change the password + //note that the first password is not stored as a hash because + //it's under a unit test context + request = mock(HttpServletRequest.class); + when(request.getParameter("loginname")).thenReturn("verifyPasswordReusePoliciesWithItOff"); + when(request.getParameter("fullname")).thenReturn("verifyPasswordReusePoliciesWithItOff" + " Smith"); + when(request.getParameter("email")).thenReturn("verifyPasswordReusePoliciesWithItOff" + "@apache.org"); + when(request.getParameter("password")).thenReturn("myP@5sw0rd"); + + when(request.getSession()).thenReturn(httpSession); + context = mock(Context.class); + when(context.getHttpRequest()).thenReturn(request); + when(context.getEngine()).thenReturn(engine); + when(context.getWikiSession()).thenReturn(wikiSession); + when(request.getParameter("password")).thenReturn("passwordA2!"); + + //the proposed password + profile.setPassword("passwordA2!"); + //old one + when(request.getParameter("password0")).thenReturn("myP@5sw0rd"); + //new confirm + when(request.getParameter("password2")).thenReturn("passwordA2!"); + + userManager.validateProfile(context, profile); + Assertions.assertEquals(0, + wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, + StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); + //this should save the profile, changing the password + userManager.setUserProfile(context, profile); + Assertions.assertEquals(0, wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, StringUtils.join(wikiSession.getMessages())); + + } +} diff --git a/jspwiki-main/src/test/java/org/apache/wiki/auth/DefaultUserManagerTest.java b/jspwiki-main/src/test/java/org/apache/wiki/auth/DefaultUserManagerTest.java index c3c4b5f3f6..cc9ac32aa0 100644 --- a/jspwiki-main/src/test/java/org/apache/wiki/auth/DefaultUserManagerTest.java +++ b/jspwiki-main/src/test/java/org/apache/wiki/auth/DefaultUserManagerTest.java @@ -27,12 +27,22 @@ Licensed to the Apache Software Foundation (ASF) under one import org.junit.jupiter.api.Test; import jakarta.servlet.http.HttpServletRequest; +import java.io.File; import java.util.Properties; +import java.util.UUID; +import org.apache.commons.io.FileUtils; +import org.apache.wiki.TestEngine; +import static org.apache.wiki.auth.UserManager.PROP_DATABASE; +import org.apache.wiki.auth.authorize.XMLGroupDatabase; +import org.apache.wiki.auth.user.XMLUserDatabase; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -class DefaultUserManagerTest { +/** + * this focuses on the XML user database + */ +public class DefaultUserManagerTest extends AbstractPasswordReuseTest { @Test void testParseProfileTrimsFields() { @@ -73,4 +83,16 @@ void testParseProfileTrimsFields() { Assertions.assertEquals( "admin@example.com", profile.getEmail(), "Email should be trimmed" ); } + + + @Override + public Properties getTestProps() throws Exception { + Properties props = TestEngine.getTestProperties(); + File target = new File("target/" + UUID.randomUUID() + ".xml"); + FileUtils.copyFile(new File("src/test/resources/userdatabase.xml"), target); + props.setProperty(XMLUserDatabase.PROP_USERDATABASE, target.getAbsolutePath()); + props.put(PROP_DATABASE, XMLUserDatabase.class.getCanonicalName()); + props.put("jspwiki.groupdatabase", XMLGroupDatabase.class.getCanonicalName()); + return props; + } } diff --git a/jspwiki-main/src/test/java/org/apache/wiki/auth/PasswordComplexityVeriffierTest.java b/jspwiki-main/src/test/java/org/apache/wiki/auth/PasswordComplexityVeriffierTest.java index f27a62bd75..f30ddbe52f 100644 --- a/jspwiki-main/src/test/java/org/apache/wiki/auth/PasswordComplexityVeriffierTest.java +++ b/jspwiki-main/src/test/java/org/apache/wiki/auth/PasswordComplexityVeriffierTest.java @@ -19,10 +19,13 @@ import java.io.FileInputStream; import java.util.List; import java.util.Properties; +import java.util.UUID; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.wiki.TestEngine; import org.apache.wiki.WikiContext; import org.apache.wiki.WikiPage; +import org.apache.wiki.auth.user.XMLUserDatabase; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -50,6 +53,7 @@ public PasswordComplexityVeriffierTest() throws Exception { // i.e. 1 with "password" is ok but "passsword" is not props.setProperty("jspwiki.credentials.repeatingCharacters", "1"); props.setProperty("jspwiki.credentials.minChanged", "1"); + engine = TestEngine.build(props); context = new WikiContext(engine, new WikiPage(engine, "test")); diff --git a/jspwiki-main/src/test/java/org/apache/wiki/auth/authorize/XMLGroupDatabaseTest.java b/jspwiki-main/src/test/java/org/apache/wiki/auth/authorize/XMLGroupDatabaseTest.java index 2d4bb8441a..a6449e6451 100644 --- a/jspwiki-main/src/test/java/org/apache/wiki/auth/authorize/XMLGroupDatabaseTest.java +++ b/jspwiki-main/src/test/java/org/apache/wiki/auth/authorize/XMLGroupDatabaseTest.java @@ -18,6 +18,8 @@ Licensed to the Apache Software Foundation (ASF) under one */ package org.apache.wiki.auth.authorize; +import java.io.File; +import java.io.IOException; import org.apache.wiki.TestEngine; import org.apache.wiki.WikiEngine; import org.apache.wiki.api.exceptions.WikiException; @@ -29,6 +31,8 @@ Licensed to the Apache Software Foundation (ASF) under one import java.security.Principal; import java.util.Properties; +import java.util.UUID; +import org.apache.commons.io.FileUtils; /** @@ -38,11 +42,15 @@ public class XMLGroupDatabaseTest { @Test - public void testDelete() throws WikiException { + public void testDelete() throws Exception { XMLGroupDatabase m_db; String m_wiki; final Properties props = TestEngine.getTestProperties(); + File target = new File("target/XMLUserDatabaseTest" + UUID.randomUUID().toString() + ".xml"); + FileUtils.copyFile(new File("src/test/resources/groupdatabase.xml" ), target); + props.put( XMLGroupDatabase.PROP_DATABASE, target.getAbsolutePath() ); + final WikiEngine engine = new TestEngine(props); m_db = new XMLGroupDatabase(); m_db.initialize(engine, props); @@ -70,11 +78,13 @@ public void testDelete() throws WikiException { } @Test - public void testGroups() throws WikiSecurityException, WikiException { + public void testGroups() throws WikiSecurityException, WikiException, IOException { XMLGroupDatabase m_db; - - String m_wiki; final Properties props = TestEngine.getTestProperties(); + File target = new File("target/XMLUserDatabaseTest" + UUID.randomUUID().toString() + ".xml"); + FileUtils.copyFile(new File("src/test/resources/groupdatabase.xml" ), target); + props.put( XMLGroupDatabase.PROP_DATABASE, target.getAbsolutePath() ); + String m_wiki; final WikiEngine engine = new TestEngine(props); m_db = new XMLGroupDatabase(); m_db.initialize(engine, props); @@ -119,9 +129,11 @@ public void testGroups() throws WikiSecurityException, WikiException { @Test public void testSave() throws Exception { XMLGroupDatabase m_db; - - String m_wiki; final Properties props = TestEngine.getTestProperties(); + File target = new File("target/XMLUserDatabaseTest" + UUID.randomUUID().toString() + ".xml"); + FileUtils.copyFile(new File("src/test/resources/groupdatabase.xml" ), target); + props.put( XMLGroupDatabase.PROP_DATABASE, target.getAbsolutePath() ); + String m_wiki; final WikiEngine engine = new TestEngine(props); m_db = new XMLGroupDatabase(); m_db.initialize(engine, props); @@ -161,9 +173,11 @@ public void testSave() throws Exception { @Test public void testResave() throws Exception { XMLGroupDatabase m_db; - - String m_wiki; final Properties props = TestEngine.getTestProperties(); + File target = new File("target/XMLUserDatabaseTest" + UUID.randomUUID().toString() + ".xml"); + FileUtils.copyFile(new File("src/test/resources/groupdatabase.xml" ), target); + props.put( XMLGroupDatabase.PROP_DATABASE, target.getAbsolutePath() ); + String m_wiki; final WikiEngine engine = new TestEngine(props); m_db = new XMLGroupDatabase(); m_db.initialize(engine, props); diff --git a/jspwiki-main/src/test/java/org/apache/wiki/auth/user/JDBCUserDatabaseTest.java b/jspwiki-main/src/test/java/org/apache/wiki/auth/user/JDBCUserDatabaseTest.java index d84dc7ffdb..2f820dea6b 100644 --- a/jspwiki-main/src/test/java/org/apache/wiki/auth/user/JDBCUserDatabaseTest.java +++ b/jspwiki-main/src/test/java/org/apache/wiki/auth/user/JDBCUserDatabaseTest.java @@ -42,18 +42,22 @@ Licensed to the Apache Software Foundation (ASF) under one import java.sql.Timestamp; import java.util.Map; import java.util.Properties; +import org.apache.wiki.TestEngine; +import org.apache.wiki.auth.AbstractPasswordReuseTest; +import static org.apache.wiki.auth.UserManager.PROP_DATABASE; +import org.apache.wiki.auth.authorize.JDBCGroupDatabase; /** * */ -public class JDBCUserDatabaseTest { +public class JDBCUserDatabaseTest extends AbstractPasswordReuseTest { private final HsqlDbUtils m_hu = new HsqlDbUtils(); private JDBCUserDatabase m_db; private static final String TEST_ATTRIBUTES = "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAACdAAKYXR0cmlidXRlMXQAEXNvbWUgcmFuZG9tIHZhbHVldAAKYXR0cmlidXRlMnQADWFub3RoZXIgdmFsdWV4"; - private static final String INSERT_JANNE = "INSERT INTO users (" + + public static final String INSERT_JANNE = "INSERT INTO users (" + JDBCUserDatabase.DEFAULT_DB_UID + "," + JDBCUserDatabase.DEFAULT_DB_EMAIL + "," + JDBCUserDatabase.DEFAULT_DB_FULL_NAME + "," + @@ -68,7 +72,7 @@ public class JDBCUserDatabaseTest { "'" + new Timestamp( new Timestamp( System.currentTimeMillis() ).getTime() ) + "'," + "'" + TEST_ATTRIBUTES + "'" + ");"; - private static final String INSERT_USER = "INSERT INTO users (" + + public static final String INSERT_USER = "INSERT INTO users (" + JDBCUserDatabase.DEFAULT_DB_UID + "," + JDBCUserDatabase.DEFAULT_DB_EMAIL + "," + JDBCUserDatabase.DEFAULT_DB_LOGIN_NAME + "," + @@ -414,4 +418,15 @@ public void testValidatePassword() { Assertions.assertTrue( m_db.validatePassword( "user", "password" ) ); } + @Override + public Properties getTestProps() throws Exception { + final Properties props = TestEngine.getTestProperties(); + + props.put(PROP_DATABASE, JDBCUserDatabase.class.getCanonicalName()); + props.put("jspwiki.groupdatabase", JDBCGroupDatabase.class.getCanonicalName()); + props.put("jspwiki.groupdatabase.datasource", JDBCUserDatabase.DEFAULT_DB_JNDI_NAME); + props.put("jspwiki.userdatabase.datasource", JDBCUserDatabase.DEFAULT_DB_JNDI_NAME); + return props; + } + } diff --git a/jspwiki-main/src/test/java/org/apache/wiki/auth/user/XMLUserDatabaseTest.java b/jspwiki-main/src/test/java/org/apache/wiki/auth/user/XMLUserDatabaseTest.java index 3ea0fdd38f..7aec48c073 100644 --- a/jspwiki-main/src/test/java/org/apache/wiki/auth/user/XMLUserDatabaseTest.java +++ b/jspwiki-main/src/test/java/org/apache/wiki/auth/user/XMLUserDatabaseTest.java @@ -18,6 +18,7 @@ Licensed to the Apache Software Foundation (ASF) under one */ package org.apache.wiki.auth.user; +import java.io.File; import org.apache.commons.lang3.ArrayUtils; import org.apache.wiki.TestEngine; import org.apache.wiki.WikiEngine; @@ -28,13 +29,14 @@ Licensed to the Apache Software Foundation (ASF) under one import org.apache.wiki.util.CryptoUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.Serializable; import java.security.Principal; import java.util.Map; import java.util.Properties; +import java.util.UUID; +import org.apache.commons.io.FileUtils; public class XMLUserDatabaseTest { @@ -44,7 +46,9 @@ public class XMLUserDatabaseTest { @BeforeEach public void setUp() throws Exception { final Properties props = TestEngine.getTestProperties(); - props.put( XMLUserDatabase.PROP_USERDATABASE, "target/test-classes/userdatabase.xml" ); + File target = new File("target/XMLUserDatabaseTest" + UUID.randomUUID().toString() + ".xml"); + FileUtils.copyFile(new File("src/test/resources/userdatabase.xml" ), target); + props.put( XMLUserDatabase.PROP_USERDATABASE, target.getAbsolutePath() ); final WikiEngine engine = new TestEngine( props ); m_db = new XMLUserDatabase(); m_db.initialize( engine, props ); @@ -53,29 +57,34 @@ public void setUp() throws Exception { @Test public void testDeleteByLoginName() throws WikiSecurityException { // First, count the number of users in the db now. - final int oldUserCount = m_db.getWikiNames().length; - - // Create a new user with random name final String loginName = "TestUser" + System.currentTimeMillis(); - UserProfile profile = m_db.newProfile(); - profile.setEmail( "jspwiki.tests@mailinator.com" ); - profile.setLoginName( loginName ); - profile.setFullname( "FullName" + loginName ); - profile.setPassword( "password" ); - m_db.save( profile ); - - // Make sure the profile saved successfully - profile = m_db.findByLoginName( loginName ); - Assertions.assertEquals( loginName, profile.getLoginName() ); - Assertions.assertEquals( oldUserCount + 1, m_db.getWikiNames().length ); - - // Now delete the profile; should be back to old count - m_db.deleteByLoginName( loginName ); - Assertions.assertEquals( oldUserCount, m_db.getWikiNames().length ); + synchronized (m_db) { + final int oldUserCount = m_db.getWikiNames().length; + try { + + // Create a new user with random name + UserProfile profile = m_db.newProfile(); + profile.setEmail("jspwiki.tests@mailinator.com"); + profile.setLoginName(loginName); + profile.setFullname("FullName" + loginName); + profile.setPassword("password"); + m_db.save(profile); + + // Make sure the profile saved successfully + profile = m_db.findByLoginName(loginName); + Assertions.assertEquals(loginName, profile.getLoginName()); + Assertions.assertEquals(oldUserCount + 1, m_db.getWikiNames().length); + } finally { + // Now delete the profile; should be back to old count + m_db.deleteByLoginName(loginName); + Assertions.assertEquals(oldUserCount, m_db.getWikiNames().length); + } + } } @Test public void testAttributes() throws Exception { + final Principal[] p = m_db.getWikiNames(); UserProfile profile = m_db.findByEmail( "janne@ecyrd.com" ); Map< String, Serializable > attributes = profile.getAttributes(); diff --git a/jspwiki-main/src/test/java/org/apache/wiki/plugin/GroupsTest.java b/jspwiki-main/src/test/java/org/apache/wiki/plugin/GroupsTest.java index 104c1ed6e4..5c53a2005c 100644 --- a/jspwiki-main/src/test/java/org/apache/wiki/plugin/GroupsTest.java +++ b/jspwiki-main/src/test/java/org/apache/wiki/plugin/GroupsTest.java @@ -19,17 +19,33 @@ Licensed to the Apache Software Foundation (ASF) under one package org.apache.wiki.plugin; +import java.io.File; +import java.io.IOException; +import java.util.Properties; +import java.util.UUID; +import org.apache.commons.io.FileUtils; import org.apache.wiki.TestEngine; +import org.apache.wiki.auth.authorize.XMLGroupDatabase; import org.apache.wiki.pages.PageManager; import org.apache.wiki.render.RenderingManager; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; public class GroupsTest { + static TestEngine testEngine; - TestEngine testEngine = TestEngine.build(); + @BeforeAll + public static void init() throws IOException { + final Properties props = TestEngine.getTestProperties(); + File target = new File("target/GroupsTest" + UUID.randomUUID().toString() + ".xml"); + FileUtils.copyFile(new File("src/test/resources/groupdatabase.xml"), target); + props.put(XMLGroupDatabase.PROP_DATABASE, target.getAbsolutePath()); + testEngine = TestEngine.build(props); + } + @AfterEach public void tearDown() throws Exception {