Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImplTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImplTest.java (revision 1393356) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImplTest.java (working copy) @@ -142,9 +142,11 @@ // plain text passwords pwds.put("abc", "abc"); pwds.put("{a}password", "{a}password"); - // passwords already in hashed format. - pwds.put(sha1Hash, "abc"); - pwds.put(md5Hash, "abc"); + // passwords with hash-like char-sequence -> must still be hashed. + pwds.put(sha1Hash, sha1Hash); + pwds.put(md5Hash, md5Hash); + pwds.put("{"+SecurityConstants.DEFAULT_DIGEST+"}any", "{"+SecurityConstants.DEFAULT_DIGEST+"}any"); + pwds.put("{"+SecurityConstants.DEFAULT_DIGEST+"}", "{"+SecurityConstants.DEFAULT_DIGEST+"}"); for (String pw : pwds.keySet()) { u.changePassword(pw); @@ -159,11 +161,9 @@ // valid passwords, non-matching plain text MapnoMatch = new HashMap(); noMatch.put("{"+SecurityConstants.DEFAULT_DIGEST+"}", ""); - noMatch.put("{"+SecurityConstants.DEFAULT_DIGEST+"}", "{"+SecurityConstants.DEFAULT_DIGEST+"}"); noMatch.put("{"+SecurityConstants.DEFAULT_DIGEST+"}any", "any"); - noMatch.put("{"+SecurityConstants.DEFAULT_DIGEST+"}any", "{"+SecurityConstants.DEFAULT_DIGEST+"}any"); - noMatch.put(sha1Hash, sha1Hash); - noMatch.put(md5Hash, md5Hash); + noMatch.put(sha1Hash, "abc"); + noMatch.put(md5Hash, "abc"); for (String pw : noMatch.keySet()) { u.changePassword(pw); @@ -172,10 +172,14 @@ SimpleCredentials sc = new SimpleCredentials(u.getID(), plain.toCharArray()); CryptedSimpleCredentials cc = (CryptedSimpleCredentials) u.getCredentials(); - assertFalse(cc.matches(sc)); + assertFalse(pw, cc.matches(sc)); } + } - // invalid pw string + public void testChangePasswordNull() throws RepositoryException { + User u = (User) userMgr.getAuthorizable(uID); + + // invalid 'null' pw string try { u.changePassword(null); fail("invalid pw null"); Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImporterTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImporterTest.java (revision 1393356) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImporterTest.java (working copy) @@ -860,7 +860,7 @@ } public void testImportNonExistingMemberBestEffort() throws IOException, RepositoryException, SAXException, NotExecutableException { - if (umgr.getGroupMembershipSplitSize() > 0) { + if (umgr.hasMemberSplitSize()) { throw new NotExecutableException(); } @@ -914,7 +914,7 @@ String g1Id = "0120a4f9-196a-3f9e-b9f5-23f31f914da7"; String nonExistingId = "b2f5ff47-4366-31b6-a533-d8dc3614845d"; // groupId of 'g' group. - if (umgr.getAuthorizable("g") != null || umgr.getGroupMembershipSplitSize() > 0) { + if (umgr.getAuthorizable("g") != null || umgr.hasMemberSplitSize()) { throw new NotExecutableException(); } Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TestAll.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TestAll.java (revision 1393356) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TestAll.java (working copy) @@ -54,6 +54,7 @@ suite.addTestSuite(UserAccessControlProviderTest.class); suite.addTestSuite(DefaultPrincipalProviderTest.class); + suite.addTestSuite(PasswordUtilityTest.class); return suite; } } Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/PasswordUtilityTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/PasswordUtilityTest.java (revision 0) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/PasswordUtilityTest.java (revision 0) @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.test.JUnitTest; + +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * PasswordUtilityTest... + */ +public class PasswordUtilityTest extends JUnitTest { + + private static List PLAIN_PWDS = new ArrayList(); + static { + PLAIN_PWDS.add("pw"); + PLAIN_PWDS.add("PassWord123"); + PLAIN_PWDS.add("_"); + PLAIN_PWDS.add("{invalidAlgo}"); + PLAIN_PWDS.add("{invalidAlgo}Password"); + PLAIN_PWDS.add("{SHA-256}"); + PLAIN_PWDS.add("pw{SHA-256}"); + PLAIN_PWDS.add("p{SHA-256}w"); + PLAIN_PWDS.add(""); + } + + private static Map HASHED_PWDS = new HashMap(); + static { + for (String pw : PLAIN_PWDS) { + try { + HASHED_PWDS.put(pw, PasswordUtility.buildPasswordHash(pw)); + } catch (Exception e) { + // should not get here + } + } + } + + public void testBuildPasswordHash() throws Exception { + for (String pw : PLAIN_PWDS) { + String pwHash = PasswordUtility.buildPasswordHash(pw); + assertFalse(pw.equals(pwHash)); + } + + List l = new ArrayList(); + l.add(new Integer[] {0, 1000}); + l.add(new Integer[] {1, 10}); + l.add(new Integer[] {8, 50}); + l.add(new Integer[] {10, 5}); + l.add(new Integer[] {-1, -1}); + for (Integer[] params : l) { + for (String pw : PLAIN_PWDS) { + int saltsize = params[0]; + int iterations = params[1]; + + String pwHash = PasswordUtility.buildPasswordHash(pw, PasswordUtility.DEFAULT_ALGORITHM, saltsize, iterations); + assertFalse(pw.equals(pwHash)); + } + } + } + + public void testBuildPasswordHashInvalidAlgorithm() throws Exception { + List invalidAlgorithms = new ArrayList(); + invalidAlgorithms.add(""); + invalidAlgorithms.add("+"); + invalidAlgorithms.add("invalid"); + + for (String invalid : invalidAlgorithms) { + try { + String pwHash = PasswordUtility.buildPasswordHash("pw", invalid, PasswordUtility.DEFAULT_SALT_SIZE, PasswordUtility.DEFAULT_ITERATIONS); + fail("Invalid algorithm " + invalid); + } catch (NoSuchAlgorithmException e) { + // success + } + } + + } + + public void testIsPlainTextPassword() throws Exception { + for (String pw : PLAIN_PWDS) { + assertTrue(pw + " should be plain text.", PasswordUtility.isPlainTextPassword(pw)); + } + } + + public void testIsPlainTextForNull() throws Exception { + assertTrue(PasswordUtility.isPlainTextPassword(null)); + } + + public void testIsPlainTextForPwHash() throws Exception { + for (String pwHash : HASHED_PWDS.values()) { + assertFalse(pwHash + " should not be plain text.", PasswordUtility.isPlainTextPassword(pwHash)); + } + } + + public void testIsSame() throws Exception { + for (String pw : HASHED_PWDS.keySet()) { + String pwHash = HASHED_PWDS.get(pw); + assertTrue("Not the same " + pw + ", " + pwHash, PasswordUtility.isSame(pwHash, pw)); + } + + String pw = "password"; + String pwHash = PasswordUtility.buildPasswordHash(pw, SecurityConstants.DEFAULT_DIGEST, 4, 50); + assertTrue("Not the same '" + pw + "', " + pwHash, PasswordUtility.isSame(pwHash, pw)); + + pwHash = PasswordUtility.buildPasswordHash(pw, "md5", 0, 5); + assertTrue("Not the same '" + pw + "', " + pwHash, PasswordUtility.isSame(pwHash, pw)); + + pwHash = PasswordUtility.buildPasswordHash(pw, "md5", -1, -1); + assertTrue("Not the same '" + pw + "', " + pwHash, PasswordUtility.isSame(pwHash, pw)); + } + + public void testIsNotSame() throws Exception { + String previous = null; + for (String pw : HASHED_PWDS.keySet()) { + String pwHash = HASHED_PWDS.get(pw); + assertFalse(pw, PasswordUtility.isSame(pw, pw)); + assertFalse(pwHash, PasswordUtility.isSame(pwHash, pwHash)); + if (previous != null) { + assertFalse(previous, PasswordUtility.isSame(pwHash, previous)); + } + previous = pw; + } + } + + public void testExtractAlgorithmFromPlainPw() throws Exception { + for (String pw : PLAIN_PWDS) { + assertNull(pw + " is no pw-hash -> no algorithm expected.", PasswordUtility.extractAlgorithm(pw)); + } + } + + public void testExtractAlgorithmFromNull() throws Exception { + assertNull("null pw -> no algorithm expected.", PasswordUtility.extractAlgorithm(null)); + } + + public void testExtractAlgorithmFromPwHash() throws Exception { + for (String pwHash : HASHED_PWDS.values()) { + String algorithm = PasswordUtility.extractAlgorithm(pwHash); + assertNotNull(pwHash + " is pw-hash -> algorithm expected.", algorithm); + assertEquals("Wrong algorithm extracted from " + pwHash, PasswordUtility.DEFAULT_ALGORITHM, algorithm); + } + + String pwHash = PasswordUtility.buildPasswordHash("pw", SecurityConstants.DEFAULT_DIGEST, 4, 50); + assertEquals(SecurityConstants.DEFAULT_DIGEST, PasswordUtility.extractAlgorithm(pwHash)); + + pwHash = PasswordUtility.buildPasswordHash("pw", "md5", 0, 5); + assertEquals("md5", PasswordUtility.extractAlgorithm(pwHash)); + + pwHash = PasswordUtility.buildPasswordHash("pw", "md5", -1, -1); + assertEquals("md5", PasswordUtility.extractAlgorithm(pwHash)); + } +} Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableActionTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableActionTest.java (revision 1393356) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableActionTest.java (working copy) @@ -230,20 +230,44 @@ } } - public void testPasswordValidationActionIgnoresHashedPwString() throws Exception { + public void testPasswordValidationActionIgnoresHashedPwStringOnCreate() throws Exception { User u = null; try { + PasswordValidationAction pwAction = new PasswordValidationAction(); + pwAction.setConstraint("^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z]).*"); + setActions(pwAction); + String uid = getTestPrincipal().getName(); + String hashed = PasswordUtility.buildPasswordHash("DWkej32H"); + u = impl.createUser(uid, hashed); + + } finally { + if (u != null) { + u.remove(); + } + save(superuser); + } + } + + public void testPasswordValidationActionOnChange() throws Exception { + User u = null; + + try { + String uid = getTestPrincipal().getName(); u = impl.createUser(uid, buildPassword(uid)); PasswordValidationAction pwAction = new PasswordValidationAction(); - pwAction.setConstraint("^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z]).*"); + pwAction.setConstraint("abc"); setActions(pwAction); - String hashed = ((UserImpl) u).buildPasswordValue("DWkej32H"); + String hashed = PasswordUtility.buildPasswordHash("abc"); u.changePassword(hashed); + fail("Password change must always enforce password validation."); + + } catch (ConstraintViolationException e) { + // success } finally { if (u != null) { u.remove(); Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentials.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentials.java (revision 1393356) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentials.java (working copy) @@ -17,17 +17,14 @@ package org.apache.jackrabbit.core.security.authentication; import org.apache.jackrabbit.core.security.SecurityConstants; -import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.core.security.user.PasswordUtility; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.Credentials; -import javax.jcr.RepositoryException; import javax.jcr.SimpleCredentials; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; -import java.security.MessageDigest; -import java.security.SecureRandom; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -39,9 +36,6 @@ private static final Logger log = LoggerFactory.getLogger(CryptedSimpleCredentials.class); - private final String algorithm; - private final String salt; - private final String hashedPassword; private final String userId; private final Map attributes; @@ -68,9 +62,7 @@ throw new IllegalArgumentException(); } String password = new String(pwd); - algorithm = SecurityConstants.DEFAULT_DIGEST; - salt = null; // backwards compatibility. - hashedPassword = generateHash(password, algorithm, salt); + hashedPassword = PasswordUtility.buildPasswordHash(password); String[] attNames = credentials.getAttributeNames(); attributes = new HashMap(attNames.length); @@ -85,8 +77,7 @@ * In contrast to {@link CryptedSimpleCredentials(SimpleCredentials)} that * expects the password to be plain text this constructor expects the * password to be already crypted. However, it performs a simple validation - * and calls {@link Text#digest} using the - * {@link SecurityConstants#DEFAULT_DIGEST default digest} in case the + * and calls {@link PasswordUtility#buildPasswordHash(String)} in case the * given password is found to be plain text. * * @param userId @@ -102,17 +93,11 @@ throw new IllegalArgumentException("Password may not be null."); } this.userId = userId; - String algo = extractAlgorithm(hashedPassword); - if (algo == null) { + if (PasswordUtility.isPlainTextPassword(hashedPassword)) { // password is plain text (including those starting with {invalidAlgorithm}) - log.debug("Plain text password -> Using " + SecurityConstants.DEFAULT_DIGEST + " to create digest."); - algorithm = SecurityConstants.DEFAULT_DIGEST; - salt = generateSalt(); - this.hashedPassword = generateHash(hashedPassword, algorithm, salt); + log.warn("Plain text password -> Using default algorithm to create digest."); + this.hashedPassword = PasswordUtility.buildPasswordHash(hashedPassword); } else { - // password is already hashed and started with {validAlgorithm} - algorithm = algo; - salt = extractSalt(hashedPassword, algorithm); this.hashedPassword = hashedPassword; } attributes = Collections.emptyMap(); @@ -131,7 +116,7 @@ } public String getAlgorithm() { - return algorithm; + return PasswordUtility.extractAlgorithm(hashedPassword); } public String getPassword() { @@ -162,114 +147,10 @@ if (getUserID().equalsIgnoreCase(credentials.getUserID())) { // crypt the password retrieved from the given simple credentials - // and test if it is equal to the cryptedPassword field. - return hashedPassword.equals(generateHash(String.valueOf(credentials.getPassword()), algorithm, salt)); + // and test if it is equal to the password hash defined with this + // CryptedSimpleCredentials instance. + return PasswordUtility.isSame(hashedPassword, String.valueOf(credentials.getPassword())); } return false; } - - /** - * Creates a hash of the specified password if it is found to be plain text. - * - * @param password - * @return - * @throws javax.jcr.RepositoryException - */ - public static String buildPasswordHash(String password) throws RepositoryException { - try { - return new CryptedSimpleCredentials("_", password).getPassword(); - } catch (NoSuchAlgorithmException e) { - throw new RepositoryException(e); - } catch (UnsupportedEncodingException e) { - throw new RepositoryException(e); - } - } - - /** - * @param pwd Plain text password - * @param algorithm The algorithm to be used for the digest. - * @param salt The salt to be used for the digest. - * @return Digest of the given password with leading algorithm and optionally - * salt information. - * @throws NoSuchAlgorithmException - * @throws UnsupportedEncodingException - */ - private static String generateHash(String pwd, String algorithm, String salt) - throws NoSuchAlgorithmException, UnsupportedEncodingException { - - StringBuilder password = new StringBuilder(); - password.append("{").append(algorithm).append("}"); - if (salt != null && salt.length() > 0) { - password.append(salt).append("-"); - StringBuilder data = new StringBuilder(); - data.append(salt).append(pwd); - password.append(Text.digest(algorithm, data.toString().getBytes("UTF-8"))); - } else { - password.append(Text.digest(algorithm, pwd.getBytes("UTF-8"))); - } - return password.toString(); - } - - /** - * Extract the algorithm from the given crypted password string. Returns the - * algorithm or null if the given string doesn't have a - * leading {algorithm} such as created by {@link #generateHash(String, String, String) - * or if the extracted string doesn't represent an available algorithm. - * - * @param hashedPwd - * @return The algorithm or null if the given string doesn't have a - * leading {algorith} such as created by {@link #crypt(String, String) - * or if the extracted string isn't an available algorithm. - */ - private static String extractAlgorithm(String hashedPwd) { - int end = hashedPwd.indexOf('}'); - if (hashedPwd.startsWith("{") && end > 0) { - String algorithm = hashedPwd.substring(1, end); - try { - MessageDigest.getInstance(algorithm); - return algorithm; - } catch (NoSuchAlgorithmException e) { - log.debug("Invalid algorithm detected " + algorithm); - } - } - - // not starting with {} or invalid algorithm - return null; - } - - /** - * Extract the salt from the password hash. - * - * @param hashedPwd - * @param algorithm - * @return salt or null - */ - private static String extractSalt(String hashedPwd, String algorithm) { - int start = algorithm.length()+2; - int end = hashedPwd.indexOf('-', start); - if (end > -1) { - return hashedPwd.substring(start, end); - } - - // no salt - return null; - } - - /** - * Generate a new random salt for password digest. - * - * @return a new random salt. - */ - private static String generateSalt() { - SecureRandom random = new SecureRandom(); - byte salt[] = new byte[8]; - random.nextBytes(salt); - - StringBuffer res = new StringBuffer(salt.length * 2); - for (byte b : salt) { - res.append(Text.hexTable[(b >> 4) & 15]); - res.append(Text.hexTable[b & 15]); - } - return res.toString(); - } } Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerConfig.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerConfig.java (revision 0) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerConfig.java (revision 0) @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.core.security.user.action.AuthorizableAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Properties; + +/** + * Utility to retrieve configuration parameters for UserManagerImpl + */ +class UserManagerConfig { + + private static final Logger log = LoggerFactory.getLogger(UserManagerImpl.class); + + private final Properties config; + private final String adminId; + /** + * Authorizable actions that will all be executed upon creation and removal + * of authorizables in the order they are contained in the array.

