You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by an...@apache.org on 2021/07/22 15:20:36 UTC

[jackrabbit-oak] branch trunk updated: OAK-9511 : Improvements to security related Delegators in org.apache.jackrabbit.oak.jcr.delegate

This is an automated email from the ASF dual-hosted git repository.

angela pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 36f9c23  OAK-9511 : Improvements to security related Delegators in org.apache.jackrabbit.oak.jcr.delegate
36f9c23 is described below

commit 36f9c2395be765a7317827d4124d593f22bbad26
Author: angela <an...@adobe.com>
AuthorDate: Thu Jul 22 17:20:12 2021 +0200

    OAK-9511 : Improvements to security related Delegators in org.apache.jackrabbit.oak.jcr.delegate
---
 .../delegate/AccessControlManagerDelegator.java    |  28 +--
 .../oak/jcr/delegate/AuthorizableDelegator.java    |  35 ++--
 .../oak/jcr/delegate/GroupDelegator.java           |  30 +--
 .../oak/jcr/delegate/ImpersonationDelegator.java   |  12 +-
 .../JackrabbitAccessControlManagerDelegator.java   |  37 ++--
 .../jcr/delegate/PrincipalManagerDelegator.java    |  22 +-
 .../jcr/delegate/PrivilegeManagerDelegator.java    |  19 +-
 .../oak/jcr/delegate/SessionDelegate.java          |  22 +-
 .../jackrabbit/oak/jcr/delegate/UserDelegator.java |  10 +-
 .../oak/jcr/delegate/UserManagerDelegator.java     |  43 +---
 .../oak/jcr/delegate/WorkspaceDelegate.java        |  25 ++-
 ...elegateTest.java => AbstractDelegatorTest.java} |  66 ++----
 .../jcr/delegate/AuthorizableDelegatorTest.java    | 232 +++++++++++++++++++++
 .../jcr/delegate/ImpersonationDelegatorTest.java   |  89 ++++++++
 ...ackrabbitAccessControlManagerDelegatorTest.java | 205 ++++++++++++++++++
 .../delegate/PrincipalManagerDelegatorTest.java    | 128 ++++++++++++
 .../oak/jcr/delegate/SessionDelegateTest.java      |  76 +------
 .../oak/jcr/delegate/UserManagerDelegatorTest.java | 217 +++++++++++++++++++
 18 files changed, 999 insertions(+), 297 deletions(-)

diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/AccessControlManagerDelegator.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/AccessControlManagerDelegator.java
index c64ef28..80bb337 100644
--- a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/AccessControlManagerDelegator.java
+++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/AccessControlManagerDelegator.java
@@ -38,7 +38,7 @@ public class AccessControlManagerDelegator implements AccessControlManager {
     private final SessionDelegate delegate;
     private final AccessControlManager acManager;
 
-    public AccessControlManagerDelegator(SessionDelegate delegate, AccessControlManager acManager) {
+    public AccessControlManagerDelegator(@NotNull SessionDelegate delegate, @NotNull AccessControlManager acManager) {
         this.acManager = acManager;
         this.delegate = delegate;
     }
@@ -48,7 +48,7 @@ public class AccessControlManagerDelegator implements AccessControlManager {
         return delegate.perform(new SessionOperation<Privilege[]>("getSupportedPrivileges") {
             @NotNull
             @Override
-            public Privilege[] perform() throws RepositoryException {
+            public Privilege @NotNull [] perform() throws RepositoryException {
                 return acManager.getSupportedPrivileges(absPath);
             }
         });
@@ -66,8 +66,7 @@ public class AccessControlManagerDelegator implements AccessControlManager {
     }
 
     @Override
-    public boolean hasPrivileges(final String absPath, final Privilege[] privileges)
-            throws RepositoryException {
+    public boolean hasPrivileges(final String absPath, final Privilege[] privileges) throws RepositoryException {
         return delegate.perform(new SessionOperation<Boolean>("hasPrivileges") {
             @NotNull
             @Override
@@ -80,9 +79,8 @@ public class AccessControlManagerDelegator implements AccessControlManager {
     @Override
     public Privilege[] getPrivileges(final String absPath) throws RepositoryException {
         return delegate.perform(new SessionOperation<Privilege[]>("getPrivileges") {
-            @NotNull
             @Override
-            public Privilege[] perform() throws RepositoryException {
+            public Privilege @NotNull [] perform() throws RepositoryException {
                 return acManager.getPrivileges(absPath);
             }
         });
@@ -91,29 +89,25 @@ public class AccessControlManagerDelegator implements AccessControlManager {
     @Override
     public AccessControlPolicy[] getPolicies(final String absPath) throws RepositoryException {
         return delegate.perform(new SessionOperation<AccessControlPolicy[]>("getPolicies") {
-            @NotNull
             @Override
-            public AccessControlPolicy[] perform() throws RepositoryException {
+            public AccessControlPolicy @NotNull [] perform() throws RepositoryException {
                 return acManager.getPolicies(absPath);
             }
         });
     }
 
     @Override
-    public AccessControlPolicy[] getEffectivePolicies(final String absPath)
-            throws RepositoryException {
+    public AccessControlPolicy[] getEffectivePolicies(final String absPath) throws RepositoryException {
         return delegate.perform(new SessionOperation<AccessControlPolicy[]>("getEffectivePolicies") {
-            @NotNull
             @Override
-            public AccessControlPolicy[] perform() throws RepositoryException {
+            public AccessControlPolicy @NotNull [] perform() throws RepositoryException {
                 return acManager.getEffectivePolicies(absPath);
             }
         });
     }
 
     @Override
-    public AccessControlPolicyIterator getApplicablePolicies(final String absPath)
-            throws RepositoryException {
+    public AccessControlPolicyIterator getApplicablePolicies(final String absPath) throws RepositoryException {
         return delegate.perform(new SessionOperation<AccessControlPolicyIterator>("getApplicablePolicies") {
             @NotNull
             @Override
@@ -124,8 +118,7 @@ public class AccessControlManagerDelegator implements AccessControlManager {
     }
 
     @Override
-    public void setPolicy(final String absPath, final AccessControlPolicy policy)
-            throws RepositoryException {
+    public void setPolicy(final String absPath, final AccessControlPolicy policy) throws RepositoryException {
         delegate.performVoid(new SessionOperation<Void>("setPolicy", true) {
             @Override
             public void performVoid() throws RepositoryException {
@@ -135,8 +128,7 @@ public class AccessControlManagerDelegator implements AccessControlManager {
     }
 
     @Override
-    public void removePolicy(final String absPath, final AccessControlPolicy policy)
-            throws RepositoryException {
+    public void removePolicy(final String absPath, final AccessControlPolicy policy) throws RepositoryException {
         delegate.performVoid(new SessionOperation<Void>("removePolicy", true) {
             @Override
             public void performVoid() throws RepositoryException {
diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/AuthorizableDelegator.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/AuthorizableDelegator.java
index b54eca5..bf27824 100644
--- a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/AuthorizableDelegator.java
+++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/AuthorizableDelegator.java
@@ -19,15 +19,6 @@
 
 package org.apache.jackrabbit.oak.jcr.delegate;
 
-import static com.google.common.base.Preconditions.checkArgument;
-
-import java.security.Principal;
-import java.util.Iterator;
-
-import javax.jcr.RepositoryException;
-import javax.jcr.Value;
-
-import com.google.common.base.Function;
 import com.google.common.collect.Iterators;
 import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.api.security.user.Group;
@@ -36,6 +27,14 @@ import org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import java.security.Principal;
+import java.util.Iterator;
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
 /**
  * Base class for {@link GroupDelegator} and {@link UserDelegator}.
  */
@@ -50,6 +49,7 @@ abstract class AuthorizableDelegator implements Authorizable {
         this.delegate = delegate;
     }
 
+    @Nullable
     static Authorizable wrap(@NotNull SessionDelegate sessionDelegate, @Nullable Authorizable authorizable) {
         if (authorizable == null) {
             return null;
@@ -61,6 +61,7 @@ abstract class AuthorizableDelegator implements Authorizable {
         }
     }
 
+    @NotNull
     static Authorizable unwrap(@NotNull Authorizable authorizable) {
         if (authorizable.isGroup()) {
             return GroupDelegator.unwrap((Group) authorizable);
@@ -115,13 +116,7 @@ abstract class AuthorizableDelegator implements Authorizable {
             @Override
             public Iterator<Group> perform() throws RepositoryException {
                 Iterator<Group> groups = delegate.declaredMemberOf();
-                return Iterators.transform(groups, new Function<Group, Group>() {
-                    @Nullable
-                    @Override
-                    public Group apply(@Nullable Group group) {
-                        return GroupDelegator.wrap(sessionDelegate, group);
-                    }
-                });
+                return Iterators.transform(Iterators.filter(groups, Objects::nonNull), group -> GroupDelegator.wrap(sessionDelegate, group));
             }
         });
     }
@@ -134,13 +129,7 @@ abstract class AuthorizableDelegator implements Authorizable {
             @Override
             public Iterator<Group> perform() throws RepositoryException {
                 Iterator<Group> groups = delegate.memberOf();
-                return Iterators.transform(groups, new Function<Group, Group>() {
-                    @Nullable
-                    @Override
-                    public Group apply(@Nullable Group group) {
-                        return GroupDelegator.wrap(sessionDelegate, group);
-                    }
-                });
+                return Iterators.transform(Iterators.filter(groups, Objects::nonNull), group -> GroupDelegator.wrap(sessionDelegate, group));
             }
         });
     }
diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/GroupDelegator.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/GroupDelegator.java
index a09380a..b4fb5f7 100644
--- a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/GroupDelegator.java
+++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/GroupDelegator.java
@@ -23,13 +23,11 @@ import java.util.Iterator;
 import java.util.Set;
 import javax.jcr.RepositoryException;
 
-import com.google.common.base.Function;
 import com.google.common.collect.Iterators;
 import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.api.security.user.Group;
 import org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * This implementation of {@code Group} delegates back to a
@@ -39,16 +37,13 @@ import org.jetbrains.annotations.Nullable;
  */
 final class GroupDelegator extends AuthorizableDelegator implements Group {
 
-    private GroupDelegator(SessionDelegate sessionDelegate, Group groupDelegate) {
+    private GroupDelegator(@NotNull SessionDelegate sessionDelegate, @NotNull Group groupDelegate) {
         super(sessionDelegate, groupDelegate);
     }
 
-    static Group wrap(@NotNull SessionDelegate sessionDelegate, Group group) {
-        if (group == null) {
-            return null;
-        } else {
-            return new GroupDelegator(sessionDelegate, group);
-        }
+    @NotNull
+    static Group wrap(@NotNull SessionDelegate sessionDelegate, @NotNull Group group) {
+        return new GroupDelegator(sessionDelegate, group);
     }
 
     @NotNull
@@ -60,6 +55,7 @@ final class GroupDelegator extends AuthorizableDelegator implements Group {
         }
     }
 
+    @NotNull
     private Group getDelegate() {
         return (Group) delegate;
     }
@@ -73,13 +69,7 @@ final class GroupDelegator extends AuthorizableDelegator implements Group {
             @Override
             public Iterator<Authorizable> perform() throws RepositoryException {
                 Iterator<Authorizable> authorizables = getDelegate().getDeclaredMembers();
-                return Iterators.transform(authorizables, new Function<Authorizable, Authorizable>() {
-                    @Nullable
-                    @Override
-                    public Authorizable apply(@Nullable Authorizable authorizable) {
-                        return wrap(sessionDelegate, authorizable);
-                    }
-                });
+                return Iterators.transform(authorizables, authorizable -> wrap(sessionDelegate, authorizable));
             }
         });
     }
@@ -92,13 +82,7 @@ final class GroupDelegator extends AuthorizableDelegator implements Group {
             @Override
             public Iterator<Authorizable> perform() throws RepositoryException {
                 Iterator<Authorizable> authorizables = getDelegate().getMembers();
-                return Iterators.transform(authorizables, new Function<Authorizable, Authorizable>() {
-                    @Nullable
-                    @Override
-                    public Authorizable apply(@Nullable Authorizable authorizable) {
-                        return wrap(sessionDelegate, authorizable);
-                    }
-                });
+                return Iterators.transform(authorizables, authorizable -> wrap(sessionDelegate, authorizable));
             }
         });
     }
diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/ImpersonationDelegator.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/ImpersonationDelegator.java
index 1f1e1c7..da9e279 100644
--- a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/ImpersonationDelegator.java
+++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/ImpersonationDelegator.java
@@ -38,21 +38,19 @@ import org.jetbrains.annotations.NotNull;
  * @see SessionDelegate#perform(SessionOperation)
  */
 final class ImpersonationDelegator implements Impersonation {
+    
     private final SessionDelegate sessionDelegate;
     private final Impersonation impersonationDelegate;
 
-    private ImpersonationDelegator(SessionDelegate sessionDelegate, Impersonation impersonationDelegate) {
+    private ImpersonationDelegator(@NotNull SessionDelegate sessionDelegate, @NotNull Impersonation impersonationDelegate) {
         checkArgument(!(impersonationDelegate instanceof ImpersonationDelegator));
         this.sessionDelegate = sessionDelegate;
         this.impersonationDelegate = impersonationDelegate;
     }
 
-    static Impersonation wrap(SessionDelegate sessionDelegate, Impersonation impersonation) {
-        if (impersonation == null) {
-            return null;
-        } else {
-            return new ImpersonationDelegator(sessionDelegate, impersonation);
-        }
+    @NotNull
+    static Impersonation wrap(@NotNull SessionDelegate sessionDelegate, @NotNull Impersonation impersonation) {
+        return new ImpersonationDelegator(sessionDelegate, impersonation);
     }
 
     @NotNull
diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/JackrabbitAccessControlManagerDelegator.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/JackrabbitAccessControlManagerDelegator.java
index f842c5e..2ab4abd 100644
--- a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/JackrabbitAccessControlManagerDelegator.java
+++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/JackrabbitAccessControlManagerDelegator.java
@@ -41,12 +41,12 @@ import org.jetbrains.annotations.Nullable;
  * @see SessionDelegate#perform(SessionOperation)
  */
 public class JackrabbitAccessControlManagerDelegator implements JackrabbitAccessControlManager {
+    
     private final JackrabbitAccessControlManager jackrabbitACManager;
     private final SessionDelegate delegate;
     private final AccessControlManagerDelegator jcrACManager;
 
-    public JackrabbitAccessControlManagerDelegator(SessionDelegate delegate,
-            JackrabbitAccessControlManager acManager) {
+    public JackrabbitAccessControlManagerDelegator(@NotNull SessionDelegate delegate, @NotNull JackrabbitAccessControlManager acManager) {
         this.jackrabbitACManager = acManager;
         this.delegate = delegate;
         this.jcrACManager = new AccessControlManagerDelegator(delegate, acManager);
@@ -54,12 +54,10 @@ public class JackrabbitAccessControlManagerDelegator implements JackrabbitAccess
 
     @NotNull
     @Override
-    public JackrabbitAccessControlPolicy[] getApplicablePolicies(@NotNull final Principal principal)
-            throws RepositoryException {
+    public JackrabbitAccessControlPolicy[] getApplicablePolicies(@NotNull final Principal principal) throws RepositoryException {
         return delegate.perform(new SessionOperation<JackrabbitAccessControlPolicy[]>("getApplicablePolicies") {
-            @NotNull
             @Override
-            public JackrabbitAccessControlPolicy[] perform() throws RepositoryException {
+            public JackrabbitAccessControlPolicy @NotNull [] perform() throws RepositoryException {
                 return jackrabbitACManager.getApplicablePolicies(principal);
             }
         });
@@ -67,12 +65,10 @@ public class JackrabbitAccessControlManagerDelegator implements JackrabbitAccess
 
     @NotNull
     @Override
-    public JackrabbitAccessControlPolicy[] getPolicies(@NotNull final Principal principal)
-            throws RepositoryException {
+    public JackrabbitAccessControlPolicy[] getPolicies(@NotNull final Principal principal) throws RepositoryException {
         return delegate.perform(new SessionOperation<JackrabbitAccessControlPolicy[]>("getPolicies") {
-            @NotNull
             @Override
-            public JackrabbitAccessControlPolicy[] perform() throws RepositoryException {
+            public JackrabbitAccessControlPolicy @NotNull [] perform() throws RepositoryException {
                 return jackrabbitACManager.getPolicies(principal);
             }
         });
@@ -80,12 +76,10 @@ public class JackrabbitAccessControlManagerDelegator implements JackrabbitAccess
 
     @NotNull
     @Override
-    public AccessControlPolicy[] getEffectivePolicies(@NotNull final Set<Principal> principals)
-            throws RepositoryException {
+    public AccessControlPolicy[] getEffectivePolicies(@NotNull final Set<Principal> principals) throws RepositoryException {
         return delegate.perform(new SessionOperation<AccessControlPolicy[]>("getEffectivePolicies") {
-            @NotNull
             @Override
-            public AccessControlPolicy[] perform() throws RepositoryException {
+            public AccessControlPolicy @NotNull [] perform() throws RepositoryException {
                 return jackrabbitACManager.getEffectivePolicies(principals);
             }
         });
@@ -105,12 +99,10 @@ public class JackrabbitAccessControlManagerDelegator implements JackrabbitAccess
 
     @NotNull
     @Override
-    public Privilege[] getPrivileges(@Nullable final String absPath, @NotNull final Set<Principal> principals)
-            throws RepositoryException {
+    public Privilege[] getPrivileges(@Nullable final String absPath, @NotNull final Set<Principal> principals) throws RepositoryException {
         return delegate.perform(new SessionOperation<Privilege[]>("getPrivileges") {
-            @NotNull
             @Override
-            public Privilege[] perform() throws RepositoryException {
+            public Privilege @NotNull [] perform() throws RepositoryException {
                 return jackrabbitACManager.getPrivileges(absPath, principals);
             }
         });
@@ -149,8 +141,7 @@ public class JackrabbitAccessControlManagerDelegator implements JackrabbitAccess
     }
 
     @Override
-    public boolean hasPrivileges(String absPath, Privilege[] privileges)
-            throws RepositoryException {
+    public boolean hasPrivileges(String absPath, Privilege[] privileges) throws RepositoryException {
         return jcrACManager.hasPrivileges(absPath, privileges);
     }
 
@@ -170,8 +161,7 @@ public class JackrabbitAccessControlManagerDelegator implements JackrabbitAccess
     }
 
     @Override
-    public AccessControlPolicyIterator getApplicablePolicies(String absPath)
-            throws RepositoryException {
+    public AccessControlPolicyIterator getApplicablePolicies(String absPath) throws RepositoryException {
         return jcrACManager.getApplicablePolicies(absPath);
     }
 
@@ -181,8 +171,7 @@ public class JackrabbitAccessControlManagerDelegator implements JackrabbitAccess
     }
 
     @Override
-    public void removePolicy(String absPath, AccessControlPolicy policy)
-            throws RepositoryException {
+    public void removePolicy(String absPath, AccessControlPolicy policy) throws RepositoryException {
         jcrACManager.removePolicy(absPath, policy);
     }
 }
diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/PrincipalManagerDelegator.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/PrincipalManagerDelegator.java
index 22deb7b..114546b 100644
--- a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/PrincipalManagerDelegator.java
+++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/PrincipalManagerDelegator.java
@@ -21,8 +21,6 @@ package org.apache.jackrabbit.oak.jcr.delegate;
 
 import java.security.Principal;
 
-import javax.jcr.RepositoryException;
-
 import org.apache.jackrabbit.api.security.principal.PrincipalIterator;
 import org.apache.jackrabbit.api.security.principal.PrincipalManager;
 import org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation;
@@ -37,11 +35,11 @@ import org.jetbrains.annotations.Nullable;
  * @see SessionDelegate#perform(SessionOperation)
  */
 public class PrincipalManagerDelegator implements PrincipalManager, PrincipalQueryManager {
+    
     private final SessionDelegate delegate;
     private final PrincipalManager principalManager;
 
-    public PrincipalManagerDelegator(SessionDelegate delegate,
-            PrincipalManager principalManager) {
+    public PrincipalManagerDelegator(@NotNull SessionDelegate delegate, @NotNull PrincipalManager principalManager) {
         this.principalManager = principalManager;
         this.delegate = delegate;
     }
@@ -60,16 +58,12 @@ public class PrincipalManagerDelegator implements PrincipalManager, PrincipalQue
     @Nullable
     @Override
     public Principal getPrincipal(@NotNull final String principalName) {
-        try {
-            return delegate.performNullable(new SessionOperation<Principal>("getPrincipal") {
-                @Override
-                public Principal performNullable() {
-                    return principalManager.getPrincipal(principalName);
-                }
-            });
-        } catch (RepositoryException e) {
-            throw new RuntimeException("Unexpected exception thrown by operation 'getPrincipal'", e);
-        }
+        return delegate.safePerformNullable(new SessionOperation<Principal>("getPrincipal") {
+            @Override
+            public Principal performNullable() {
+                return principalManager.getPrincipal(principalName);
+            }
+        });
     }
 
     @NotNull
diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/PrivilegeManagerDelegator.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/PrivilegeManagerDelegator.java
index fb1af7b..dd50bc6 100644
--- a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/PrivilegeManagerDelegator.java
+++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/PrivilegeManagerDelegator.java
@@ -19,17 +19,14 @@
 
 package org.apache.jackrabbit.oak.jcr.delegate;
 
-import javax.jcr.AccessDeniedException;
-import javax.jcr.NamespaceException;
-import javax.jcr.RepositoryException;
-import javax.jcr.security.AccessControlException;
-import javax.jcr.security.Privilege;
-
 import org.apache.jackrabbit.api.security.authorization.PrivilegeManager;
 import org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import javax.jcr.RepositoryException;
+import javax.jcr.security.Privilege;
+
 /**
  * This implementation of {@code PrivilegeManager} delegates back to a
  * delegatee wrapping each call into a {@link SessionOperation} closure.
@@ -37,10 +34,11 @@ import org.jetbrains.annotations.Nullable;
  * @see SessionDelegate#perform(SessionOperation)
  */
 public class PrivilegeManagerDelegator implements PrivilegeManager {
+    
     private final PrivilegeManager pm;
     private final SessionDelegate delegate;
 
-    public PrivilegeManagerDelegator(SessionDelegate delegate, PrivilegeManager pm) {
+    public PrivilegeManagerDelegator(@NotNull SessionDelegate delegate, @NotNull PrivilegeManager pm) {
         this.pm = pm;
         this.delegate = delegate;
     }
@@ -49,9 +47,8 @@ public class PrivilegeManagerDelegator implements PrivilegeManager {
     @Override
     public Privilege[] getRegisteredPrivileges() throws RepositoryException {
         return delegate.perform(new SessionOperation<Privilege[]>("getRegisteredPrivileges") {
-            @NotNull
             @Override
-            public Privilege[] perform() throws RepositoryException {
+            public Privilege @NotNull [] perform() throws RepositoryException {
                 return pm.getRegisteredPrivileges();
             }
         });
@@ -59,7 +56,7 @@ public class PrivilegeManagerDelegator implements PrivilegeManager {
 
     @NotNull
     @Override
-    public Privilege getPrivilege(@NotNull final String privilegeName) throws AccessControlException, RepositoryException {
+    public Privilege getPrivilege(@NotNull final String privilegeName) throws RepositoryException {
         return delegate.perform(new SessionOperation<Privilege>("getPrivilege") {
             @NotNull
             @Override
@@ -71,7 +68,7 @@ public class PrivilegeManagerDelegator implements PrivilegeManager {
 
     @NotNull
     @Override
-    public Privilege registerPrivilege(@NotNull final String privilegeName, final boolean isAbstract, @Nullable final String[] declaredAggregateNames) throws AccessDeniedException, NamespaceException, RepositoryException {
+    public Privilege registerPrivilege(@NotNull final String privilegeName, final boolean isAbstract, @Nullable final String[] declaredAggregateNames) throws RepositoryException {
         return delegate.perform(new SessionOperation<Privilege>("registerPrivilege", true) {
             @NotNull
             @Override
diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java
index cfab8f4..ab8013f 100644
--- a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java
+++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java
@@ -179,7 +179,7 @@ public class SessionDelegate {
      * @return  synchronized iterator
      */
     public <T> Iterator<T> sync(Iterator<T> iterator) {
-        return new SynchronizedIterator<T>(iterator, lock);
+        return new SynchronizedIterator<>(iterator, lock);
     }
 
     /**
@@ -293,6 +293,7 @@ public class SessionDelegate {
      * @return  the result of {@code sessionOperation.perform()}
      * @see #getRoot()
      */
+    @NotNull
     public <T> T safePerform(SessionOperation<T> sessionOperation) {
         try {
             return perform(sessionOperation);
@@ -301,6 +302,25 @@ public class SessionDelegate {
         }
     }
 
+    /**
+     * Same as {@link #performNullable(SessionOperation)} unless this method expects
+     * {@link SessionOperation#performNullable} <em>not</em> to throw a {@code RepositoryException}.
+     * Such exceptions will be wrapped into a {@code RuntimeException} and rethrown as they
+     * are considered an internal error.
+     *
+     * @param sessionOperation  the {@code SessionOperation} to perform
+     * @param <T>  return type of {@code sessionOperation}
+     * @return  the result of {@code sessionOperation.performNullable()}
+     */
+    @Nullable
+    public <T> T safePerformNullable(SessionOperation<T> sessionOperation) {
+        try {
+            return performNullable(sessionOperation);
+        } catch (RepositoryException e) {
+            throw new RuntimeException("Unexpected exception thrown by operation " + sessionOperation, e);
+        }
+    }
+
     @NotNull
     public ContentSession getContentSession() {
         return contentSession;
diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/UserDelegator.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/UserDelegator.java
index 1e2043c..982f446 100644
--- a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/UserDelegator.java
+++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/UserDelegator.java
@@ -40,12 +40,9 @@ final class UserDelegator extends AuthorizableDelegator implements User {
         super(sessionDelegate, userDelegate);
     }
 
-    static User wrap(SessionDelegate sessionDelegate, User user) {
-        if (user == null) {
-            return null;
-        } else {
-            return new UserDelegator(sessionDelegate, user);
-        }
+    @NotNull
+    static User wrap(@NotNull SessionDelegate sessionDelegate, @NotNull User user) {
+        return new UserDelegator(sessionDelegate, user);
     }
 
     @NotNull
@@ -57,6 +54,7 @@ final class UserDelegator extends AuthorizableDelegator implements User {
         }
     }
 
+    @NotNull
     private User getDelegate() {
         return (User) delegate;
     }
diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/UserManagerDelegator.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/UserManagerDelegator.java
index eaa5853..a21b6b5 100644
--- a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/UserManagerDelegator.java
+++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/UserManagerDelegator.java
@@ -19,17 +19,8 @@
 
 package org.apache.jackrabbit.oak.jcr.delegate;
 
-import static com.google.common.base.Preconditions.checkState;
-
-import java.security.Principal;
-import java.util.Iterator;
-
-import javax.jcr.RepositoryException;
-
-import com.google.common.base.Function;
 import com.google.common.collect.Iterators;
 import org.apache.jackrabbit.api.security.user.Authorizable;
-import org.apache.jackrabbit.api.security.user.AuthorizableExistsException;
 import org.apache.jackrabbit.api.security.user.Group;
 import org.apache.jackrabbit.api.security.user.Query;
 import org.apache.jackrabbit.api.security.user.User;
@@ -39,6 +30,12 @@ import org.apache.jackrabbit.oak.spi.security.user.util.UserUtil;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import javax.jcr.RepositoryException;
+import java.security.Principal;
+import java.util.Iterator;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
 /**
  * This implementation of {@code UserManager} delegates back to a
  * delegatee wrapping each call into a {@link UserManager} closure.
@@ -51,7 +48,7 @@ public class UserManagerDelegator implements UserManager {
     private final UserManager userManagerDelegate;
 
     public UserManagerDelegator(final SessionDelegate sessionDelegate, UserManager userManagerDelegate) {
-        checkState(!(userManagerDelegate instanceof UserManagerDelegator));
+        checkArgument(!(userManagerDelegate instanceof UserManagerDelegator));
         this.sessionDelegate = sessionDelegate;
         this.userManagerDelegate = userManagerDelegate;
     }
@@ -114,13 +111,7 @@ public class UserManagerDelegator implements UserManager {
             @Override
             public Iterator<Authorizable> perform() throws RepositoryException {
                 Iterator<Authorizable> authorizables = userManagerDelegate.findAuthorizables(relPath, value);
-                return Iterators.transform(authorizables, new Function<Authorizable, Authorizable>() {
-                    @Nullable
-                    @Override
-                    public Authorizable apply(Authorizable authorizable) {
-                        return AuthorizableDelegator.wrap(sessionDelegate, authorizable);
-                    }
-                });
+                return Iterators.transform(authorizables, authorizable -> AuthorizableDelegator.wrap(sessionDelegate, authorizable));
             }
         });
     }
@@ -133,13 +124,7 @@ public class UserManagerDelegator implements UserManager {
             @Override
             public Iterator<Authorizable> perform() throws RepositoryException {
                 Iterator<Authorizable> authorizables = userManagerDelegate.findAuthorizables(relPath, value, searchType);
-                return Iterators.transform(authorizables, new Function<Authorizable, Authorizable>() {
-                    @Nullable
-                    @Override
-                    public Authorizable apply(Authorizable authorizable) {
-                        return AuthorizableDelegator.wrap(sessionDelegate, authorizable);
-                    }
-                });
+                return Iterators.transform(authorizables, authorizable -> AuthorizableDelegator.wrap(sessionDelegate, authorizable));
             }
         });
     }
@@ -152,20 +137,14 @@ public class UserManagerDelegator implements UserManager {
             @Override
             public Iterator<Authorizable> perform() throws RepositoryException {
                 Iterator<Authorizable> authorizables = userManagerDelegate.findAuthorizables(query);
-                return Iterators.transform(authorizables, new Function<Authorizable, Authorizable>() {
-                    @Nullable
-                    @Override
-                    public Authorizable apply(Authorizable authorizable) {
-                        return AuthorizableDelegator.wrap(sessionDelegate, authorizable);
-                    }
-                });
+                return Iterators.transform(authorizables, authorizable -> AuthorizableDelegator.wrap(sessionDelegate, authorizable));
             }
         });
     }
 
     @NotNull
     @Override
-    public User createUser(@NotNull final String userID, @Nullable final String password) throws AuthorizableExistsException, RepositoryException {
+    public User createUser(@NotNull final String userID, @Nullable final String password) throws RepositoryException {
         return sessionDelegate.perform(new UserManagerOperation<User>(sessionDelegate, "createUser", true) {
             @NotNull
             @Override
diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/WorkspaceDelegate.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/WorkspaceDelegate.java
index a2a5704..8353e4a 100644
--- a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/WorkspaceDelegate.java
+++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/WorkspaceDelegate.java
@@ -43,6 +43,7 @@ import org.apache.jackrabbit.oak.spi.security.authorization.permission.Permissio
 import org.apache.jackrabbit.oak.spi.version.VersionConstants;
 import org.apache.jackrabbit.util.Text;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES;
@@ -67,7 +68,7 @@ public class WorkspaceDelegate {
      * @param destPath  oak path to the destination
      * @throws RepositoryException
      */
-    public void copy(String srcPath, String destPath) throws RepositoryException {
+    public void copy(@NotNull String srcPath, @NotNull String destPath) throws RepositoryException {
         SessionDelegate sessionDelegate = context.getSessionDelegate();
         AccessManager accessManager = context.getAccessManager();
         Root root = sessionDelegate.getContentSession().getLatestRoot();
@@ -114,29 +115,28 @@ public class WorkspaceDelegate {
             this.destName = destName;
         }
 
-        public void perform(@NotNull Root root, @NotNull String userId) throws RepositoryException {
+        public void perform(@NotNull Root root, @Nullable String userId) throws RepositoryException {
             try {
                 Tree typeRoot = root.getTree(NODE_TYPES_PATH);
                 copy(source, destParent, destName, typeRoot, userId);
                 updateReferences(source, destParent.getChild(destName));
 
-                Map<String, String> copyInfo = new HashMap<String, String>();
+                Map<String, String> copyInfo = new HashMap<>();
                 copyInfo.put("copy-source", source.getPath());
                 if (TreeUtil.isNodeType(source, JcrConstants.MIX_VERSIONABLE, typeRoot)) {
                     String sourceBaseVersionId = source.getProperty(JcrConstants.JCR_BASEVERSION).getValue(Type.STRING);
                     copyInfo.put(VersionConstants.JCR_COPIED_FROM, sourceBaseVersionId);
                 }
-                root.commit(ImmutableMap.<String, Object>copyOf(copyInfo));
+                root.commit(ImmutableMap.copyOf(copyInfo));
             } catch (CommitFailedException e) {
                 throw e.asRepositoryException();
             }
         }
 
-        private void copy(Tree source, Tree destParent, String destName, Tree typeRoot, String userId)
+        private void copy(@NotNull Tree source, @NotNull Tree destParent, @NotNull String destName, @NotNull Tree typeRoot, @Nullable String userId)
                 throws RepositoryException {
             String primaryType = TreeUtil.getPrimaryTypeName(source);
-            Tree dest = TreeUtil.addChild(
-                    destParent, destName, primaryType, typeRoot, userId);
+            Tree dest = TreeUtil.addChild(destParent, destName, primaryType, typeRoot, userId);
             for (PropertyState property : source.getProperties()) {
                 String propName = property.getName();
                 if (JCR_MIXINTYPES.equals(propName)) {
@@ -168,8 +168,7 @@ public class WorkspaceDelegate {
          * @param src  the source tree of the copy operation.
          * @param dest the unprocessed copy of the tree.
          */
-        private void updateReferences(Tree src, Tree dest)
-                throws RepositoryException {
+        private void updateReferences(@NotNull Tree src, @NotNull Tree dest) {
             for (PropertyState prop : src.getProperties()) {
                 if (isReferenceType(prop) && !VersionConstants.VERSION_PROPERTY_NAMES.contains(prop.getName())) {
                     updateProperty(prop, dest);
@@ -180,7 +179,7 @@ public class WorkspaceDelegate {
             }
         }
 
-        private boolean isReferenceType(PropertyState property) {
+        private static boolean isReferenceType(@NotNull PropertyState property) {
             Type<?> type = property.getType();
             return (type == Type.REFERENCE
                     || type == Type.REFERENCES
@@ -188,11 +187,11 @@ public class WorkspaceDelegate {
                     || type == Type.WEAKREFERENCES);
         }
 
-        private void updateProperty(PropertyState prop, Tree dest) {
+        private void updateProperty(@NotNull PropertyState prop, @NotNull Tree dest) {
             boolean multi = prop.isArray();
             boolean weak = prop.getType() == Type.WEAKREFERENCE
                     || prop.getType() == Type.WEAKREFERENCES;
-            List<String> ids = new ArrayList<String>();
+            List<String> ids = new ArrayList<>();
             for (int i = 0; i < prop.count(); i++) {
                 String id;
                 if (weak) {
@@ -221,7 +220,7 @@ public class WorkspaceDelegate {
             dest.setProperty(p);
         }
 
-        private void translateId(String id, List<String> ids) {
+        private void translateId(@NotNull String id, @NotNull List<String> ids) {
             String newId = translated.get(id);
             if (newId != null) {
                 ids.add(newId);
diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegateTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/AbstractDelegatorTest.java
similarity index 69%
copy from oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegateTest.java
copy to oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/AbstractDelegatorTest.java
index 9fc7ed8..68a35e9 100644
--- a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegateTest.java
+++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/AbstractDelegatorTest.java
@@ -31,9 +31,10 @@ import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
 import org.apache.jackrabbit.oak.stats.Clock;
 import org.apache.jackrabbit.oak.stats.StatisticManager;
 import org.jetbrains.annotations.NotNull;
-import org.junit.Test;
 import org.mockito.MockSettings;
 import org.mockito.stubbing.Answer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.Collections;
 import java.util.concurrent.Executors;
@@ -42,25 +43,31 @@ import java.util.concurrent.ScheduledExecutorService;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.withSettings;
 
-public class SessionDelegateTest {
+public abstract class AbstractDelegatorTest {
 
+    private static final Logger log = LoggerFactory.getLogger(AbstractDelegatorTest.class);
+
+    @NotNull
+    static SessionDelegate mockSessionDelegate() {
+        PermissionProvider pp = mock(PermissionProvider.class);
+        return mockSessionDelegate(mockRoot(pp, true), pp);
+    }
+    
     @NotNull
-    private static SessionDelegate mockSessionDelegate(@NotNull Root root, @NotNull PermissionProvider pp) {
+    static SessionDelegate mockSessionDelegate(@NotNull Root root, @NotNull PermissionProvider pp) {
         ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
         Whiteboard wb = new DefaultWhiteboard();
         StatisticManager statisticManager = new StatisticManager(wb, executorService);
-        return new SessionDelegate(mockContentSession(root), mockSecurityProvider(root, pp),
-                RefreshStrategy.Composite.create(), new ThreadLocal<>(), statisticManager, new Clock.Virtual());
+        return spy(new SessionDelegate(mockContentSession(root), mockSecurityProvider(root, pp),
+                RefreshStrategy.Composite.create(), new ThreadLocal<>(), statisticManager, new Clock.Virtual()));
     }
 
     @NotNull
-    private static Root mockRoot(@NotNull PermissionProvider pp, boolean permissionAware) {
+    static Root mockRoot(@NotNull PermissionProvider pp, boolean permissionAware) {
         MockSettings s = withSettings();
         if (permissionAware) {
             s.extraInterfaces(PermissionAware.class);
@@ -101,45 +108,4 @@ public class SessionDelegateTest {
         when(root.getContentSession()).thenReturn(cs);
         return cs;
     }
-
-    @Test
-    public void testRefreshAware() {
-        PermissionProvider pp = mock(PermissionProvider.class);
-        Root r = mockRoot(pp, true);
-        SessionDelegate delegate = mockSessionDelegate(r, pp);
-
-        PermissionAware pa = (PermissionAware) r;
-        verify(pa, never()).getPermissionProvider();
-
-        // calling refresh without permissionprovider field being assigned
-        delegate.refresh(true);
-        delegate.refresh(false);
-        verify(pp, times(2)).refresh();
-
-        // calling refresh with permissionprovider field being assigned
-        delegate.getPermissionProvider();
-        delegate.refresh(true);
-        delegate.refresh(false);
-        verify(pp, times(4)).refresh();
-
-        verify(pa, times(1)).getPermissionProvider();
-    }
-
-    @Test
-    public void testRefreshUnaware() {
-        PermissionProvider pp = mock(PermissionProvider.class);
-        Root r = mockRoot(pp, false);
-        SessionDelegate delegate = mockSessionDelegate(r, pp);
-
-        // calling refresh without permissionprovider field being assigned
-        delegate.refresh(true);
-        delegate.refresh(false);
-        verify(pp, times(2)).refresh();
-
-        // calling refresh with permissionprovider field being assigned
-        delegate.getPermissionProvider();
-        delegate.refresh(true);
-        delegate.refresh(false);
-        verify(pp, times(6)).refresh();
-    }
 }
\ No newline at end of file
diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/AuthorizableDelegatorTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/AuthorizableDelegatorTest.java
new file mode 100644
index 0000000..4bdb4f5
--- /dev/null
+++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/AuthorizableDelegatorTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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.jcr.delegate;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import javax.jcr.Value;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+public class AuthorizableDelegatorTest extends AbstractDelegatorTest {
+    
+    private final SessionDelegate sessionDelegate = mockSessionDelegate();
+    private final Authorizable authorizable = mock(Authorizable.class);
+    private final Authorizable user = when(mock(Authorizable.class, withSettings().extraInterfaces(User.class)).isGroup()).thenReturn(false).getMock();
+    private final Authorizable group = when(mock(Authorizable.class, withSettings().extraInterfaces(Group.class)).isGroup()).thenReturn(true).getMock();
+    private final AuthorizableDelegator ad = mockDelegator(sessionDelegate, authorizable);
+
+    private static AuthorizableDelegator mockDelegator(@NotNull SessionDelegate sd, @NotNull Authorizable a) {
+        return mock(AuthorizableDelegator.class, withSettings().useConstructor(sd, a).defaultAnswer(CALLS_REAL_METHODS));
+    }
+    
+    @Test(expected = IllegalArgumentException.class)
+    public void testCreateFromDelegator() {
+        new AuthorizableDelegator(sessionDelegate, ad) {};
+    }
+    
+    @Test
+    public void testWrap() {
+        assertNull(AuthorizableDelegator.wrap(sessionDelegate, null));
+        
+        Authorizable a = AuthorizableDelegator.wrap(sessionDelegate, group);
+        assertTrue(a instanceof GroupDelegator);
+
+        a = AuthorizableDelegator.wrap(sessionDelegate, user);
+        assertTrue(a instanceof UserDelegator);
+        
+        verify(user).isGroup();
+        verify(group).isGroup();
+        verifyNoMoreInteractions(user, group);
+        verifyNoInteractions(sessionDelegate);
+    }
+
+    @Test
+    public void testUnwrap() {
+        Authorizable a = AuthorizableDelegator.unwrap(group);
+        assertSame(group, a);
+
+        a = AuthorizableDelegator.unwrap(user);
+        assertSame(user, a);
+
+        verify(user).isGroup();
+        verify(group).isGroup();
+        verifyNoMoreInteractions(user, group, sessionDelegate);
+    }
+
+    @Test
+    public void testUnwrapDelegator() throws Exception {
+        Authorizable gDelegator = AuthorizableDelegator.wrap(sessionDelegate, group);
+        Authorizable a = AuthorizableDelegator.unwrap(gDelegator);
+        assertSame(group, a);
+
+        Authorizable uDelegator = AuthorizableDelegator.wrap(sessionDelegate, user);
+        a = AuthorizableDelegator.unwrap(uDelegator);
+        assertSame(user, a);
+
+        verify(user, times(2)).isGroup();
+        verify(group, times(2)).isGroup();
+        verify(sessionDelegate, times(2)).safePerform(any(SessionOperation.class));
+        verify(sessionDelegate, times(2)).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(user, group, sessionDelegate);
+    }
+    
+    @Test
+    public void testIsGroup() throws Exception {
+        ad.isGroup();
+        
+        verify(authorizable).isGroup();
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verify(sessionDelegate).safePerform(any(SessionOperation.class));
+        verifyNoMoreInteractions(authorizable, sessionDelegate);
+    }
+
+    @Test
+    public void testGetId() throws Exception {
+        ad.getID();
+
+        verify(authorizable).getID();
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(authorizable, sessionDelegate);
+    }
+
+    @Test
+    public void testGetPrincipal() throws Exception {
+        ad.getPrincipal();
+
+        verify(authorizable).getPrincipal();
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(authorizable, sessionDelegate);
+    }
+    
+    @Test
+    public void testMemberOf() throws Exception {
+        doReturn(Collections.emptyIterator()).when(authorizable).memberOf();
+        ad.memberOf();
+
+        verify(authorizable).memberOf();
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(authorizable, sessionDelegate);
+    }
+
+    @Test
+    public void testDeclaredMemberOf() throws Exception {
+        doReturn(Collections.emptyIterator()).when(authorizable).declaredMemberOf();
+        ad.declaredMemberOf();
+
+        verify(authorizable).declaredMemberOf();
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(authorizable, sessionDelegate);
+    }
+
+    @Test
+    public void testRemove() throws Exception {
+        ad.remove();
+
+        verify(authorizable).remove();
+        verify(sessionDelegate).performVoid(any(SessionOperation.class));
+        verifyNoMoreInteractions(authorizable, sessionDelegate);
+    }
+    
+    @Test
+    public void testGetPropertyNames() throws Exception {
+        ad.getPropertyNames();
+        ad.getPropertyNames("rel/path");
+        
+        verify(authorizable).getPropertyNames();
+        verify(authorizable).getPropertyNames("rel/path");
+        verify(sessionDelegate, times(2)).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(authorizable, sessionDelegate);
+    }
+    
+    @Test
+    public void testHasProperty() throws Exception {
+        ad.hasProperty("rel/path");
+        verify(authorizable).hasProperty("rel/path");
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(authorizable, sessionDelegate);
+    }
+
+    @Test
+    public void testGetProperty() throws Exception {
+        ad.getProperty("rel/path");
+        verify(authorizable).getProperty("rel/path");
+        verify(sessionDelegate).performNullable(any(SessionOperation.class));
+        verifyNoMoreInteractions(authorizable, sessionDelegate);
+    }
+
+    @Test
+    public void testSetProperty() throws Exception {
+        Value v = mock(Value.class);
+        ad.setProperty("rel/path", v);
+        ad.setProperty("rel/path", new Value[] {v});
+        verify(authorizable).setProperty("rel/path", v);
+        verify(authorizable).setProperty("rel/path", new Value[] {v});
+        verify(sessionDelegate, times(2)).performVoid(any(SessionOperation.class));
+        verifyNoMoreInteractions(authorizable, sessionDelegate);
+    }
+
+    @Test
+    public void testRemoveProperty() throws Exception {
+        ad.removeProperty("rel/path");
+        verify(authorizable).removeProperty("rel/path");
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(authorizable, sessionDelegate);
+    }
+
+    @Test
+    public void testGetPath() throws Exception {
+        ad.getPath();
+
+        verify(authorizable).getPath();
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(authorizable, sessionDelegate);
+    }
+    
+    @Test
+    public void testEquals() {
+        AuthorizableDelegator delegator = (AuthorizableDelegator) AuthorizableDelegator.wrap(sessionDelegate, user);
+        assertTrue(delegator.equals(delegator));
+        assertNotEquals(delegator, user);
+        
+        AuthorizableDelegator uDelegator = (AuthorizableDelegator) AuthorizableDelegator.wrap(sessionDelegate, user);
+        assertEquals(delegator, uDelegator);
+
+        AuthorizableDelegator gDelegator = (AuthorizableDelegator) AuthorizableDelegator.wrap(sessionDelegate, group);
+        assertNotEquals(delegator, gDelegator);
+    }
+}
\ No newline at end of file
diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/ImpersonationDelegatorTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/ImpersonationDelegatorTest.java
new file mode 100644
index 0000000..ded4042
--- /dev/null
+++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/ImpersonationDelegatorTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.jcr.delegate;
+
+import org.apache.jackrabbit.api.security.user.Impersonation;
+import org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import javax.security.auth.Subject;
+import java.security.Principal;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+public class ImpersonationDelegatorTest extends AbstractDelegatorTest {
+    
+    private final SessionDelegate sessionDelegate = mockSessionDelegate();
+    private final Impersonation impersonation = mock(Impersonation.class);
+    private final ImpersonationDelegator delegator = mockDelegator(sessionDelegate, impersonation);
+
+    private static ImpersonationDelegator mockDelegator(@NotNull SessionDelegate sd, @NotNull Impersonation impersonation) {
+        Impersonation i = ImpersonationDelegator.wrap(sd, impersonation);
+        assertTrue(i instanceof ImpersonationDelegator);
+        return spy((ImpersonationDelegator) i);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testWrapDelegator() {
+        ImpersonationDelegator.wrap(sessionDelegate, delegator);
+    }
+    
+    @Test
+    public void testGetImpersonators() throws Exception {
+        delegator.getImpersonators();
+        
+        verify(impersonation).getImpersonators();
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(impersonation, sessionDelegate);
+    }
+    
+    @Test
+    public void testGrantImpersonation() throws Exception {
+        Principal principal = mock(Principal.class);
+        delegator.grantImpersonation(principal);
+
+        verify(impersonation).grantImpersonation(principal);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(impersonation, sessionDelegate);
+    }
+    
+    @Test
+    public void testRevokeImpersonation() throws Exception {
+        Principal principal = mock(Principal.class);
+        delegator.revokeImpersonation(principal);
+
+        verify(impersonation).revokeImpersonation(principal);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(impersonation, sessionDelegate);
+    }
+
+    @Test
+    public void testAllows() throws Exception {
+        Subject subject = new Subject();
+        delegator.allows(subject);
+
+        verify(impersonation).allows(subject);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(impersonation, sessionDelegate);
+    }
+}
\ No newline at end of file
diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/JackrabbitAccessControlManagerDelegatorTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/JackrabbitAccessControlManagerDelegatorTest.java
new file mode 100644
index 0000000..5ee5367
--- /dev/null
+++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/JackrabbitAccessControlManagerDelegatorTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.jcr.delegate;
+
+import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager;
+import org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation;
+import org.junit.Test;
+
+import javax.jcr.security.AccessControlPolicy;
+import javax.jcr.security.Privilege;
+import java.security.Principal;
+import java.util.Collections;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+public class JackrabbitAccessControlManagerDelegatorTest extends AbstractDelegatorTest {
+    
+    private final SessionDelegate sessionDelegate = mockSessionDelegate();
+    private final JackrabbitAccessControlManager jrAcMgr = mock(JackrabbitAccessControlManager.class);
+    private final Principal principal = mock(Principal.class);
+    private final String absPath = "/path";
+    
+    private final JackrabbitAccessControlManagerDelegator delegator = new JackrabbitAccessControlManagerDelegator(sessionDelegate, jrAcMgr);
+    
+    @Test
+    public void testGetApplicablePoliciesByPrincipal() throws Exception {
+        delegator.getApplicablePolicies(principal);
+        
+        verify(jrAcMgr).getApplicablePolicies(principal);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+    @Test
+    public void testGetPoliciesByPrincipal() throws Exception {
+        delegator.getPolicies(principal);
+
+        verify(jrAcMgr).getPolicies(principal);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+    @Test
+    public void testGetEffectivePoliciesByPrincipals() throws Exception {
+        delegator.getEffectivePolicies(Collections.singleton(principal));
+
+        verify(jrAcMgr).getEffectivePolicies(Collections.singleton(principal));
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+    
+    @Test
+    public void testHasPrivilegesByPrincipals() throws Exception {
+        delegator.hasPrivileges(absPath, Collections.singleton(principal), new Privilege[0]);
+
+        verify(jrAcMgr).hasPrivileges(absPath, Collections.singleton(principal), new Privilege[0]);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+    @Test
+    public void testGetPrivilegesByPrincipals() throws Exception {
+        delegator.getPrivileges(absPath, Collections.singleton(principal));
+
+        verify(jrAcMgr).getPrivileges(absPath, Collections.singleton(principal));
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+    @Test
+    public void testGetPrivilegeCollectionByPrincipals() throws Exception {
+        delegator.getPrivilegeCollection(absPath, Collections.singleton(principal));
+
+        verify(jrAcMgr).getPrivilegeCollection(absPath, Collections.singleton(principal));
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+    @Test
+    public void testGetPrivilegeCollection() throws Exception {
+        delegator.getPrivilegeCollection(absPath);
+
+        verify(jrAcMgr).getPrivilegeCollection(absPath);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+    @Test
+    public void testGetSupportedPrivileges() throws Exception {
+        delegator.getSupportedPrivileges(absPath);
+
+        verify(jrAcMgr).getSupportedPrivileges(absPath);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+    @Test
+    public void testPrivilegeFromName() throws Exception {
+        delegator.privilegeFromName("name");
+
+        verify(jrAcMgr).privilegeFromName("name");
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+    @Test
+    public void testHasPrivileges() throws Exception {
+        delegator.hasPrivileges(absPath, new Privilege[0]);
+
+        verify(jrAcMgr).hasPrivileges(absPath, new Privilege[0]);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+    @Test
+    public void testGetPrivileges() throws Exception {
+        delegator.getPrivileges(absPath);
+
+        verify(jrAcMgr).getPrivileges(absPath);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+
+    @Test
+    public void testGetApplicablePolicies() throws Exception {
+        delegator.getApplicablePolicies(absPath);
+
+        verify(jrAcMgr).getApplicablePolicies(absPath);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+    @Test
+    public void testGetPolicies() throws Exception {
+        delegator.getPolicies(absPath);
+
+        verify(jrAcMgr).getPolicies(absPath);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+    @Test
+    public void testGetEffectivePolicies() throws Exception {
+        delegator.getEffectivePolicies(absPath);
+
+        verify(jrAcMgr).getEffectivePolicies(absPath);
+        verify(sessionDelegate).perform(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+
+    @Test
+    public void testSetPolicy() throws Exception {
+        AccessControlPolicy policy = mock(AccessControlPolicy.class);
+        delegator.setPolicy(absPath, policy);
+
+        verify(jrAcMgr).setPolicy(absPath, policy);
+        verify(sessionDelegate).performVoid(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+    
+    @Test
+    public void testRemovePolicy() throws Exception {
+        AccessControlPolicy policy = mock(AccessControlPolicy.class);
+        delegator.removePolicy(absPath, policy);
+
+        verify(jrAcMgr).removePolicy(absPath, policy);
+        verify(sessionDelegate).performVoid(any(SessionOperation.class));
+        verifyNoMoreInteractions(jrAcMgr, sessionDelegate);
+        verifyNoInteractions(principal);
+    }
+}
\ No newline at end of file
diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/PrincipalManagerDelegatorTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/PrincipalManagerDelegatorTest.java
new file mode 100644
index 0000000..9c3865d
--- /dev/null
+++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/PrincipalManagerDelegatorTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.jcr.delegate;
+
+import com.google.common.collect.Iterators;
+import org.apache.jackrabbit.api.security.principal.PrincipalIterator;
+import org.apache.jackrabbit.api.security.principal.PrincipalManager;
+import org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation;
+import org.apache.jackrabbit.oak.spi.security.principal.PrincipalQueryManager;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import javax.jcr.RepositoryException;
+import java.security.Principal;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.withSettings;
+
+public class PrincipalManagerDelegatorTest extends AbstractDelegatorTest {
+
+    private final SessionDelegate sessionDelegate = mockSessionDelegate();
+    private final PrincipalManager principalManager = mock(PrincipalManager.class);
+    private final Principal principal = mock(Principal.class);
+
+    private final PrincipalManagerDelegator delegator = new PrincipalManagerDelegator(sessionDelegate, principalManager);
+
+    private static void verifySavePerform(@NotNull SessionDelegate sessionDelegate, int times) throws RepositoryException {
+        verify(sessionDelegate, times(times)).safePerform(any(SessionOperation.class));
+        verify(sessionDelegate, times(times)).perform(any(SessionOperation.class));
+    }
+    
+    @Test
+    public void testHasPrincipal() throws Exception {
+        delegator.hasPrincipal("name");
+        
+        verify(principalManager).hasPrincipal("name");
+        verifySavePerform(sessionDelegate, 1);
+        verifyNoMoreInteractions(principalManager, sessionDelegate);
+    }
+    
+    @Test
+    public void testGetPrincipal() throws Exception {
+        delegator.getPrincipal("name");
+
+        verify(principalManager).getPrincipal("name");
+        verify(sessionDelegate).safePerformNullable(any(SessionOperation.class));
+        verify(sessionDelegate).performNullable(any(SessionOperation.class));
+        verifyNoMoreInteractions(principalManager, sessionDelegate);
+    }
+    
+    @Test
+    public void testFindPrincipals() throws Exception {
+        PrincipalIterator it = mock(PrincipalIterator.class);
+        doReturn(it).when(principalManager).findPrincipals(anyString(), anyInt());
+        
+        String simpleFilter = "simpleFilter";
+        delegator.findPrincipals(simpleFilter);
+        delegator.findPrincipals(simpleFilter, PrincipalManager.SEARCH_TYPE_NOT_GROUP);
+        delegator.findPrincipals(simpleFilter, true, PrincipalManager.SEARCH_TYPE_GROUP, 6, 0);
+        
+        verify(principalManager).findPrincipals(simpleFilter);
+        verify(principalManager).findPrincipals(simpleFilter, PrincipalManager.SEARCH_TYPE_NOT_GROUP);
+        verify(principalManager).findPrincipals(simpleFilter, PrincipalManager.SEARCH_TYPE_GROUP);
+        verify(it).skip(6);
+        verifySavePerform(sessionDelegate, 3);
+        verifyNoMoreInteractions(principalManager, sessionDelegate);
+    }
+
+    @Test
+    public void testFindPrincipalsOnPrincipalQueryManager() throws Exception {
+        PrincipalManager pm = mock(PrincipalManager.class, withSettings().extraInterfaces(PrincipalQueryManager.class));
+        PrincipalManagerDelegator del = new PrincipalManagerDelegator(sessionDelegate, pm);
+        del.findPrincipals("simpleFilter", false, PrincipalManager.SEARCH_TYPE_ALL, 25, 2);
+
+        verify((PrincipalQueryManager) pm).findPrincipals("simpleFilter", false, PrincipalManager.SEARCH_TYPE_ALL, 25, 2);
+
+        verifySavePerform(sessionDelegate, 1);
+        verifyNoMoreInteractions(principalManager, sessionDelegate);
+    }
+    
+    @Test
+    public void testGetPrincipals() throws Exception {
+        delegator.getPrincipals(PrincipalManager.SEARCH_TYPE_ALL);
+
+        verify(principalManager).getPrincipals(PrincipalManager.SEARCH_TYPE_ALL);
+        verifySavePerform(sessionDelegate, 1);
+        verifyNoMoreInteractions(principalManager, sessionDelegate);
+    }
+    
+    @Test
+    public void testGetGroupMembership() throws Exception {
+        delegator.getGroupMembership(principal);
+
+        verify(principalManager).getGroupMembership(principal);
+        verifySavePerform(sessionDelegate, 1);
+        verifyNoMoreInteractions(principalManager, sessionDelegate);
+    }
+    
+    @Test
+    public void testGetEveryone() throws Exception {
+        delegator.getEveryone();
+        
+        verify(principalManager).getEveryone();
+        verifySavePerform(sessionDelegate, 1);
+        verifyNoMoreInteractions(principalManager, sessionDelegate);
+    }
+}
\ No newline at end of file
diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegateTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegateTest.java
index 9fc7ed8..89c7ca2 100644
--- a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegateTest.java
+++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegateTest.java
@@ -16,91 +16,17 @@
  */
 package org.apache.jackrabbit.oak.jcr.delegate;
 
-import org.apache.jackrabbit.oak.Oak;
-import org.apache.jackrabbit.oak.api.AuthInfo;
-import org.apache.jackrabbit.oak.api.ContentSession;
 import org.apache.jackrabbit.oak.api.Root;
-import org.apache.jackrabbit.oak.api.Tree;
-import org.apache.jackrabbit.oak.jcr.session.RefreshStrategy;
-import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
-import org.apache.jackrabbit.oak.spi.security.authorization.AuthorizationConfiguration;
 import org.apache.jackrabbit.oak.spi.security.authorization.permission.PermissionAware;
 import org.apache.jackrabbit.oak.spi.security.authorization.permission.PermissionProvider;
-import org.apache.jackrabbit.oak.spi.whiteboard.DefaultWhiteboard;
-import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
-import org.apache.jackrabbit.oak.stats.Clock;
-import org.apache.jackrabbit.oak.stats.StatisticManager;
-import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
-import org.mockito.MockSettings;
-import org.mockito.stubbing.Answer;
 
-import java.util.Collections;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.withSettings;
-
-public class SessionDelegateTest {
-
-    @NotNull
-    private static SessionDelegate mockSessionDelegate(@NotNull Root root, @NotNull PermissionProvider pp) {
-        ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
-        Whiteboard wb = new DefaultWhiteboard();
-        StatisticManager statisticManager = new StatisticManager(wb, executorService);
-        return new SessionDelegate(mockContentSession(root), mockSecurityProvider(root, pp),
-                RefreshStrategy.Composite.create(), new ThreadLocal<>(), statisticManager, new Clock.Virtual());
-    }
-
-    @NotNull
-    private static Root mockRoot(@NotNull PermissionProvider pp, boolean permissionAware) {
-        MockSettings s = withSettings();
-        if (permissionAware) {
-            s.extraInterfaces(PermissionAware.class);
-        }
-        Root r = mock(Root.class, s);
-        Answer answer = invocationOnMock -> {
-            pp.refresh();
-            return invocationOnMock;
-        };
-        doAnswer(answer).when(r).refresh();
-        doAnswer(answer).when(r).rebase();
-
-        Tree t = mock(Tree.class);
-        when(t.getChild(anyString())).thenReturn(t);
-        when(r.getTree(anyString())).thenReturn(t);
 
-        if (permissionAware) {
-            when(((PermissionAware) r).getPermissionProvider()).thenReturn(pp);
-        }
-        return r;
-    }
-
-    @NotNull
-    private static SecurityProvider mockSecurityProvider(@NotNull Root root, @NotNull PermissionProvider pp) {
-        AuthorizationConfiguration authorizationConfiguration = mock(AuthorizationConfiguration.class);
-        when(authorizationConfiguration.getPermissionProvider(root, Oak.DEFAULT_WORKSPACE_NAME, Collections.emptySet())).thenReturn(pp);
-
-        SecurityProvider securityProvider = mock(SecurityProvider.class);
-        when(securityProvider.getConfiguration(AuthorizationConfiguration.class)).thenReturn(authorizationConfiguration);
-        return securityProvider;
-    }
-
-    @NotNull
-    private static ContentSession mockContentSession(@NotNull Root root) {
-        ContentSession cs = when(mock(ContentSession.class).getAuthInfo()).thenReturn(AuthInfo.EMPTY).getMock();
-        when(cs.getWorkspaceName()).thenReturn(Oak.DEFAULT_WORKSPACE_NAME);
-        when(cs.getLatestRoot()).thenReturn(root);
-        when(root.getContentSession()).thenReturn(cs);
-        return cs;
-    }
+public class SessionDelegateTest extends AbstractDelegatorTest {
 
     @Test
     public void testRefreshAware() {
diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/UserManagerDelegatorTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/UserManagerDelegatorTest.java
new file mode 100644
index 0000000..b860a79
--- /dev/null
+++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/delegate/UserManagerDelegatorTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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.jcr.delegate;
+
+import com.google.common.collect.Iterators;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.Query;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.oak.jcr.session.operation.UserManagerOperation;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.security.Principal;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+public class UserManagerDelegatorTest extends AbstractDelegatorTest {
+
+    private final SessionDelegate sessionDelegate = mockSessionDelegate();
+    private final UserManager um = mock(UserManager.class);
+    private final Authorizable user = when(mock(Authorizable.class, withSettings().extraInterfaces(User.class)).isGroup()).thenReturn(false).getMock();
+    private final Authorizable group = when(mock(Authorizable.class, withSettings().extraInterfaces(Group.class)).isGroup()).thenReturn(true).getMock();
+
+    private final UserManagerDelegator delegator = new UserManagerDelegator(sessionDelegate, um);
+    
+    private static void verifySessionDelegatePerform(@NotNull SessionDelegate sessionDelegate, int times) throws Exception {
+        verify(sessionDelegate, times(times)).checkAlive();
+        verify(sessionDelegate, times(times)).isAlive();
+        verify(sessionDelegate, times(times)).perform(any(UserManagerOperation.class));
+    }
+
+    private static void verifySessionDelegatePerformNullable(@NotNull SessionDelegate sessionDelegate,  int times) throws Exception {
+        verify(sessionDelegate, times(times)).checkAlive();
+        verify(sessionDelegate, times(times)).isAlive();
+        verify(sessionDelegate, times(times)).performNullable(any(UserManagerOperation.class));
+    }
+    
+    @Test(expected = IllegalArgumentException.class)
+    public void testCreateFromDelegator() {
+        new UserManagerDelegator(sessionDelegate, delegator);
+    }
+    
+    @Test
+    public void testGetAuthorizableById() throws Exception {
+        assertNull(delegator.getAuthorizable("id"));
+        
+        doReturn(user).when(um).getAuthorizable("id");
+        Authorizable a = delegator.getAuthorizable("id");
+        assertTrue(a instanceof UserDelegator);
+        
+        verify(um, times(2)).getAuthorizable("id");
+        verify(user).isGroup();
+        verifySessionDelegatePerformNullable(sessionDelegate, 2);
+        verifyNoMoreInteractions(um, user, sessionDelegate);
+        verifyNoMoreInteractions(group);
+    }
+
+    @Test
+    public void testGetAuthorizableByPrincipal() throws Exception {
+        Principal principal = mock(Principal.class);
+        assertNull(delegator.getAuthorizable(principal));
+
+        doReturn(group).when(um).getAuthorizable(principal);
+        Authorizable a = delegator.getAuthorizable(principal);
+        assertTrue(a instanceof GroupDelegator);
+
+        verify(um, times(2)).getAuthorizable(principal);
+        verify(group).isGroup();
+        verifySessionDelegatePerformNullable(sessionDelegate, 2);
+        verifyNoMoreInteractions(um, group, sessionDelegate);
+        verifyNoMoreInteractions(user);
+    }
+
+    @Test
+    public void testGetAuthorizableByPath() throws Exception {
+        String path = "/path";
+        assertNull(delegator.getAuthorizableByPath(path));
+
+        doReturn(user).when(um).getAuthorizableByPath(path);
+        Authorizable a = delegator.getAuthorizableByPath(path);
+        assertTrue(a instanceof UserDelegator);
+
+        verify(um, times(2)).getAuthorizableByPath(path);
+        verify(user).isGroup();
+        verifySessionDelegatePerformNullable(sessionDelegate, 2);
+        verifyNoMoreInteractions(um, user, sessionDelegate);
+        verifyNoMoreInteractions(group);
+    }
+    
+    @Test
+    public void testFindAuthorizables() throws Exception {
+        Query query = mock(Query.class);
+        doReturn(Iterators.emptyIterator()).when(um).findAuthorizables(query);
+        doReturn(Iterators.emptyIterator()).when(um).findAuthorizables(anyString(), anyString());
+        doReturn(Iterators.emptyIterator()).when(um).findAuthorizables(anyString(), anyString(), anyInt());
+        
+        delegator.findAuthorizables(query);
+        delegator.findAuthorizables("rel/path", "value");
+        delegator.findAuthorizables("rel/path", "value", UserManager.SEARCH_TYPE_AUTHORIZABLE);
+        
+        verify(um).findAuthorizables(query);
+        verify(um).findAuthorizables("rel/path", "value");
+        verify(um).findAuthorizables("rel/path", "value", UserManager.SEARCH_TYPE_AUTHORIZABLE);
+        
+        verifySessionDelegatePerform(sessionDelegate, 3);
+        verifyNoMoreInteractions(um, sessionDelegate);
+    }
+    
+    @Test
+    public void testCreateUser() throws Exception {
+        doReturn(user).when(um).createUser(anyString(), anyString());
+        doReturn(user).when(um).createUser(anyString(), anyString(), any(Principal.class), anyString());
+        
+        Principal p = mock(Principal.class);
+        User u1 = delegator.createUser("uid", "pw");
+        User u2 = delegator.createUser("uid", "pw", p, "rel/path");
+        assertTrue(u1 instanceof UserDelegator);
+        assertTrue(u2 instanceof UserDelegator);
+        
+        verify(um).createUser("uid", "pw");
+        verify(um).createUser("uid", "pw", p, "rel/path");
+
+        verifySessionDelegatePerform(sessionDelegate, 2);
+        verifyNoMoreInteractions(um, sessionDelegate);
+        verifyNoInteractions(user);
+    }
+
+    @Test
+    public void testCreateSystemUser() throws Exception {
+        doReturn(user).when(um).createSystemUser(anyString(), anyString());
+
+        User u = delegator.createSystemUser("sys-uid", "rel/path");
+        assertTrue(u instanceof UserDelegator);
+
+        verify(um).createSystemUser("sys-uid", "rel/path");
+        verifySessionDelegatePerform(sessionDelegate, 1);
+        verifyNoMoreInteractions(um, sessionDelegate);
+        verifyNoInteractions(user);
+    }
+    
+    @Test
+    public void testCreateGroup() throws Exception {
+        doReturn(group).when(um).createGroup(anyString());
+        doReturn(group).when(um).createGroup(anyString(), any(Principal.class), anyString());
+        doReturn(group).when(um).createGroup(any(Principal.class));
+        doReturn(group).when(um).createGroup(any(Principal.class), anyString());
+
+        Principal p = mock(Principal.class);
+        assertTrue(delegator.createGroup("groupId") instanceof GroupDelegator);
+        assertTrue(delegator.createGroup("groupId", p, "rel/path") instanceof GroupDelegator);
+        assertTrue(delegator.createGroup(p) instanceof GroupDelegator);
+        assertTrue(delegator.createGroup(p, "rel/path") instanceof GroupDelegator);
+        
+        verify(um).createGroup("groupId");
+        verify(um).createGroup("groupId", p, "rel/path");
+        verify(um).createGroup(p);
+        verify(um).createGroup(p, "rel/path");
+        
+        verifySessionDelegatePerform(sessionDelegate, 4);
+        verifyNoMoreInteractions(um, sessionDelegate);
+        verifyNoInteractions(group);
+    }
+    
+    
+    @Test
+    public void testIsAutoSave() throws Exception {
+        delegator.isAutoSave();
+
+        verify(um).isAutoSave();
+        verify(sessionDelegate).checkAlive();
+        verify(sessionDelegate).isAlive();
+        verify(sessionDelegate).safePerform(any(UserManagerOperation.class));
+        verify(sessionDelegate).perform(any(UserManagerOperation.class));
+        verifyNoMoreInteractions(um, sessionDelegate);
+    }
+    
+    @Test
+    public void testAutoSave() throws Exception {
+        delegator.autoSave(true);
+        delegator.autoSave(false);
+        
+        verify(um, times(2)).autoSave(anyBoolean());
+        verify(sessionDelegate, times(2)).checkAlive();
+        verify(sessionDelegate, times(2)).isAlive();
+        verify(sessionDelegate, times(2)).performVoid(any(UserManagerOperation.class));
+        verifyNoMoreInteractions(um, sessionDelegate);
+    }
+}
\ No newline at end of file