Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java (revision 1844811) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java (revision ) @@ -471,7 +471,7 @@ boolean forceInitialPwChange = forceInitialPasswordChangeEnabled(); boolean isNewUser = userTree.getStatus() == Tree.Status.NEW; - if (!UserUtil.isAdmin(config, userId) + if (Utils.canHavePasswordExpired(userId, config) // only expiry is enabled, set in all cases && ((expiryEnabled && !forceInitialPwChange) // as soon as force initial pw is enabled, we set in all cases except new users, Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthentication.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthentication.java (revision 1844811) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserAuthentication.java (revision ) @@ -79,6 +79,8 @@ private static final Logger log = LoggerFactory.getLogger(UserAuthentication.class); + public static final String PARAM_PASSWORD_EXPIRY_FOR_ADMIN = "passwordExpiryForAdmin"; + private final UserConfiguration config; private final Root root; private final String loginId; @@ -242,13 +244,13 @@ } private boolean isPasswordExpired(@NotNull User user) throws RepositoryException { - // the password of the "admin" user never expires - if (user.isAdmin()) { + ConfigurationParameters params = config.getParameters(); + // unless PARAM_PASSWORD_EXPIRY_FOR_ADMIN is enabled, the password of the "admin" user never expires + if (!Utils.canHavePasswordExpired(user, params)) { return false; } boolean expired = false; - ConfigurationParameters params = config.getParameters(); int maxAge = params.getConfigValue(PARAM_PASSWORD_MAX_AGE, DEFAULT_PASSWORD_MAX_AGE); boolean forceInitialPwChange = params.getConfigValue(PARAM_PASSWORD_INITIAL_CHANGE, DEFAULT_PASSWORD_INITIAL_CHANGE); if (maxAge > 0) { Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/Utils.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/Utils.java (revision 1844811) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/Utils.java (revision ) @@ -18,13 +18,16 @@ import javax.jcr.AccessDeniedException; +import org.apache.jackrabbit.api.security.user.User; import org.apache.jackrabbit.oak.api.Tree; import org.apache.jackrabbit.oak.commons.PathUtils; import org.apache.jackrabbit.oak.plugins.tree.TreeUtil; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.user.util.UserUtil; import org.apache.jackrabbit.util.Text; import org.jetbrains.annotations.NotNull; -class Utils { +final class Utils { private Utils() {} @@ -74,6 +77,22 @@ } else { return t; } + } + } + + static boolean canHavePasswordExpired(@NotNull String userId, @NotNull ConfigurationParameters config) { + if (UserUtil.isAdmin(config, userId) && !config.getConfigValue(UserAuthentication.PARAM_PASSWORD_EXPIRY_FOR_ADMIN, false)) { + return false; + } else { + return true; + } + } + + static boolean canHavePasswordExpired(@NotNull User user, @NotNull ConfigurationParameters config) { + if (user.isAdmin() && !config.getConfigValue(UserAuthentication.PARAM_PASSWORD_EXPIRY_FOR_ADMIN, false)) { + return false; + } else { + return true; } } } Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java (revision 1844811) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java (revision ) @@ -151,6 +151,12 @@ int passwordHistorySize() default UserConstants.PASSWORD_HISTORY_DISABLED_SIZE; @AttributeDefinition( + name = "Enable Password Expiry for Admin User", + description = "When enabled, the admin user will also be subject to password expiry. The default value is false for backwards compatibility." + ) + boolean passwordExpiryForAdmin() default false; + + @AttributeDefinition( name = "Principal Cache Expiration", description = "Optional configuration defining the number of milliseconds " + "until the principal cache expires (NOTE: currently only respected for principal resolution " + Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryTest.java (revision 1844811) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryTest.java (revision ) @@ -154,4 +154,10 @@ root.commit(); assertTrue(a.authenticate(new SimpleCredentials(userId, userId.toCharArray()))); } + + @Test + public void testByDefaultAdminHasNoPwNode() throws Exception { + User adminUser = getUserManager(root).getAuthorizable(getUserConfiguration().getParameters().getConfigValue(UserConstants.PARAM_ADMIN_ID, UserConstants.DEFAULT_ADMIN_ID), User.class); + assertFalse(root.getTree(adminUser.getPath()).getChild(UserConstants.REP_PWD).exists()); + } } Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryAdminTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryAdminTest.java (revision ) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/PasswordExpiryAdminTest.java (revision ) @@ -0,0 +1,167 @@ +/* + * 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.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.SimpleCredentials; +import javax.security.auth.Subject; +import javax.security.auth.login.CredentialExpiredException; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.oak.AbstractSecurityTest; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.nodetype.ReadOnlyNodeTypeManager; +import org.apache.jackrabbit.oak.plugins.tree.TreeUtil; +import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.authentication.Authentication; +import org.apache.jackrabbit.oak.spi.security.authentication.SystemSubject; +import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.jetbrains.annotations.NotNull; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class PasswordExpiryAdminTest extends AbstractSecurityTest { + + private User user; + private String userId; + + @Before + public void before() throws Exception { + super.before(); + user = getUserManager(root).getAuthorizable(UserConstants.DEFAULT_ADMIN_ID, User.class); + userId = user.getID(); + } + + + @Override + protected ConfigurationParameters getSecurityConfigParameters() { + ConfigurationParameters userConfig = ConfigurationParameters.of( + UserConstants.PARAM_PASSWORD_MAX_AGE, 10, + UserAuthentication.PARAM_PASSWORD_EXPIRY_FOR_ADMIN, true); + return ConfigurationParameters.of(UserConfiguration.NAME, userConfig); + } + + @NotNull + @Override + protected ContentSession createAdminSession(@NotNull ContentRepository repository) throws LoginException, NoSuchWorkspaceException { + try { + return Subject.doAs(SystemSubject.INSTANCE, new PrivilegedExceptionAction() { + @Override + public ContentSession run() throws NoSuchWorkspaceException, LoginException { + return repository.login(null, null); + } + }); + } catch (PrivilegedActionException e) { + throw new RuntimeException(e); + } + } + + @Test + public void testUserNode() throws Exception { + Tree pwdTree = root.getTree(user.getPath()).getChild(UserConstants.REP_PWD); + assertTrue(pwdTree.exists()); + assertTrue(TreeUtil.isNodeType(pwdTree, UserConstants.NT_REP_PASSWORD, root.getTree(NodeTypeConstants.NODE_TYPES_PATH))); + + ReadOnlyNodeTypeManager ntMgr = ReadOnlyNodeTypeManager.getInstance(root, getNamePathMapper()); + assertTrue(ntMgr.getDefinition(pwdTree.getParent(), pwdTree).isProtected()); + + PropertyState property = pwdTree.getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED); + assertNotNull(property); + assertEquals(Type.LONG, property.getType()); + assertTrue(property.getValue(Type.LONG, 0) > 0); + + // protected properties must not be exposed by User#hasProperty + assertFalse(user.hasProperty(UserConstants.REP_PWD + "/" + UserConstants.REP_PASSWORD_LAST_MODIFIED)); + } + + @Test + public void testChangePassword() throws Exception { + PropertyState p1 = root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED); + long oldModTime = p1.getValue(Type.LONG, 0); + assertTrue(oldModTime > 0); + waitForSystemTimeIncrement(oldModTime); + user.changePassword(user.getID()); + root.commit(); + PropertyState p2 = root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).getProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED); + long newModTime = p2.getValue(Type.LONG, 0); + assertTrue(newModTime > oldModTime); + } + + @Test + public void testAuthenticatePasswordExpiredNewUser() throws Exception { + Authentication a = new UserAuthentication(getUserConfiguration(), root, userId); + // during user creation pw last modified is set, thus it shouldn't expire + a.authenticate(new SimpleCredentials(userId, userId.toCharArray())); + } + + @Test + public void testAuthenticatePasswordExpired() throws Exception { + Authentication a = new UserAuthentication(getUserConfiguration(), root, userId); + // set password last modified to beginning of epoch + root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).setProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED, 0); + root.commit(); + try { + a.authenticate(new SimpleCredentials(userId, userId.toCharArray())); + fail("Credentials should be expired"); + } catch (CredentialExpiredException e) { + // success + } + } + + @Test + public void testAuthenticateBeforePasswordExpired() throws Exception { + Authentication a = new UserAuthentication(getUserConfiguration(), root, userId); + // set password last modified to beginning of epoch + root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).setProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED, 0); + root.commit(); + try { + a.authenticate(new SimpleCredentials(userId, "wrong".toCharArray())); + } catch (CredentialExpiredException e) { + fail("Login should fail before expiry"); + } catch (LoginException e) { + // success - userId/pw mismatch takes precedence over expiry + } + } + + @Test + public void testAuthenticatePasswordExpiredChangePassword() throws Exception { + Authentication a = new UserAuthentication(getUserConfiguration(), root, userId); + // set password last modified to beginning of epoch + root.getTree(user.getPath()).getChild(UserConstants.REP_PWD).setProperty(UserConstants.REP_PASSWORD_LAST_MODIFIED, 0); + root.commit(); + + // changing the password should reset the pw last mod and the pw no longer be expired + user.changePassword(userId); + root.commit(); + assertTrue(a.authenticate(new SimpleCredentials(userId, userId.toCharArray()))); + } +} Index: oak-core/src/test/java/org/apache/jackrabbit/oak/AbstractSecurityTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/AbstractSecurityTest.java (revision 1844811) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/AbstractSecurityTest.java (revision ) @@ -16,12 +16,9 @@ */ package org.apache.jackrabbit.oak; -import static com.google.common.collect.Lists.newArrayList; - import java.util.Arrays; import java.util.List; import java.util.UUID; - import javax.jcr.Credentials; import javax.jcr.NoSuchWorkspaceException; import javax.jcr.RepositoryException; @@ -71,6 +68,8 @@ import org.junit.After; import org.junit.Before; +import static com.google.common.collect.Lists.newArrayList; + /** * AbstractOakTest is the base class for oak test execution. */ @@ -108,7 +107,7 @@ withEditors(oak); contentRepository = oak.createContentRepository(); - adminSession = login(getAdminCredentials()); + adminSession = createAdminSession(contentRepository); root = adminSession.getLatestRoot(); Configuration.setConfiguration(getConfiguration()); @@ -175,6 +174,11 @@ protected Credentials getAdminCredentials() { String adminId = UserUtil.getAdminId(getUserConfiguration().getParameters()); return new SimpleCredentials(adminId, adminId.toCharArray()); + } + + @NotNull + protected ContentSession createAdminSession(@NotNull ContentRepository repository) throws LoginException, NoSuchWorkspaceException { + return repository.login(getAdminCredentials(), null); } protected NamePathMapper getNamePathMapper() {