+ * Note, that if {@link #isAutoSave() autosave} is turned on, the configured + * actions are executed before persisting the creation or removal. + */ + private AuthorizableAction[] actions; + + UserManagerConfig(Properties config, String adminId, AuthorizableAction[] actions) { + this.config = config; + this.adminId = adminId; + this.actions = (actions == null) ? new AuthorizableAction[0] : actions; + } + + public T getConfigValue(String key, T defaultValue) { + if (config != null && config.containsKey(key)) { + return convert(config.get(key), defaultValue); + } else { + return defaultValue; + } + } + + public String getAdminId() { + return adminId; + } + + public AuthorizableAction[] getAuthorizableActions() { + return actions; + } + + public void setAuthorizableActions(AuthorizableAction[] actions) { + if (actions != null) { + this.actions = actions; + } + } + + //--------------------------------------------------------< private >--- + private T convert(Object v, T defaultValue) { + if (v == null) { + return null; + } + + T value; + String str = v.toString(); + Class targetClass = (defaultValue == null) ? String.class : defaultValue.getClass(); + try { + if (targetClass == String.class) { + value = (T) str; + } else if (targetClass == Integer.class) { + value = (T) Integer.valueOf(str); + } else if (targetClass == Long.class) { + value = (T) Long.valueOf(str); + } else if (targetClass == Double.class) { + value = (T) Double.valueOf(str); + } else if (targetClass == Boolean.class) { + value = (T) Boolean.valueOf(str); + } else { + // unsupported target type + log.warn("Unsupported target type {} for value {}", targetClass.getName(), v); + throw new IllegalArgumentException("Cannot convert config entry " + v + " to " + targetClass.getName()); + } + } catch (NumberFormatException e) { + log.warn("Invalid value {}; cannot be parsed into {}", v, targetClass.getName()); + value = defaultValue; + } + + return value; + } +} Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImpl.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImpl.java (revision 1393356) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImpl.java (working copy) @@ -26,7 +26,6 @@ import javax.jcr.Credentials; import javax.jcr.RepositoryException; -import javax.jcr.SimpleCredentials; import javax.jcr.Value; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; @@ -44,19 +43,6 @@ super(node, userManager); } - //-------------------------------------------------------------------------- - /** - * Creates a hash of the specified password if it is found to be plain text. - * - * @param password The password string. - * @return Hash for the given password string. - * @throws RepositoryException If an error occurs. - * @see CryptedSimpleCredentials#buildPasswordHash(String) - */ - static String buildPasswordValue(String password) throws RepositoryException { - return CryptedSimpleCredentials.buildPasswordHash(password); - } - //-------------------------------------------------------< Authorizable >--- /** * @see org.apache.jackrabbit.api.security.user.Authorizable#isGroup() @@ -122,8 +108,10 @@ */ public void changePassword(String password) throws RepositoryException { userManager.onPasswordChange(this, password); - Value v = getSession().getValueFactory().createValue(buildPasswordValue(password)); - userManager.setProtectedProperty(getNode(), P_PASSWORD, v); + userManager.setPassword(getNode(), password, true); + if (userManager.isAutoSave()) { + getNode().save(); + } } /** @@ -131,18 +119,10 @@ */ public void changePassword(String password, String oldPassword) throws RepositoryException { // make sure the old password matches. - try { - CryptedSimpleCredentials csc = (CryptedSimpleCredentials) getCredentials(); - SimpleCredentials creds = new SimpleCredentials(getID(), oldPassword.toCharArray()); - if (!csc.matches(creds)) { - throw new RepositoryException("Failed to change password: Old password does not match."); - } - } catch (NoSuchAlgorithmException e) { - throw new RepositoryException("Cannot change password: failed to validate old password."); - } catch (UnsupportedEncodingException e) { - throw new RepositoryException("Cannot change password: failed to validate old password."); + String pwHash = getNode().getProperty(P_PASSWORD).getString(); + if (!PasswordUtility.isSame(pwHash, oldPassword)) { + throw new RepositoryException("Failed to change password: Old password does not match."); } - changePassword(password); } Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImporter.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImporter.java (revision 1393356) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImporter.java (working copy) @@ -266,7 +266,7 @@ Value v = protectedPropInfo.getValues(PropertyType.STRING, resolver)[0]; String pw = v.getString(); - ((User) a).changePassword(pw); + userManager.setPassword(parent, pw, false); /* Execute authorizable actions for a NEW user at this point after @@ -440,7 +440,7 @@ log.info("ImportBehavior.BESTEFFORT: Found " + nonExisting.size() + " entries of rep:members pointing to non-existing authorizables. Adding to rep:members."); final NodeImpl groupNode = ((AuthorizableImpl) gr).getNode(); - if (userManager.getGroupMembershipSplitSize() > 0) { + if (userManager.hasMemberSplitSize()) { userManager.performProtectedOperation((SessionImpl) session, new SessionWriteOperation() { public Boolean perform(SessionContext context) throws RepositoryException { NodeImpl nMembers = (groupNode.hasNode(UserConstants.N_MEMBERS) Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserAccessControlProvider.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserAccessControlProvider.java (revision 1393356) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserAccessControlProvider.java (working copy) @@ -182,7 +182,7 @@ usersPath = (uMgr instanceof UserManagerImpl) ? ((UserManagerImpl) uMgr).getUsersPath() : UserConstants.USERS_PATH; groupsPath = (uMgr instanceof UserManagerImpl) ? ((UserManagerImpl) uMgr).getGroupsPath() : UserConstants.GROUPS_PATH; - membersInProperty = (!(uMgr instanceof UserManagerImpl)) || ((UserManagerImpl) uMgr).getGroupMembershipSplitSize() <= 0; + membersInProperty = !(uMgr instanceof UserManagerImpl) || !((UserManagerImpl) uMgr).hasMemberSplitSize(); if (configuration.containsKey(PARAM_ANONYMOUS_ID)) { anonymousId = (String) configuration.get(PARAM_ANONYMOUS_ID); Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/GroupImpl.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/GroupImpl.java (revision 1393356) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/GroupImpl.java (working copy) @@ -202,7 +202,7 @@ */ private MembershipProvider getMembershipProvider(NodeImpl node) throws RepositoryException { MembershipProvider msp; - if (userManager.getGroupMembershipSplitSize() > 0) { + if (userManager.hasMemberSplitSize()) { if (node.hasNode(N_MEMBERS) || !node.hasProperty(P_MEMBERS)) { msp = new NodeBasedMembershipProvider(node); } else { @@ -214,7 +214,7 @@ if (node.hasProperty(P_MEMBERS) && node.hasNode(N_MEMBERS)) { log.warn("Found members node and members property on node {}. Ignoring {} members", node, - userManager.getGroupMembershipSplitSize() > 0 ? "property" : "node"); + userManager.hasMemberSplitSize() ? "property" : "node"); } return msp; @@ -267,7 +267,7 @@ static PropertySequence getPropertySequence(Node nMembers, UserManagerImpl userManager) throws RepositoryException { Comparator order = Rank.comparableComparator(); - int maxChildren = userManager.getGroupMembershipSplitSize(); + int maxChildren = userManager.getMemberSplitSize(); int minChildren = maxChildren / 2; TreeManager treeManager = new BTreeManager(nMembers, minChildren, maxChildren, order, Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/PasswordUtility.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/PasswordUtility.java (revision 0) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/PasswordUtility.java (revision 0) @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +/** + * Utility to generate and compare password hashes. + */ +public class PasswordUtility { + + private static final Logger log = LoggerFactory.getLogger(PasswordUtility.class); + + private static final char DELIMITER = '-'; + private static final int NO_ITERATIONS = 1; + private static final String ENCODING = "UTF-8"; + + public static final String DEFAULT_ALGORITHM = "SHA-256"; + public static final int DEFAULT_SALT_SIZE = 8; + public static final int DEFAULT_ITERATIONS = 1000; + + /** + * Avoid instantiation + */ + private PasswordUtility() {} + + /** + * Generates a hash of the specified password with the default values + * for algorithm, salt-size and number of iterations. + * + * @param password The password to be hashed. + * @return The password hash. + * @throws NoSuchAlgorithmException If {@link #DEFAULT_ALGORITHM} is not supported. + * @throws UnsupportedEncodingException If utf-8 is not supported. + */ + public static String buildPasswordHash(String password) throws NoSuchAlgorithmException, UnsupportedEncodingException { + return buildPasswordHash(password, DEFAULT_ALGORITHM, DEFAULT_SALT_SIZE, DEFAULT_ITERATIONS); + } + + /** + * Generates a hash of the specified password using the specified algorithm, + * salt size and number of iterations into account. + * + * @param password The password to be hashed. + * @param algorithm The desired hash algorithm. + * @param saltSize The desired salt size. If the specified integer is lower + * that {@link #DEFAULT_SALT_SIZE} the default is used. + * @param iterations The desired number of iterations. If the specified + * integer is lower than 1 the {@link #DEFAULT_ITERATIONS default} value is used. + * @return The password hash. + * @throws NoSuchAlgorithmException If the specified algorithm is not supported. + * @throws UnsupportedEncodingException If utf-8 is not supported. + */ + public static String buildPasswordHash(String password, String algorithm, + int saltSize, int iterations) throws NoSuchAlgorithmException, UnsupportedEncodingException { + if (password == null) { + throw new IllegalArgumentException("Password may not be null."); + } + if (iterations < NO_ITERATIONS) { + iterations = DEFAULT_ITERATIONS; + } + if (saltSize < DEFAULT_SALT_SIZE) { + saltSize = DEFAULT_SALT_SIZE; + } + String salt = generateSalt(saltSize); + String alg = (algorithm == null) ? DEFAULT_ALGORITHM : algorithm; + return generateHash(password, alg, salt, iterations); + } + + /** + * Returns {@code true} if the specified string doesn't start with a + * valid algorithm name in curly brackets. + * + * @param password The string to be tested. + * @return {@code true} if the specified string doesn't start with a + * valid algorithm name in curly brackets. + */ + public static boolean isPlainTextPassword(String password) { + return extractAlgorithm(password) == null; + } + + /** + * Returns {@code true} if hash of the specified {@code password} equals the + * given hashed password. + * + * @param hashedPassword Password hash. + * @param password The password to compare. + * @return If the hash of the specified {@code password} equals the given + * {@code hashedPassword} string. + */ + public static boolean isSame(String hashedPassword, String password) { + try { + String algorithm = extractAlgorithm(hashedPassword); + if (algorithm != null) { + int startPos = algorithm.length()+2; + String salt = extractSalt(hashedPassword, startPos); + int iterations = NO_ITERATIONS; + if (salt != null) { + startPos += salt.length()+1; + iterations = extractIterations(hashedPassword, startPos); + } + + String hash = generateHash(password, algorithm, salt, iterations); + return hashedPassword.equals(hash); + } // hashedPassword is plaintext -> return false + } catch (NoSuchAlgorithmException e) { + log.warn(e.getMessage()); + } catch (UnsupportedEncodingException e) { + log.warn(e.getMessage()); + } + return false; + } + + /** + * Extract the algorithm from the given crypted password string. Returns the + * algorithm or {@code null} if the given string doesn't have a + * leading {@code algorithm} such as created by {@code buildPasswordHash} + * or if the extracted string doesn't represent an available algorithm. + * + * @param hashedPwd The password hash. + * @return The algorithm or {@code null} if the given string doesn't have a + * leading {@code algorithm} such as created by {@code buildPasswordHash} + * or if the extracted string isn't a supported algorithm. + */ + public static String extractAlgorithm(String hashedPwd) { + if (hashedPwd != null && hashedPwd.length() > 0) { + int end = hashedPwd.indexOf('}'); + if (hashedPwd.charAt(0) == '{' && end > 0 && end < hashedPwd.length()-1) { + String algorithm = hashedPwd.substring(1, end); + try { + MessageDigest.getInstance(algorithm); + return algorithm; + } catch (NoSuchAlgorithmException e) { + log.debug("Invalid algorithm detected " + algorithm); + } + } + } + + // not starting with {} or invalid algorithm + return null; + } + + //------------------------------------------------------------< private >--- + + private static String generateHash(String pwd, String algorithm, String salt, int iterations) throws NoSuchAlgorithmException, UnsupportedEncodingException { + StringBuilder passwordHash = new StringBuilder(); + passwordHash.append('{').append(algorithm).append('}'); + if (salt != null && salt.length() > 0) { + StringBuilder data = new StringBuilder(); + data.append(salt).append(pwd); + + passwordHash.append(salt).append(DELIMITER); + if (iterations > NO_ITERATIONS) { + passwordHash.append(iterations).append(DELIMITER); + } + passwordHash.append(generateDigest(data.toString(), algorithm, iterations)); + } else { + // backwards compatible to jr 2.0: no salt, no iterations + passwordHash.append(Text.digest(algorithm, pwd.getBytes(ENCODING))); + } + return passwordHash.toString(); + } + + private static String generateSalt(int saltSize) { + SecureRandom random = new SecureRandom(); + byte[] salt = new byte[saltSize]; + random.nextBytes(salt); + + StringBuilder res = new StringBuilder(salt.length * 2); + for (byte b : salt) { + res.append(Text.hexTable[(b >> 4) & 15]); + res.append(Text.hexTable[b & 15]); + } + return res.toString(); + } + + private static String generateDigest(String data, String algorithm, int iterations) throws UnsupportedEncodingException, NoSuchAlgorithmException { + byte[] bytes = data.getBytes(ENCODING); + MessageDigest md = MessageDigest.getInstance(algorithm); + + for (int i = 0; i < iterations; i++) { + md.reset(); + bytes = md.digest(bytes); + } + + StringBuilder res = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + res.append(Text.hexTable[(b >> 4) & 15]); + res.append(Text.hexTable[b & 15]); + } + return res.toString(); + } + + private static String extractSalt(String hashedPwd, int start) { + int end = hashedPwd.indexOf(DELIMITER, start); + if (end > -1) { + return hashedPwd.substring(start, end); + } + // no salt + return null; + } + + private static int extractIterations(String hashedPwd, int start) { + int end = hashedPwd.indexOf(DELIMITER, start); + if (end > -1) { + String str = hashedPwd.substring(start, end); + try { + return Integer.parseInt(str); + } catch (NumberFormatException e) { + log.debug("Expected number of iterations. Found: " + str); + } + } + + // no extra iterations + return NO_ITERATIONS; + } +} Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java (revision 1393356) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java (working copy) @@ -51,6 +51,7 @@ import javax.jcr.nodetype.ConstraintViolationException; import javax.jcr.version.VersionException; import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.util.HashSet; import java.util.Iterator; @@ -60,6 +61,8 @@ import java.util.UUID; /** + *

Implementation Characteristics

+ * * Default implementation of the UserManager interface with the * following characteristics: * @@ -78,6 +81,8 @@ * * * + *

Authorizable Creation

+ * * The built-in logic applies the following rules: *
    *
  • The names of the hierarchy folders is determined from ID of the @@ -115,9 +120,12 @@ * + aSmith [rep:User] * * + *

    Configuration

    + * * This UserManager is able to handle the following configuration * options: * + *

    Configuration Parameters

    *
      *
    • {@link #PARAM_USERS_PATH}: Defines where user nodes are created. * If missing set to {@link #USERS_PATH}.
    • @@ -142,7 +150,25 @@ *
    • {@link #PARAM_AUTO_EXPAND_SIZE}: This parameter only takes effect * if {@link #PARAM_AUTO_EXPAND_TREE} is enabled.
      The value is expected to be * a positive long greater than zero. The default value is 1000.
    • + *
    • {@link #PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE}: If this parameter is present + * group memberships are collected in a node structure below {@link UserConstants#N_MEMBERS} + * instead of the default multi valued property {@link UserConstants#P_MEMBERS}. + * Its value determines the maximum number of member properties until additional + * intermediate nodes are inserted. Valid parameter values are integers > 4.
    • + *
    • {@link #PARAM_PASSWORD_HASH_ALGORITHM}: Optional parameter to configure + * the algorithm used for password hash generation. The default value is + * {@link PasswordUtility#DEFAULT_ALGORITHM}.
    • + *
    • {@link #PARAM_PASSWORD_HASH_ITERATIONS}: Optional parameter to configure + * the number of iterations used for password hash generations. The default + * value is {@link PasswordUtility#DEFAULT_ITERATIONS}.
    • *
    + * + *

    Authorizable Actions

    + * In addition to the specified configuration parameters this user manager + * implementation allows to define zero to many {@link AuthorizableAction}s. + * Authorizable actions provide the ability to execute additional validation or + * tasks upon authorizable creation, removal and upon changing a users password.
    + * See also {@link org.apache.jackrabbit.core.config.UserManagerConfig#getAuthorizableActions()} */ public class UserManagerImpl extends ProtectedItemModifier implements UserManager, UserConstants, SessionListener { @@ -212,65 +238,41 @@ public static final String PARAM_AUTO_EXPAND_SIZE = "autoExpandSize"; /** - * If this parameter is present group memberships are collected in a node + * If this parameter is present group members are collected in a node * structure below {@link UserConstants#N_MEMBERS} instead of the default * multi valued property {@link UserConstants#P_MEMBERS}. Its value determines * the maximum number of member properties until additional intermediate nodes - * are inserted. Valid values are integers > 4. + * are inserted. Valid values are integers > 4. The default value is 0 and + * indicates that the {@link UserConstants#P_MEMBERS} property is used to + * record group members. */ public static final String PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE = "groupMembershipSplitSize"; + /** + * Configuration parameter to change the default algorithm used to generate + * password hashes. The default value is {@link PasswordUtility#DEFAULT_ALGORITHM}. + */ + public static final String PARAM_PASSWORD_HASH_ALGORITHM = "passwordHashAlgorithm"; + + /** + * Configuration parameter to change the number of iterations used for + * password hash generation. The default value is {@link PasswordUtility#DEFAULT_ITERATIONS}. + */ + public static final String PARAM_PASSWORD_HASH_ITERATIONS = "passwordHashIterations"; + private static final Logger log = LoggerFactory.getLogger(UserManagerImpl.class); private final SessionImpl session; private final String adminId; private final NodeResolver authResolver; private final NodeCreator nodeCreator; + private final UserManagerConfig config; - /** - * Configuration value defining the node where User nodes will be created. - * Default value is {@link UserConstants#USERS_PATH}. - */ private final String usersPath; - - /** - * Configuration value defining the node where Group nodes will be created. - * Default value is {@link UserConstants#GROUPS_PATH}. - */ private final String groupsPath; - - /** - * Flag indicating if {@link #getAuthorizable(String)} should be able to deal - * with users or groups created with Jackrabbit < 2.0.
    - * As of 2.0 authorizables are created using a defined logic that allows - * to retrieve them without searching/traversing. If this flag is - * true this method will try to find authorizables using the - * authResolver if not found otherwise. - */ - private final boolean compatibleJR16; - - /** - * Maximum number of properties on the group membership node structure under - * {@link UserConstants#N_MEMBERS} until additional intermediate nodes are inserted. - * If 0 (default), {@link UserConstants#P_MEMBERS} is used to record group - * memberships. - */ - private final int groupMembershipSplitSize; - - /** - * The membership cache. - */ private final MembershipCache membershipCache; /** - * Authorizable actions that will all be executed upon creation and removal - * of authorizables in the order they are contained in the array.

    - * Note, that if {@link #isAutoSave() autosave} is turned on, the configured - * actions are executed before persisting the creation or removal. - */ - private AuthorizableAction[] authorizableActions = new AuthorizableAction[0]; - - /** * Create a new UserManager with the default configuration. * * @param session The editing/reading session. @@ -317,27 +319,31 @@ */ public UserManagerImpl(SessionImpl session, String adminId, Properties config, MembershipCache mCache) throws RepositoryException { + this(session, new UserManagerConfig(config, adminId, null), mCache); + } + + /** + * Create a new UserManager for the given session. + * + * @param session The editing/reading session. + * @param config The user manager configuration. + * @param mCache The shared membership cache. + * @throws RepositoryException If an error occurs. + */ + private UserManagerImpl(SessionImpl session, UserManagerConfig config, MembershipCache mCache) throws RepositoryException { this.session = session; - this.adminId = adminId; + this.adminId = config.getAdminId(); + this.config = config; nodeCreator = new NodeCreator(config); - Object param = (config != null) ? config.get(PARAM_USERS_PATH) : null; - usersPath = (param != null) ? param.toString() : USERS_PATH; + this.usersPath = config.getConfigValue(PARAM_USERS_PATH, USERS_PATH); + this.groupsPath = config.getConfigValue(PARAM_GROUPS_PATH, GROUPS_PATH); - param = (config != null) ? config.get(PARAM_GROUPS_PATH) : null; - groupsPath = (param != null) ? param.toString() : GROUPS_PATH; - - param = (config != null) ? config.get(PARAM_COMPATIBLE_JR16) : null; - compatibleJR16 = (param != null) && Boolean.parseBoolean(param.toString()); - - param = (config != null) ? config.get(PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE) : null; - groupMembershipSplitSize = parseMembershipSplitSize(param); - if (mCache != null) { membershipCache = mCache; } else { - membershipCache = new MembershipCache(session, groupsPath, groupMembershipSplitSize > 0); + membershipCache = new MembershipCache(session, groupsPath, hasMemberSplitSize()); } NodeResolver nr; @@ -388,20 +394,35 @@ * * @return The maximum number of group members before splitting up the structure. */ - public int getGroupMembershipSplitSize() { - return groupMembershipSplitSize; + public int getMemberSplitSize() { + int splitSize = config.getConfigValue(PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE, 0); + if (splitSize != 0 && splitSize < 4) { + log.warn("Invalid value {} for {}. Expected integer >= 4", splitSize, PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE); + splitSize = 0; + } + return splitSize; } /** + * Returns true if the split-member configuration parameter + * is greater or equal than 4 indicating that group members should be stored + * in a tree instead of a single multivalued property. + * + * @return true if group members are being stored in a tree instead of a + * single multivalued property. + */ + public boolean hasMemberSplitSize() { + return getMemberSplitSize() >= 4; + } + + /** * Set the authorizable actions that will be invoked upon authorizable * creation and removal. * * @param authorizableActions An array of authorizable actions. */ public void setAuthorizableActions(AuthorizableAction[] authorizableActions) { - if (authorizableActions != null) { - this.authorizableActions = authorizableActions; - } + config.setAuthorizableActions(authorizableActions); } //--------------------------------------------------------< UserManager >--- @@ -548,15 +569,14 @@ Principal principal, String intermediatePath) throws AuthorizableExistsException, RepositoryException { checkValidID(userID); - if (password == null) { - throw new IllegalArgumentException("Cannot create user: null password."); - } + + // NOTE: password validation during setPassword and onCreate. // NOTE: principal validation during setPrincipal call. try { NodeImpl userNode = (NodeImpl) nodeCreator.createUserNode(userID, intermediatePath); setPrincipal(userNode, principal); - setProperty(userNode, P_PASSWORD, getValue(UserImpl.buildPasswordValue(password)), true); + setPassword(userNode, password, true); User user = createUser(userNode); onCreate(user, password); @@ -577,8 +597,7 @@ /** * @see UserManager#createGroup(String) */ - public Group createGroup(String groupID) - throws AuthorizableExistsException, RepositoryException { + public Group createGroup(String groupID) throws AuthorizableExistsException, RepositoryException { return createGroup(groupID, new PrincipalImpl(groupID), null); } @@ -705,6 +724,39 @@ setProperty(node, P_PRINCIPAL_NAME, getValue(principal.getName()), true); } + /** + * Generate a password value from the specified string and set the + * {@link UserConstants#P_PASSWORD} property to the given user node. + * + * @param userNode A user node. + * @param password The password value. + * @param forceHash If true the specified password string will + * always be hashed; otherwise the hash will only be generated if it appears + * to be a {@link PasswordUtility#isPlainTextPassword(String) plain text} password. + * @throws RepositoryException If an exception occurs. + */ + void setPassword(NodeImpl userNode, String password, boolean forceHash) throws RepositoryException { + if (password == null) { + throw new IllegalArgumentException("Password may not be null."); + } + String pwHash; + if (forceHash || PasswordUtility.isPlainTextPassword(password)) { + try { + String algorithm = config.getConfigValue(PARAM_PASSWORD_HASH_ALGORITHM, PasswordUtility.DEFAULT_ALGORITHM); + int iterations = config.getConfigValue(PARAM_PASSWORD_HASH_ITERATIONS, PasswordUtility.DEFAULT_ITERATIONS); + pwHash = PasswordUtility.buildPasswordHash(password, algorithm, PasswordUtility.DEFAULT_SALT_SIZE, iterations); + } catch (NoSuchAlgorithmException e) { + throw new RepositoryException(e); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(e); + } + } else { + pwHash = password; + } + Value v = getSession().getValueFactory().createValue(pwHash); + setProperty(userNode, P_PASSWORD, getValue(pwHash), userNode.isNew()); + } + void setProtectedProperty(NodeImpl node, Name propName, Value value) throws RepositoryException, LockException, ConstraintViolationException, ItemExistsException, VersionException { setProperty(node, propName, value); if (isAutoSave()) { @@ -831,6 +883,7 @@ try { n = session.getNodeById(nodeId); } catch (ItemNotFoundException e) { + boolean compatibleJR16 = config.getConfigValue(PARAM_COMPATIBLE_JR16, false); if (compatibleJR16) { // backwards-compatibility with JR < 2.0 user/group structure that doesn't // allow to determine existence of an authorizable from the id directly. @@ -1018,27 +1071,6 @@ } } - private static int parseMembershipSplitSize(Object param) { - int n = 0; - if (param != null) { - try { - n = Integer.parseInt(param.toString()); - if (n < 4) { - n = 0; - } - } - catch (NumberFormatException e) { - n = 0; - } - if (n == 0) { - log.warn("Invalid value {} for {}. Expected integer >= 4", - param.toString(), PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE); - } - } - - return n; - } - //-------------------------------------------------------------------------- /** * Let the configured AuthorizableActions perform additional @@ -1050,7 +1082,7 @@ * @throws RepositoryException If an exception occurs. */ void onCreate(User user, String pw) throws RepositoryException { - for (AuthorizableAction action : authorizableActions) { + for (AuthorizableAction action : config.getAuthorizableActions()) { action.onCreate(user, pw, session); } } @@ -1064,7 +1096,7 @@ * @throws RepositoryException If an exception occurs. */ void onCreate(Group group) throws RepositoryException { - for (AuthorizableAction action : authorizableActions) { + for (AuthorizableAction action : config.getAuthorizableActions()) { action.onCreate(group, session); } } @@ -1078,7 +1110,7 @@ * @throws RepositoryException If an exception occurs. */ void onRemove(Authorizable authorizable) throws RepositoryException { - for (AuthorizableAction action : authorizableActions) { + for (AuthorizableAction action : config.getAuthorizableActions()) { action.onRemove(authorizable, session); } } @@ -1093,7 +1125,7 @@ * @throws RepositoryException If an exception occurs. */ void onPasswordChange(User user, String password) throws RepositoryException { - for (AuthorizableAction action : authorizableActions) { + for (AuthorizableAction action : config.getAuthorizableActions()) { action.onPasswordChange(user, password, session); } } @@ -1331,37 +1363,23 @@ // all child nodes. private final long autoExpandSize; - private NodeCreator(Properties config) { + private NodeCreator(UserManagerConfig config) { int d = DEFAULT_DEPTH; boolean expand = false; long size = DEFAULT_SIZE; if (config != null) { - if (config.containsKey(PARAM_DEFAULT_DEPTH)) { - try { - d = Integer.parseInt(config.get(PARAM_DEFAULT_DEPTH).toString()); - if (d <= 0) { - log.warn("Invalid defaultDepth '" + d + "' -> using default."); - d = DEFAULT_DEPTH; - } - } catch (NumberFormatException e) { - log.warn("Unable to parse defaultDepth config parameter -> using default.", e); - } + d = config.getConfigValue(PARAM_DEFAULT_DEPTH, DEFAULT_DEPTH); + if (d <= 0) { + log.warn("Invalid defaultDepth '" + d + "' -> using default."); + d = DEFAULT_DEPTH; } - if (config.containsKey(PARAM_AUTO_EXPAND_TREE)) { - expand = Boolean.parseBoolean(config.get(PARAM_AUTO_EXPAND_TREE).toString()); + expand = config.getConfigValue(PARAM_AUTO_EXPAND_TREE, false); + size = config.getConfigValue(PARAM_AUTO_EXPAND_SIZE, DEFAULT_SIZE); + if (expand && size <= 0) { + log.warn("Invalid autoExpandSize '" + size + "' -> using default."); + size = DEFAULT_SIZE; } - if (config.containsKey(PARAM_AUTO_EXPAND_SIZE)) { - try { - size = Integer.parseInt(config.get(PARAM_AUTO_EXPAND_SIZE).toString()); - if (expand && size <= 0) { - log.warn("Invalid autoExpandSize '" + size + "' -> using default."); - size = DEFAULT_SIZE; - } - } catch (NumberFormatException e) { - log.warn("Unable to parse autoExpandSize config parameter -> using default.", e); - } - } } defaultDepth = d; Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/PasswordValidationAction.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/PasswordValidationAction.java (revision 1393356) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/PasswordValidationAction.java (working copy) @@ -17,7 +17,7 @@ package org.apache.jackrabbit.core.security.user.action; import org.apache.jackrabbit.api.security.user.User; -import org.apache.jackrabbit.core.security.authentication.CryptedSimpleCredentials; +import org.apache.jackrabbit.core.security.user.PasswordUtility; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -70,12 +70,12 @@ //-------------------------------------------------< AuthorizableAction >--- @Override public void onCreate(User user, String password, Session session) throws RepositoryException { - validatePassword(password); + validatePassword(password, false); // don't force validation of hashed passwords. } @Override public void onPasswordChange(User user, String newPassword, Session session) throws RepositoryException { - validatePassword(newPassword); + validatePassword(newPassword, true); // force validation of all passwords } //---------------------------------------------------------< BeanConfig >--- @@ -97,23 +97,16 @@ * Validate the specified password. * * @param password The password to be validated + * @param forceMatch If true the specified password is always validated; + * otherwise only if it is a plain text password * @throws RepositoryException If the specified password is too short or * doesn't match the specified password pattern. */ - private void validatePassword(String password) throws RepositoryException { - if (password != null && isPlainText(password)) { + private void validatePassword(String password, boolean forceMatch) throws RepositoryException { + if (password != null && (forceMatch || PasswordUtility.isPlainTextPassword(password))) { if (pattern != null && !pattern.matcher(password).matches()) { throw new ConstraintViolationException("Password violates password constraint (" + pattern.pattern() + ")."); } } } - - private static boolean isPlainText(String password) { - try { - return !CryptedSimpleCredentials.buildPasswordHash(password).equals(password); - } catch (RepositoryException e) { - // failed to build hash from pw -> proceed with the validation. - return true; - } - } } \ No newline at end of file