SLING-3524: Clone JCR session when a resource resolver is cloned.

Detect when a session-based resource resolver is being cloned, and clone
the underlying JCR session using the self-impersonation trick. This
requires slight API changes in order to detect cloning.
diff --git a/pom.xml b/pom.xml
index 0ba0132..ee5ac67 100644
--- a/pom.xml
+++ b/pom.xml
@@ -176,7 +176,7 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.api</artifactId>
-            <version>2.16.4</version>
+            <version>2.18.1-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrProviderStateFactory.java b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrProviderStateFactory.java
index 8767fa9..1c3649e 100644
--- a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrProviderStateFactory.java
+++ b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrProviderStateFactory.java
@@ -145,7 +145,12 @@
             @Nonnull final Map<String, Object> authenticationInfo,
             @Nullable final BundleContext ctx
     ) throws LoginException {
-        final Session impersonatedSession = handleImpersonation(session, authenticationInfo, logoutSession);
+        boolean explicitSessionUsed = (getSession(authenticationInfo) != null);
+        final Session impersonatedSession = handleImpersonation(session, authenticationInfo, logoutSession, explicitSessionUsed);
+        if (impersonatedSession != session && explicitSessionUsed) {
+            // update the session in the auth info map in case the resolver gets cloned in the future
+            authenticationInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, impersonatedSession);
+        }
         // if we're actually impersonating, we're responsible for closing the session we've created, regardless
         // of what the original logoutSession value was.
         boolean doLogoutSession = logoutSession || (impersonatedSession != session);
@@ -167,18 +172,26 @@
      * @param logoutSession
      *            whether to logout the <code>session</code> after impersonation
      *            or not.
+     * @param explicitSessionUsed
+     *            whether the JCR session was explicitly given in the auth info or not.
      * @return The original session or impersonated session.
      * @throws LoginException
      *             If something goes wrong.
      */
     private static Session handleImpersonation(final Session session, final Map<String, Object> authenticationInfo,
-            final boolean logoutSession) throws LoginException {
+            final boolean logoutSession, boolean explicitSessionUsed) throws LoginException {
         final String sudoUser = getSudoUser(authenticationInfo);
-        if (sudoUser != null && !session.getUserID().equals(sudoUser)) {
+        boolean needsSudo = (sudoUser != null) && !session.getUserID().equals(sudoUser);
+        boolean needsCloning = !needsSudo && explicitSessionUsed && authenticationInfo.containsKey(ResourceProvider.AUTH_CLONE);
+        if (needsCloning || needsSudo) {
+            // If we just need to clone the session, we impersonate with the same user ID and not set an impersonator attribute.
+            // In all other cases, it's a "proper" sudo to the given user.
             try {
-                final SimpleCredentials creds = new SimpleCredentials(sudoUser, new char[0]);
+                final SimpleCredentials creds = new SimpleCredentials(needsSudo ? sudoUser : session.getUserID(), new char[0]);
                 copyAttributes(creds, authenticationInfo);
-                creds.setAttribute(ResourceResolver.USER_IMPERSONATOR, session.getUserID());
+                if (needsSudo) {
+                    creds.setAttribute(ResourceResolver.USER_IMPERSONATOR, session.getUserID());
+                }
                 return session.impersonate(creds);
             } catch (final RepositoryException re) {
                 throw getLoginException(re);
diff --git a/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderTest.java b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderTest.java
index aa40162..a520dc1 100644
--- a/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderTest.java
+++ b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderTest.java
@@ -32,6 +32,7 @@
 import org.apache.sling.commons.testing.jcr.RepositoryTestBase;
 import org.apache.sling.jcr.resource.api.JcrResourceConstants;
 import org.apache.sling.spi.resource.provider.ResolveContext;
+import org.apache.sling.spi.resource.provider.ResourceProvider;
 import org.junit.Assert;
 import org.mockito.Mockito;
 import org.osgi.framework.ServiceReference;
@@ -47,26 +48,26 @@
         super.setUp();
         // create the session
         session = getSession();
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-    }
-
-    public void testAdaptTo_Principal() {
-        jcrResourceProvider = new JcrResourceProvider();
-        ResolveContext ctx = Mockito.mock(ResolveContext.class);
-        Mockito.when(ctx.getProviderState()).thenReturn(new JcrProviderState(session, null, false));
-        Assert.assertNotNull(jcrResourceProvider.adaptTo(ctx, Principal.class));
-    }
-    
-    public void testLeakOnSudo() throws LoginException, RepositoryException, NamingException {
         Repository repo = getRepository();
         ComponentContext ctx = Mockito.mock(ComponentContext.class);
         Mockito.when(ctx.locateService(Mockito.anyString(), Mockito.any(ServiceReference.class))).thenReturn(repo);
         jcrResourceProvider = new JcrResourceProvider();
         jcrResourceProvider.activate(ctx);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        jcrResourceProvider.deactivate();
+        super.tearDown();
+    }
+
+    public void testAdaptTo_Principal() {
+        ResolveContext ctx = Mockito.mock(ResolveContext.class);
+        Mockito.when(ctx.getProviderState()).thenReturn(new JcrProviderState(session, null, false));
+        Assert.assertNotNull(jcrResourceProvider.adaptTo(ctx, Principal.class));
+    }
+
+    public void testLeakOnSudo() throws LoginException, RepositoryException, NamingException {
         Map<String, Object> authInfo = new HashMap<String, Object>();
         authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session);
         authInfo.put(ResourceResolverFactory.USER_IMPERSONATION, "anonymous");
@@ -75,6 +76,15 @@
         jcrResourceProvider.logout(providerState);
         assertFalse("Impersonated session wasn't closed.", providerState.getSession().isLive());
     }
+
+    public void testNoSessionSharing() throws LoginException {
+        Map<String, Object> authInfo = new HashMap<String, Object>();
+        authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session);
+        authInfo.put(ResourceProvider.AUTH_CLONE, true);
+        JcrProviderState providerState = jcrResourceProvider.authenticate(authInfo);
+        Assert.assertNotEquals("Cloned resolver didn't clone session.", session, providerState.getSession());
+        jcrResourceProvider.logout(providerState);
+    }
 }