Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImpl.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/UserImpl.java (revision 1846497) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImpl.java (revision ) @@ -131,6 +131,9 @@ if (isAdmin) { throw new RepositoryException("The administrator user cannot be disabled."); } + + getUserManager().onDisable(this, reason); + Tree tree = getTree(); if (reason == null) { if (tree.hasProperty(REP_DISABLED)) { 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 1846497) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java (revision ) @@ -20,13 +20,12 @@ import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.util.Iterator; -import java.util.List; import java.util.Set; import javax.jcr.RepositoryException; import javax.jcr.UnsupportedRepositoryOperationException; import com.google.common.base.Strings; -import com.google.common.collect.Lists; +import com.google.common.collect.Iterables; import org.apache.jackrabbit.api.security.principal.PrincipalManager; import org.apache.jackrabbit.api.security.user.Authorizable; import org.apache.jackrabbit.api.security.user.AuthorizableExistsException; @@ -39,6 +38,7 @@ import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.namepath.NamePathMapper; import org.apache.jackrabbit.oak.plugins.nodetype.ReadOnlyNodeTypeManager; +import org.apache.jackrabbit.oak.plugins.tree.TreeUtil; import org.apache.jackrabbit.oak.plugins.value.jcr.PartialValueFactory; import org.apache.jackrabbit.oak.security.user.query.UserQueryManager; import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; @@ -53,9 +53,9 @@ import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableActionProvider; import org.apache.jackrabbit.oak.spi.security.user.action.DefaultAuthorizableActionProvider; import org.apache.jackrabbit.oak.spi.security.user.action.GroupAction; +import org.apache.jackrabbit.oak.spi.security.user.action.UserAction; import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtil; import org.apache.jackrabbit.oak.spi.security.user.util.UserUtil; -import org.apache.jackrabbit.oak.plugins.tree.TreeUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -315,6 +315,22 @@ } } + void onDisable(@NotNull User user, @Nullable String disableReason) throws RepositoryException { + for (UserAction action : filterUserActions()) { + action.onDisable(user, disableReason, root, namePathMapper); + } + } + + void onImpersonation(@NotNull User user, @NotNull Principal principal, boolean granting) throws RepositoryException { + for (UserAction action : filterUserActions()) { + if (granting) { + action.onGrantImpersonation(user, principal, root, namePathMapper); + } else { + action.onRevokeImpersonation(user, principal, root, namePathMapper); + } + } + } + /** * Upon a group being updated (single {@code Authorizable} successfully added or removed), * call available {@code GroupAction}s and execute the method specific to removal or addition. @@ -326,7 +342,7 @@ * @throws RepositoryException If an error occurs. */ void onGroupUpdate(@NotNull Group group, boolean isRemove, @NotNull Authorizable member) throws RepositoryException { - for (GroupAction action : selectGroupActions()) { + for (GroupAction action : filterGroupActions()) { if (isRemove) { action.onMemberRemoved(group, member, root, namePathMapper); } else { @@ -348,7 +364,7 @@ * @throws RepositoryException If an error occurs. */ void onGroupUpdate(@NotNull Group group, boolean isRemove, boolean isContentId, @NotNull Set memberIds, @NotNull Set failedIds) throws RepositoryException { - for (GroupAction action : selectGroupActions()) { + for (GroupAction action : filterGroupActions()) { if (isRemove) { action.onMembersRemoved(group, memberIds, failedIds, root, namePathMapper); } else { @@ -506,13 +522,12 @@ * @return A {@code List} of {@code GroupAction}s. List may be empty. */ @NotNull - private List selectGroupActions() { - List actions = Lists.newArrayList(); - for (AuthorizableAction action : actionProvider.getAuthorizableActions(securityProvider)) { - if (action instanceof GroupAction) { - actions.add((GroupAction) action); + private Iterable filterGroupActions() { + return Iterables.filter(actionProvider.getAuthorizableActions(securityProvider), GroupAction.class); - } + } - } - return actions; + + @NotNull + private Iterable filterUserActions() { + return Iterables.filter(actionProvider.getAuthorizableActions(securityProvider), UserAction.class); } } Index: oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/UserAction.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/UserAction.java (revision ) +++ oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/UserAction.java (revision ) @@ -0,0 +1,80 @@ +/* + * 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.spi.security.user.action; + +import java.security.Principal; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * The {@code UserAction} interface allows for implementations to be informed + * about and react to the following changes to a {@link User}: + * + *
    + *
  • {@link #onDisable(User, String, Root, NamePathMapper)}
  • + *
+ * + *

+ * See {@link AuthorizableAction} for details on persisting changes, + * configuring actions and the API through which actions are invoked. + *

+ * + * @since OAK 1.10 + */ +public interface UserAction extends AuthorizableAction { + + /** + * Allows to add application specific behavior associated with disabling (or + * re-enabling) an user. + * + * @param user The user to be disabled or re-enabled. + * @param disableReason The reason passed to {@link User#disable(String)} or {@code null} if the user is to be enabled again. + * @param root The root associated with the user manager. + * @param namePathMapper The mapper associated with the user manager. + * @throws RepositoryException If an error occurs. + */ + void onDisable(@NotNull User user, @Nullable String disableReason, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException; + + /** + * Allows to add application specific behavior associated with granting a given + * principal the ability to impersonate the user. + * + * @param user The user associated with the given {@link org.apache.jackrabbit.api.security.user.Impersonation#grantImpersonation(Principal)} call. + * @param principal The target principal to be granted impersonation. + * @param root The root associated with the user manager. + * @param namePathMapper The mapper associated with the user manager. + * @throws RepositoryException If an error occurs. + */ + void onGrantImpersonation(@NotNull User user, @NotNull Principal principal, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException; + + /** + * Allows to add application specific behavior associated with revoking a given + * principal the ability to impersonate the user. + * + * @param user The user associated with the given {@link org.apache.jackrabbit.api.security.user.Impersonation#revokeImpersonation(Principal)} call. + * @param principal The target principal for which impersonation is revoked. + * @param root The root associated with the user manager. + * @param namePathMapper The mapper associated with the user manager. + * @throws RepositoryException If an error occurs. + */ + void onRevokeImpersonation(@NotNull User user, @NotNull Principal principal, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException; +} \ No newline at end of file Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/action/UserActionTest.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/action/UserActionTest.java (revision ) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/action/UserActionTest.java (revision ) @@ -0,0 +1,174 @@ +/* + * 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.action; + +import java.security.Principal; +import java.util.List; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFactory; + +import com.google.common.collect.ImmutableList; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.oak.AbstractSecurityTest; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.SecurityProvider; +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.action.AbstractAuthorizableAction; +import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableAction; +import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableActionProvider; +import org.apache.jackrabbit.oak.spi.security.user.action.UserAction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class UserActionTest extends AbstractSecurityTest { + + private CountingUserAction cntAction = new CountingUserAction(); + private ClearProfileAction clearProfileAction = new ClearProfileAction(); + + private final AuthorizableActionProvider actionProvider = new AuthorizableActionProvider() { + @Override + public @NotNull List getAuthorizableActions(@NotNull SecurityProvider securityProvider) { + return ImmutableList.of(cntAction, clearProfileAction); + } + }; + + @Override + protected ConfigurationParameters getSecurityConfigParameters() { + ConfigurationParameters userParams = ConfigurationParameters.of(UserConstants.PARAM_AUTHORIZABLE_ACTION_PROVIDER, actionProvider); + return ConfigurationParameters.of(UserConfiguration.NAME, userParams); + } + + @Test + public void testDisableUserCnt() throws Exception { + User user = getTestUser(); + user.disable("disabled"); + + assertEquals(1, cntAction.onDisabledCnt); + assertEquals(0, cntAction.onGrantImpCnt); + assertEquals(0, cntAction.onRevokeImpCnt); + + user.disable(null); + assertEquals(2, cntAction.onDisabledCnt); + assertEquals(0, cntAction.onGrantImpCnt); + assertEquals(0, cntAction.onRevokeImpCnt); + } + + @Test + public void testGrantImpCnt() throws Exception { + User user = getTestUser(); + Principal p2 = getUserManager(root).createUser("tmpUser", null).getPrincipal(); + + user.getImpersonation().grantImpersonation(p2); + + assertEquals(0, cntAction.onDisabledCnt); + assertEquals(1, cntAction.onGrantImpCnt); + assertEquals(0, cntAction.onRevokeImpCnt); + } + + @Test + public void testRevokeImpCnt() throws Exception { + User user = getTestUser(); + Principal p2 = getUserManager(root).createUser("tmpUser", null).getPrincipal(); + + user.getImpersonation().revokeImpersonation(p2); + + assertEquals(0, cntAction.onDisabledCnt); + assertEquals(0, cntAction.onGrantImpCnt); + assertEquals(1, cntAction.onRevokeImpCnt); + } + + @Test + public void testDisableRemovesProfiles() throws Exception { + User user = getTestUser(); + ValueFactory vf = getValueFactory(); + user.setProperty("any", vf.createValue("value")); + user.setProperty("profiles/public/nickname", vf.createValue("amal")); + user.setProperty("profiles/private/age", vf.createValue(14)); + root.commit(); + + user.disable("disabled"); + + assertTrue(user.hasProperty("any")); + assertFalse(user.hasProperty("profiles/public/nickname")); + assertFalse(user.hasProperty("profiles/private/age")); + + Tree t = root.getTree(user.getPath()); + assertTrue(t.hasProperty(UserConstants.REP_DISABLED)); + assertFalse(t.hasChild("profiles")); + + // it's transient: + root.refresh(); + t = root.getTree(user.getPath()); + assertFalse(t.hasProperty(UserConstants.REP_DISABLED)); + assertTrue(t.hasChild("profiles")); + } + + + class CountingUserAction extends AbstractAuthorizableAction implements UserAction { + + int onDisabledCnt = 0; + int onGrantImpCnt = 0; + int onRevokeImpCnt = 0; + + @Override + public void onDisable(@NotNull User user, @Nullable String disableReason, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException { + onDisabledCnt++; + } + + @Override + public void onGrantImpersonation(@NotNull User user, @NotNull Principal principal, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException { + onGrantImpCnt++; + } + + @Override + public void onRevokeImpersonation(@NotNull User user, @NotNull Principal principal, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException { + onRevokeImpCnt++; + } + } + + class ClearProfileAction extends AbstractAuthorizableAction implements UserAction { + + @Override + public void onDisable(@NotNull User user, @Nullable String disableReason, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException { + if (disableReason != null) { + Tree t = root.getTree(user.getPath()); + if (t.exists() && t.hasChild("profiles")) { + t.getChild("profiles").remove(); + } + } + } + + @Override + public void onGrantImpersonation(@NotNull User user, @NotNull Principal principal, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException { + // nothing to do + } + + @Override + public void onRevokeImpersonation(@NotNull User user, @NotNull Principal principal, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException { + // nothing to do + } + } +} \ No newline at end of file Index: oak-doc/src/site/markdown/security/user/useraction.md IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-doc/src/site/markdown/security/user/useraction.md (revision ) +++ oak-doc/src/site/markdown/security/user/useraction.md (revision ) @@ -0,0 +1,87 @@ + + +User Actions +------------ + +### Overview + +Oak 1.10 comes with an extension to the Jackrabbit user management API that allows +to perform additional actions or validations for user specific operations +such as + +- disable (or enable) a user +- allowing a given principal to impersonate the target user +- revoke the ability to impersonate the target user for a given principal + + +### UserAction API + +The following public interface is provided by Oak in the package `org.apache.jackrabbit.oak.spi.security.user.action`: + +- [UserAction] + +The `UserAction` interface extends from `AuthorizableAction` and itself allows to perform validations or write +additional application specific content while executing user specific operations. Therefore these actions are executed as part of the transient +user management modifications. This contrasts to `org.apache.jackrabbit.oak.spi.commit.CommitHook`s +which in turn are only triggered once modifications are persisted. + +Consequently, implementations of the `UserAction` interface are expected +to adhere to this rule and perform transient repository operations or validation. +They must not force changes to be persisted by calling `org.apache.jackrabbit.oak.api.Root.commit()`. + +Any user actions are executed with the editing session and the +target operation will fail if any of the configured actions fails (e.g. due to +insufficient permissions by the editing Oak ContentSession). + + +### Default Implementations + +Oak 1.10 doesn't provide any base implementation for `UserAction`. + + +### XML Import + +During import the user actions are called in the same way as when the corresponding API calls are invoked. + + +### Pluggability + +Refer to [Authorizable Actions | Pluggability ](authorizableaction.html#Pluggability) for details on how to plug +a new user action into the system. + +##### Examples + +###### Example Action + +This example action removes the profile nodes upon disabling the user: + + ClearProfilesAction extends AbstractAuthorizableAction implements UserAction { + + @Override + public void onDisable(@NotNull User user, @Nullable String disableReason, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException { + if (disableReason != null) { + Tree t = root.getTree(user.getPath()); + if (t.exists() && t.hasChild("profiles")) { + t.getChild("profiles").remove(); + } + } + } + } + + +[UserAction]: /oak/docs/apidocs/org/apache/jackrabbit/oak/spi/security/user/action/UserAction.html Index: oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/package-info.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/package-info.java (revision 1846497) +++ oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/action/package-info.java (revision ) @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@Version("1.1.2") +@Version("1.2.0") package org.apache.jackrabbit.oak.spi.security.user.action; import org.osgi.annotation.versioning.Version; Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/ImpersonationImpl.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/ImpersonationImpl.java (revision 1846497) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/ImpersonationImpl.java (revision ) @@ -91,6 +91,7 @@ if (!isValidPrincipal(principal)) { return false; } + String principalName = principal.getName(); // make sure user does not impersonate himself Tree userTree = user.getTree(); @@ -100,6 +101,8 @@ return false; } + user.getUserManager().onImpersonation(user, principal, true); + Set impersonators = getImpersonatorNames(userTree); if (impersonators.add(principalName)) { updateImpersonatorNames(userTree, impersonators); @@ -115,6 +118,8 @@ @Override public boolean revokeImpersonation(@NotNull Principal principal) throws RepositoryException { String pName = principal.getName(); + + user.getUserManager().onImpersonation(user, principal, false); Tree userTree = user.getTree(); Set impersonators = getImpersonatorNames(userTree); Index: oak-doc/src/site/markdown/security/user/authorizableaction.md IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-doc/src/site/markdown/security/user/authorizableaction.md (revision 1846497) +++ oak-doc/src/site/markdown/security/user/authorizableaction.md (revision ) @@ -51,7 +51,8 @@ They must not force changes to be persisted by calling `org.apache.jackrabbit.oak.api.Root.commit()`. See section [Group Actions](groupaction.html) for a related extension to -monitor group specific operations. +monitor group specific operations and [User Actions](useraction.html) for +user specific operations. ### Default Implementations Index: oak-doc/src/site/markdown/security/overview.md IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-doc/src/site/markdown/security/overview.md (revision 1846497) +++ oak-doc/src/site/markdown/security/overview.md (revision ) @@ -86,6 +86,7 @@ * [Group Membership](user/membership.html) * [Authorizable Actions](user/authorizableaction.html) * [Group Actions](user/groupaction.html) + * [User Actions](user/useraction.html) * [Authorizable Node Name Generation](user/authorizablenodename.html) * [Password Expiry and Force Initial Password Change](user/expiry.html) * [Password History](user/history.html)