| /* |
| * 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.oak.security.user; |
| |
| import java.lang.reflect.Field; |
| import java.util.Collections; |
| import java.util.List; |
| import javax.jcr.RepositoryException; |
| import javax.jcr.nodetype.ConstraintViolationException; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Iterables; |
| import org.apache.jackrabbit.api.security.user.User; |
| import org.apache.jackrabbit.oak.AbstractSecurityTest; |
| import org.apache.jackrabbit.oak.api.Tree; |
| import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; |
| import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; |
| import org.apache.jackrabbit.oak.spi.security.user.UserConstants; |
| import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtil; |
| import org.apache.jackrabbit.oak.plugins.tree.TreeUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.junit.Test; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotSame; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| /** |
| * @see <a href="https://issues.apache.org/jira/browse/OAK-2445">OAK-2445</a> |
| */ |
| public class PasswordHistoryTest extends AbstractSecurityTest implements UserConstants { |
| |
| private static final String[] PASSWORDS = { |
| "abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz0", "123", "456", "789" |
| }; |
| |
| private static final ConfigurationParameters CONFIG = ConfigurationParameters.of(PARAM_PASSWORD_HISTORY_SIZE, 10); |
| |
| @Override |
| protected ConfigurationParameters getSecurityConfigParameters() { |
| return ConfigurationParameters.of(ImmutableMap.of(UserConfiguration.NAME, CONFIG)); |
| } |
| |
| @NotNull |
| private List<String> getHistory(@NotNull User user) throws RepositoryException { |
| Iterable<String> history = TreeUtil.getStrings(root.getTree(user.getPath()).getChild(REP_PWD), REP_PWD_HISTORY); |
| return (history == null) ? Collections.emptyList() : ImmutableList.copyOf(history).reverse(); |
| } |
| |
| /** |
| * Use reflection to access the private fields stored in the PasswordHistory |
| */ |
| private static Integer getMaxSize(@NotNull PasswordHistory history) throws Exception { |
| Field maxSize = history.getClass().getDeclaredField("maxSize"); |
| maxSize.setAccessible(true); |
| return (Integer) maxSize.get(history); |
| } |
| |
| /** |
| * Use reflection to access the private fields stored in the PasswordHistory |
| */ |
| private static boolean isEnabled(@NotNull PasswordHistory history) throws Exception { |
| Field isEnabled = history.getClass().getDeclaredField("isEnabled"); |
| isEnabled.setAccessible(true); |
| return (Boolean) isEnabled.get(history); |
| } |
| |
| @Test |
| public void testNoPwdTreeOnUserCreation() throws Exception { |
| User user = getTestUser(); |
| assertFalse(root.getTree(user.getPath()).hasChild(REP_PWD)); |
| } |
| |
| @Test |
| public void testHistoryEmptyOnUserCreationWithPassword() throws Exception { |
| User user = getTestUser(); // the user is created with a password set |
| |
| // the rep:pwd child must not exist. without the rep:pwd child no password history can exist. |
| assertFalse(root.getTree(user.getPath()).hasChild(REP_PWD)); |
| } |
| |
| @Test |
| public void testHistoryWithSinglePasswordChange() throws Exception { |
| // the user must be able to change the password |
| User user = getTestUser(); |
| String oldPassword = TreeUtil.getString(root.getTree(user.getPath()), REP_PASSWORD); |
| user.changePassword("newPwd"); |
| root.commit(); |
| |
| // after changing the password, 1 password history entry should be present and the |
| // recorded password should be equal to the user's initial password |
| // however, the user's current password must not match the old password. |
| assertTrue(root.getTree(user.getPath()).hasChild(REP_PWD)); |
| |
| Tree pwTree = root.getTree(user.getPath()).getChild(REP_PWD); |
| assertTrue(pwTree.hasProperty(REP_PWD_HISTORY)); |
| |
| List<String> history = getHistory(user); |
| assertEquals(1, history.size()); |
| assertEquals(oldPassword, history.iterator().next()); |
| |
| String currentPw = TreeUtil.getString(root.getTree(user.getPath()), REP_PASSWORD); |
| assertNotSame(currentPw, oldPassword); |
| } |
| |
| @Test |
| public void testHistoryMaxSize() throws Exception { |
| User user = getTestUser(); |
| |
| // we're changing the password 12 times, history max is 10 |
| for (String pw : PASSWORDS) { |
| user.changePassword(pw); |
| root.commit(); |
| } |
| |
| assertEquals(10, getHistory(user).size()); |
| } |
| |
| @Test |
| public void testHistoryOrder() throws Exception { |
| User user = getTestUser(); |
| |
| // we're changing the password 12 times, history max is 10 |
| for (String pw : PASSWORDS) { |
| user.changePassword(pw); |
| } |
| |
| // we skip the first entry in the password list as it was shifted out |
| // due to max history size = 10. |
| int i = 1; |
| for (String pwHash : getHistory(user)) { |
| assertTrue(PasswordUtil.isSame(pwHash, PASSWORDS[i++])); |
| } |
| } |
| |
| @Test |
| public void testRepeatedPwAfterHistorySizeReached() throws Exception { |
| User user = getTestUser(); |
| for (String pw : PASSWORDS) { |
| user.changePassword(pw); |
| } |
| |
| // changing pw back to the original value (as used for creation) must succeed |
| user.changePassword(user.getID()); |
| // now, using all old passwords must also succeed as they get shifted out |
| for (String pw : PASSWORDS) { |
| user.changePassword(pw); |
| } |
| } |
| |
| @Test(expected = ConstraintViolationException.class) |
| public void testHistoryViolationAtFirstChange() throws Exception { |
| User user = getTestUser(); |
| user.changePassword(user.getID()); |
| } |
| |
| @Test(expected = ConstraintViolationException.class) |
| public void testHistoryViolation() throws Exception { |
| User user = getTestUser(); |
| user.changePassword("abc"); |
| user.changePassword("def"); |
| user.changePassword("abc"); |
| } |
| |
| @Test |
| public void testNoHistoryUpdateOnViolation() throws Exception { |
| User user = getTestUser(); |
| try { |
| user.changePassword("abc"); |
| user.changePassword("def"); |
| user.changePassword("abc"); |
| fail("history violation not detected"); |
| } catch (ConstraintViolationException e) { |
| String[] expected = new String[] {user.getID(), "abc"}; |
| int i = 0; |
| for (String pwHash : getHistory(user)) { |
| assertTrue(PasswordUtil.isSame(pwHash, expected[i++])); |
| } |
| } |
| } |
| |
| @Test |
| public void testEnabledPasswordHistory() throws Exception { |
| PasswordHistory history = new PasswordHistory(CONFIG); |
| assertTrue(isEnabled(history)); |
| assertEquals(10, getMaxSize(history).longValue()); |
| } |
| |
| @Test |
| public void testHistoryUpperLimit() throws Exception { |
| PasswordHistory history = new PasswordHistory(ConfigurationParameters.of(PARAM_PASSWORD_HISTORY_SIZE, Integer.MAX_VALUE)); |
| |
| assertTrue(isEnabled(history)); |
| assertEquals(1000, getMaxSize(history).longValue()); |
| } |
| |
| @Test |
| public void testDisabledPasswordHistory() throws Exception { |
| User user = getTestUser(); |
| Tree userTree = root.getTree(user.getPath()); |
| |
| List<ConfigurationParameters> configs = ImmutableList.of( |
| ConfigurationParameters.EMPTY, |
| ConfigurationParameters.of(PARAM_PASSWORD_HISTORY_SIZE, PASSWORD_HISTORY_DISABLED_SIZE), |
| ConfigurationParameters.of(PARAM_PASSWORD_HISTORY_SIZE, -1), |
| ConfigurationParameters.of(PARAM_PASSWORD_HISTORY_SIZE, Integer.MIN_VALUE) |
| ); |
| |
| for (ConfigurationParameters config : configs) { |
| PasswordHistory disabledHistory = new PasswordHistory(config); |
| |
| assertFalse(isEnabled(disabledHistory)); |
| assertFalse(disabledHistory.updatePasswordHistory(userTree, user.getID())); |
| } |
| } |
| |
| @Test(expected = ConstraintViolationException.class) |
| public void testCheckPasswordHistory() throws Exception { |
| Tree userTree = root.getTree(getTestUser().getPath()); |
| |
| PasswordHistory history = new PasswordHistory(CONFIG); |
| assertTrue(isEnabled(history)); |
| assertEquals(10, getMaxSize(history).longValue()); |
| |
| history.updatePasswordHistory(userTree, getTestUser().getID()); |
| } |
| |
| @Test |
| public void testConfigurationChange() throws Exception { |
| User user = getTestUser(); |
| Tree userTree = root.getTree(user.getPath()); |
| |
| PasswordHistory history = new PasswordHistory(CONFIG); |
| for (String pw : PASSWORDS) { |
| assertTrue(history.updatePasswordHistory(userTree, pw)); |
| } |
| assertEquals(10, getHistory(user).size()); |
| |
| // change configuration to a smaller size |
| history = new PasswordHistory(ConfigurationParameters.of(PARAM_PASSWORD_HISTORY_SIZE, 5)); |
| List<String> oldPwds = getHistory(user); |
| assertEquals(10, oldPwds.size()); |
| |
| // only the configured max-size number of entries in the history must be |
| // checked. additional entries in the history must be ignored |
| Iterables.skip(oldPwds, 6); |
| history.updatePasswordHistory(userTree, oldPwds.iterator().next()); |
| |
| // after chaning the pwd again however the rep:pwdHistory property must |
| // only contain the max-size number of passwords |
| assertEquals(5, getHistory(user).size()); |
| |
| history = new PasswordHistory(CONFIG); |
| history.updatePasswordHistory(userTree, "newPwd"); |
| assertEquals(6, getHistory(user).size()); |
| } |
| |
| @Test |
| public void testEnableDisable() throws Exception { |
| User user = getTestUser(); |
| Tree userTree = root.getTree(user.getPath()); |
| |
| PasswordHistory history = new PasswordHistory(CONFIG); |
| for (String pw : PASSWORDS) { |
| assertTrue(history.updatePasswordHistory(userTree, pw)); |
| } |
| assertEquals(10, getHistory(user).size()); |
| |
| // disable the password history : changing the pw now must not |
| // modify the rep:pwdHistory property. |
| history = new PasswordHistory(ConfigurationParameters.EMPTY); |
| history.updatePasswordHistory(userTree, PASSWORDS[8]); |
| |
| assertEquals(10, getHistory(user).size()); |
| } |
| |
| @Test |
| public void testSingleTypeHistoryProperty() throws Exception { |
| Tree userTree = root.getTree(getTestUser().getPath()); |
| Tree pwdNode = TreeUtil.getOrAddChild(userTree, REP_PWD, NT_REP_PASSWORD); |
| |
| pwdNode.setProperty(REP_PWD_HISTORY, "singleValuedProperty"); |
| assertFalse(pwdNode.getProperty(REP_PWD_HISTORY).isArray()); |
| assertFalse(pwdNode.getProperty(REP_PWD_HISTORY).getType().isArray()); |
| |
| PasswordHistory history = new PasswordHistory(CONFIG); |
| assertTrue(history.updatePasswordHistory(userTree, "anyOtherPassword")); |
| |
| assertTrue(pwdNode.getProperty(REP_PWD_HISTORY).isArray()); |
| assertTrue(pwdNode.getProperty(REP_PWD_HISTORY).getType().isArray()); |
| } |
| |
| @Test |
| public void testUpdateMissingPwHash() throws Exception { |
| User u = getUserManager(root).createUser("uid", null); |
| PasswordHistory ph = new PasswordHistory(ConfigurationParameters.of(UserConstants.PARAM_PASSWORD_HISTORY_SIZE, UserConstants.PASSWORD_HISTORY_DISABLED_SIZE+1)); |
| assertFalse(ph.updatePasswordHistory(root.getTree(u.getPath()), "pw")); |
| } |
| } |