From 6ba84c5d1fbd1ac1bb5a422a7258250905319f6a Mon Sep 17 00:00:00 2001 From: Alex O'Ree Date: Sat, 22 Nov 2025 18:39:35 -0500 Subject: [PATCH 1/5] JSPWIKI-1234 tracks the last X password hashes to prevent reuse when changing passwords. unfortunately unit tests are no longer stable, probably due to the user database change. pausing to work other issues --- .../apache/wiki/auth/DefaultUserManager.java | 12 +- .../auth/PasswordComplexityVeriffier.java | 4 + .../org/apache/wiki/auth/UserManager.java | 6 +- .../wiki/auth/user/AbstractUserDatabase.java | 34 +++++ .../wiki/auth/user/DefaultUserProfile.java | 8 ++ .../wiki/auth/user/JDBCUserDatabase.java | 41 +++++- .../apache/wiki/auth/user/UserDatabase.java | 7 + .../apache/wiki/auth/user/UserProfile.java | 3 + .../wiki/auth/user/XMLUserDatabase.java | 26 +++- .../apache/wiki/preferences/Preferences.java | 15 ++- .../main/resources/CoreResources.properties | 5 +- .../resources/CoreResources_de.properties | 1 + .../resources/CoreResources_es.properties | 1 + .../resources/CoreResources_fi.properties | 1 + .../resources/CoreResources_fr.properties | 1 + .../resources/CoreResources_it.properties | 1 + .../resources/CoreResources_nl.properties | 1 + .../resources/CoreResources_pt_BR.properties | 1 + .../resources/CoreResources_ru.properties | 1 + .../resources/CoreResources_zh_CN.properties | 1 + .../src/main/resources/ini/jspwiki.properties | 3 +- .../src/test/config/hsql-userdb-setup.ddl | 1 + .../wiki/auth/DefaultUserManagerTest.java | 127 ++++++++++++++++++ .../src/test/resources/userdatabase.xml | 3 + 24 files changed, 287 insertions(+), 17 deletions(-) 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 ac76f6471a..a0a721afa6 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"; @@ -359,6 +360,15 @@ public void validateProfile( final Context context, final UserProfile profile ) 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(), password2)) { + //password reuse detected + session.addMessage(SESSION_MESSAGES, + MessageFormat.format(rb.getString("security.error.passwordReuseError"), reuseCount)); + } + } } } 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 43f709ee4b..07fd4cbcc9 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 @@ -62,6 +62,10 @@ public static List validate(String pwd, Context context) { //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 d58157f1d1..47dd518863 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 @@ -73,9 +73,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..45ce966c2d 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,40 @@ 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 ) { + } 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..5207783d10 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..bd0be147da 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 @@ -181,4 +181,11 @@ 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 + */ + 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..d61c3984a3 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,6 @@ public interface UserProfile extends Serializable */ @Override String toString(); + + 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 6f1cee8641..07b3a17d46 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 @@ -235,4 +235,5 @@ pwdcheck.minLower=Not enough lower case (a-z) characters, min={0} 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} \ No newline at end of file +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. \ 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 d7caa21352..0b9dc139d1 100644 --- a/jspwiki-main/src/main/resources/CoreResources_de.properties +++ b/jspwiki-main/src/main/resources/CoreResources_de.properties @@ -253,3 +253,4 @@ 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. diff --git a/jspwiki-main/src/main/resources/CoreResources_es.properties b/jspwiki-main/src/main/resources/CoreResources_es.properties index 4db7afd11b..383bbd9842 100644 --- a/jspwiki-main/src/main/resources/CoreResources_es.properties +++ b/jspwiki-main/src/main/resources/CoreResources_es.properties @@ -220,3 +220,4 @@ 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. diff --git a/jspwiki-main/src/main/resources/CoreResources_fi.properties b/jspwiki-main/src/main/resources/CoreResources_fi.properties index 01e35f6900..f6273d07f2 100644 --- a/jspwiki-main/src/main/resources/CoreResources_fi.properties +++ b/jspwiki-main/src/main/resources/CoreResources_fi.properties @@ -204,3 +204,4 @@ 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. diff --git a/jspwiki-main/src/main/resources/CoreResources_fr.properties b/jspwiki-main/src/main/resources/CoreResources_fr.properties index a1b3b7c5eb..7f9b70f1d1 100644 --- a/jspwiki-main/src/main/resources/CoreResources_fr.properties +++ b/jspwiki-main/src/main/resources/CoreResources_fr.properties @@ -254,3 +254,4 @@ 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. diff --git a/jspwiki-main/src/main/resources/CoreResources_it.properties b/jspwiki-main/src/main/resources/CoreResources_it.properties index c3e80388cb..3f85cf912c 100644 --- a/jspwiki-main/src/main/resources/CoreResources_it.properties +++ b/jspwiki-main/src/main/resources/CoreResources_it.properties @@ -241,3 +241,4 @@ 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. diff --git a/jspwiki-main/src/main/resources/CoreResources_nl.properties b/jspwiki-main/src/main/resources/CoreResources_nl.properties index 8cf7806802..3333bf6b89 100644 --- a/jspwiki-main/src/main/resources/CoreResources_nl.properties +++ b/jspwiki-main/src/main/resources/CoreResources_nl.properties @@ -239,3 +239,4 @@ pwdcheck.minDigits=Niet genoeg cijfers (0-9), min={0} pwdcheck.minOther=Niet genoeg symbolen of andere tekens, min={0} pwdcheck.tooshort=Wachtwoord te kort, min={0} pwdcheck.toolong=Wachtwoord te lang, max={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 4e6a7d46d8..df62e92948 100644 --- a/jspwiki-main/src/main/resources/CoreResources_pt_BR.properties +++ b/jspwiki-main/src/main/resources/CoreResources_pt_BR.properties @@ -242,3 +242,4 @@ 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. diff --git a/jspwiki-main/src/main/resources/CoreResources_ru.properties b/jspwiki-main/src/main/resources/CoreResources_ru.properties index 0f0a9b046f..146b241c15 100644 --- a/jspwiki-main/src/main/resources/CoreResources_ru.properties +++ b/jspwiki-main/src/main/resources/CoreResources_ru.properties @@ -247,3 +247,4 @@ 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. diff --git a/jspwiki-main/src/main/resources/CoreResources_zh_CN.properties b/jspwiki-main/src/main/resources/CoreResources_zh_CN.properties index d26f4aa1da..ce95be7aef 100644 --- a/jspwiki-main/src/main/resources/CoreResources_zh_CN.properties +++ b/jspwiki-main/src/main/resources/CoreResources_zh_CN.properties @@ -246,3 +246,4 @@ 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 diff --git a/jspwiki-main/src/main/resources/ini/jspwiki.properties b/jspwiki-main/src/main/resources/ini/jspwiki.properties index 7835c67bba..61cbccbd73 100644 --- a/jspwiki-main/src/main/resources/ini/jspwiki.properties +++ b/jspwiki-main/src/main/resources/ini/jspwiki.properties @@ -1132,5 +1132,6 @@ jspwiki.credentials.minSymbols=1 # allow X repeating characters but no more. 1 is the default. # i.e. 1 with "password" is ok but "passsword" is not jspwiki.credentials.repeatingCharacters=1 - +# maximum quantity of password hashes to remember to prevent password reuse +jspwiki.credentials.reuseCount=99 ### End of configuration file. 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/auth/DefaultUserManagerTest.java b/jspwiki-main/src/test/java/org/apache/wiki/auth/DefaultUserManagerTest.java index c3c4b5f3f6..d31d1865d0 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,7 +27,14 @@ Licensed to the Apache Software Foundation (ASF) under one import org.junit.jupiter.api.Test; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import java.util.Locale; import java.util.Properties; +import org.apache.commons.lang3.StringUtils; +import org.apache.wiki.TestEngine; +import org.apache.wiki.WikiSession; +import org.apache.wiki.WikiSessionTest; +import org.junit.jupiter.api.Disabled; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -73,4 +80,124 @@ void testParseProfileTrimsFields() { Assertions.assertEquals( "admin@example.com", profile.getEmail(), "Email should be trimmed" ); } + + @Test + public void verifyPasswordReusePolicies() throws Exception { + + 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); + Properties props = TestEngine.getTestProperties(); + 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.authenticatedSession( engine, "unitTestBob", "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 + final UserProfile profile = userManager.parseProfile(context); + + 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("unitTestBob"); + when(request.getParameter("fullname")).thenReturn("unitTestBob" + " Smith"); + when(request.getParameter("email")).thenReturn("unitTestBob" + "@apache.org"); + when(request.getParameter("password0")).thenReturn("myP@5sw0rd"); + 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); + 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("password0")).thenReturn("passwordA2!"); + when(request.getParameter("password2")).thenReturn("passwordA3!"); + when(request.getSession()).thenReturn(httpSession); + context = mock(Context.class); + when(context.getHttpRequest()).thenReturn(request); + when(context.getEngine()).thenReturn(engine); + when(context.getWikiSession()).thenReturn(wikiSession); + 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("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); + userManager.validateProfile(context, profile); + System.out.println(StringUtils.join(wikiSession.getMessages())); + Assertions.assertEquals(0, + wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, + StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); + userManager.setUserProfile(context, profile); + Assertions.assertEquals(1, + wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, + StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); + + + + } + @Disabled + @Test + public void verifyPasswordReusePoliciesWithItOff() throws Exception { + + Properties props = TestEngine.getTestProperties(); + props.put("jspwiki.credentials.reuseCount", "-1"); + TestEngine engine = TestEngine.build(props); + } } diff --git a/jspwiki-main/src/test/resources/userdatabase.xml b/jspwiki-main/src/test/resources/userdatabase.xml index 5d095f88d1..d3087a4fad 100644 --- a/jspwiki-main/src/test/resources/userdatabase.xml +++ b/jspwiki-main/src/test/resources/userdatabase.xml @@ -22,6 +22,9 @@ rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAACdAAKYXR0cmlidXRlMXQAEXNvbWUgcmFuZG9tIHZhbHVldAAKYXR0cmlidXRlMnQADWFub3RoZXIgdmFsdWV4 + + rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAACdAAKYXR0cmlidXRlMXQAEXNvbWUgcmFuZG9tIHZhbHVldAAKYXR0cmlidXRlMnQADWFub3RoZXIgdmFsdWV4 + From 51754b4b018596e80da9ada5ecfdd281b430c7e2 Mon Sep 17 00:00:00 2001 From: Alex O'Ree Date: Fri, 28 Nov 2025 12:20:01 -0500 Subject: [PATCH 2/5] JSPWIKI-1234 resolves unit test issues. Updates several test classes that use the xml database to be more ACID compliant. i.e. multiple test classes were working off the same file, by copying and randomizing the name, this guarantees that each unit test using the same file will not affect another --- .../apache/wiki/auth/DefaultUserManager.java | 22 +-- .../main/resources/CoreResources.properties | 3 +- .../wiki/auth/DefaultUserManagerTest.java | 127 +++++++++++++++--- .../auth/PasswordComplexityVeriffierTest.java | 4 + .../auth/authorize/XMLGroupDatabaseTest.java | 30 +++-- .../wiki/auth/user/XMLUserDatabaseTest.java | 49 ++++--- .../org/apache/wiki/plugin/GroupsTest.java | 18 ++- 7 files changed, 200 insertions(+), 53 deletions(-) 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 883a7f270d..26869128d4 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 @@ -345,29 +345,35 @@ 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(), password2)) { + if (!m_database.validatePasswordReuse(profile.getLoginName(), passwordConfirmation)) { //password reuse detected session.addMessage(SESSION_MESSAGES, MessageFormat.format(rb.getString("security.error.passwordReuseError"), reuseCount)); diff --git a/jspwiki-main/src/main/resources/CoreResources.properties b/jspwiki-main/src/main/resources/CoreResources.properties index b921c88efe..dc2b581c2b 100644 --- a/jspwiki-main/src/main/resources/CoreResources.properties +++ b/jspwiki-main/src/main/resources/CoreResources.properties @@ -236,5 +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} +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/test/java/org/apache/wiki/auth/DefaultUserManagerTest.java b/jspwiki-main/src/test/java/org/apache/wiki/auth/DefaultUserManagerTest.java index d31d1865d0..db5d6753c4 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 @@ -28,12 +28,16 @@ Licensed to the Apache Software Foundation (ASF) under one import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; +import java.io.File; import java.util.Locale; 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.WikiSession; import org.apache.wiki.WikiSessionTest; +import org.apache.wiki.auth.user.XMLUserDatabase; import org.junit.jupiter.api.Disabled; import static org.mockito.Mockito.mock; @@ -83,7 +87,11 @@ void testParseProfileTrimsFields() { @Test public void verifyPasswordReusePolicies() 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()); + final HttpSession httpSession = mock(HttpSession.class); HttpServletRequest request = mock(HttpServletRequest.class); @@ -94,7 +102,6 @@ public void verifyPasswordReusePolicies() throws Exception { when(request.getParameter("fullname")).thenReturn("unitTestBob" + " Smith"); when(request.getParameter("email")).thenReturn("unitTestBob" + "@apache.org"); when(request.getSession()).thenReturn(httpSession); - Properties props = TestEngine.getTestProperties(); props.put("jspwiki.credentials.reuseCount", "2"); props.put("jspwiki.userdatabase", "org.apache.wiki.auth.user.XMLUserDatabase"); @@ -110,8 +117,9 @@ public void verifyPasswordReusePolicies() throws Exception { when(context.getWikiSession()).thenReturn(wikiSession); // Call parseProfile - final UserProfile profile = userManager.parseProfile(context); - + UserProfile profile = userManager.parseProfile(context); + profile.setCreated(null); + profile.setLastModified(null); userManager.validateProfile(context, profile); Assertions.assertEquals(0, wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, @@ -130,13 +138,23 @@ public void verifyPasswordReusePolicies() throws Exception { 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("myP@5sw0rd"); - when(request.getParameter("password2")).thenReturn("passwordA2!"); + 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, @@ -151,13 +169,20 @@ public void verifyPasswordReusePolicies() throws Exception { 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("passwordA2!"); - when(request.getParameter("password2")).thenReturn("passwordA3!"); + 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, @@ -173,23 +198,22 @@ public void verifyPasswordReusePolicies() throws Exception { 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(0, - wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, - StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); - userManager.setUserProfile(context, profile); Assertions.assertEquals(1, wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES).length, StringUtils.join(wikiSession.getMessages(DefaultUserManager.SESSION_MESSAGES))); - - + } @Disabled @@ -197,7 +221,80 @@ public void verifyPasswordReusePolicies() throws Exception { public void verifyPasswordReusePoliciesWithItOff() 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()); + + 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", "-1"); - TestEngine engine = TestEngine.build(props); + 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.authenticatedSession( engine, "unitTestBob", "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("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())); + } } 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/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..b5cb7cdb0a 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/XMLUserDatabaseTest" + UUID.randomUUID().toString() + ".xml"); + FileUtils.copyFile(new File("src/test/resources/groupdatabase.xml"), target); + props.put(XMLGroupDatabase.PROP_DATABASE, target.getAbsolutePath()); + testEngine = TestEngine.build(); + } + @AfterEach public void tearDown() throws Exception { From bace7615c1f2b5fd7ec419aeacfb6e680d886cbe Mon Sep 17 00:00:00 2001 From: Alex O'Ree Date: Sat, 29 Nov 2025 17:37:25 -0500 Subject: [PATCH 3/5] JSPWIKI-1234 minor code cleanup. clarifies the new config settings --- .../java/org/apache/wiki/auth/user/AbstractUserDatabase.java | 1 + .../src/main/java/org/apache/wiki/auth/user/UserProfile.java | 5 +++++ jspwiki-main/src/main/resources/ini/jspwiki.properties | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) 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 45ce966c2d..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 @@ -249,6 +249,7 @@ public boolean validatePasswordReuse( final String loginName, final String passw 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 ) { 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 d61c3984a3..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 @@ -196,5 +196,10 @@ 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/resources/ini/jspwiki.properties b/jspwiki-main/src/main/resources/ini/jspwiki.properties index 7005371d30..54d389f44a 100644 --- a/jspwiki-main/src/main/resources/ini/jspwiki.properties +++ b/jspwiki-main/src/main/resources/ini/jspwiki.properties @@ -1138,7 +1138,9 @@ jspwiki.credentials.repeatingCharacters=1 jspwiki.credentials.minChanged=1 # maximum quantity of password hashes to remember to prevent password reuse -jspwiki.credentials.reuseCount=99 +# 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 From 67bf6480ecf005489ddda92a1d85da057caa7ce6 Mon Sep 17 00:00:00 2001 From: Alex O'Ree Date: Sun, 30 Nov 2025 13:03:34 -0500 Subject: [PATCH 4/5] JSPWIKI-1234 some refactoring of the unit tests. adds a test case for the jdbc user database. fixes a logic issue in the jdbc setup, so the tests were worth the time --- .../apache/wiki/auth/DefaultUserManager.java | 4 +- .../wiki/auth/user/JDBCUserDatabase.java | 2 +- .../java/org/apache/wiki/HsqlDbUtils.java | 12 +- .../org/apache/wiki/TestJDBCDataSource.java | 16 +- .../wiki/auth/AbstractPasswordReuseTest.java | 241 ++++++++++++++++++ .../wiki/auth/DefaultUserManagerTest.java | 224 +--------------- .../wiki/auth/user/JDBCUserDatabaseTest.java | 21 +- .../org/apache/wiki/plugin/GroupsTest.java | 4 +- .../src/test/resources/userdatabase.xml | 3 - 9 files changed, 296 insertions(+), 231 deletions(-) create mode 100644 jspwiki-main/src/test/java/org/apache/wiki/auth/AbstractPasswordReuseTest.java 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 26869128d4..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 @@ -389,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 ) ); @@ -398,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/user/JDBCUserDatabase.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/user/JDBCUserDatabase.java index 5207783d10..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 @@ -609,7 +609,7 @@ public void save( final UserProfile profile ) throws WikiSecurityException { password = getHash( password ); //add the hashed password profile.getPreviousHashedCredentials().add(password); - while (profile.getPreviousHashedCredentials().isEmpty() && + while (!profile.getPreviousHashedCredentials().isEmpty() && profile.getPreviousHashedCredentials().size() > m_passwordReusedCount) { profile.getPreviousHashedCredentials().remove(0); } 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 db5d6753c4..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,23 +27,22 @@ Licensed to the Apache Software Foundation (ASF) under one import org.junit.jupiter.api.Test; import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; import java.io.File; -import java.util.Locale; 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.WikiSession; -import org.apache.wiki.WikiSessionTest; +import static org.apache.wiki.auth.UserManager.PROP_DATABASE; +import org.apache.wiki.auth.authorize.XMLGroupDatabase; import org.apache.wiki.auth.user.XMLUserDatabase; -import org.junit.jupiter.api.Disabled; 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() { @@ -85,216 +84,15 @@ void testParseProfileTrimsFields() { } - @Test - public void verifyPasswordReusePolicies() 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()); - - 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.authenticatedSession( engine, "unitTestBob", "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("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))); - - - } - @Disabled - @Test - public void verifyPasswordReusePoliciesWithItOff() throws Exception { - + @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()); - - 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", "-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.authenticatedSession( engine, "unitTestBob", "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("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())); - + 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/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/plugin/GroupsTest.java b/jspwiki-main/src/test/java/org/apache/wiki/plugin/GroupsTest.java index b5cb7cdb0a..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 @@ -40,10 +40,10 @@ public class GroupsTest { @BeforeAll public static void init() throws IOException { final Properties props = TestEngine.getTestProperties(); - File target = new File("target/XMLUserDatabaseTest" + UUID.randomUUID().toString() + ".xml"); + 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(); + testEngine = TestEngine.build(props); } diff --git a/jspwiki-main/src/test/resources/userdatabase.xml b/jspwiki-main/src/test/resources/userdatabase.xml index d3087a4fad..5d095f88d1 100644 --- a/jspwiki-main/src/test/resources/userdatabase.xml +++ b/jspwiki-main/src/test/resources/userdatabase.xml @@ -22,9 +22,6 @@ rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAACdAAKYXR0cmlidXRlMXQAEXNvbWUgcmFuZG9tIHZhbHVldAAKYXR0cmlidXRlMnQADWFub3RoZXIgdmFsdWV4 - - rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAACdAAKYXR0cmlidXRlMXQAEXNvbWUgcmFuZG9tIHZhbHVldAAKYXR0cmlidXRlMnQADWFub3RoZXIgdmFsdWV4 - From 5e90944cfc7bc60537490c0733ada9b4acc75de7 Mon Sep 17 00:00:00 2001 From: Alex O'Ree Date: Sun, 30 Nov 2025 14:12:13 -0500 Subject: [PATCH 5/5] JSPWIKI-1234 fixes some javadocs --- .../org/apache/wiki/auth/user/UserDatabase.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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 bd0be147da..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(); @@ -186,6 +199,7 @@ public interface UserDatabase { * @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 ); }