SLING-11503 option to authenticate webconsole only against JCR (#2)

add an option to authenticate webconsole only against JCR; to achieve this, set the framework property "sling.webconsole.authType" to the value "jcrAuth". The default is authentication against Sling with a fallback to JCR if the Sling authentication is not available.

Co-authored-by: Carsten Ziegeler <cziegeler@apache.org>
diff --git a/.gitignore b/.gitignore
index 5b783ed..19758c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,4 @@
 .DS_Store
 jcr.log
 atlassian-ide-plugin.xml
+.vscode
diff --git a/pom.xml b/pom.xml
index 333c9f5..6f1f20b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>sling-bundle-parent</artifactId>
         <groupId>org.apache.sling</groupId>
-        <version>35</version>
+        <version>48</version>
         <relativePath />
     </parent>
 
@@ -105,5 +105,23 @@
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
         </dependency>
+        
+        <dependency>
+        	<groupId>org.apache.sling</groupId>
+        	<artifactId>org.apache.sling.testing.osgi-mock.junit4</artifactId>
+        	<version>3.3.0</version>
+        	<scope>test</scope>
+        </dependency>
+		<dependency>
+		    <groupId>junit</groupId>
+		    <artifactId>junit</artifactId>
+		    <scope>test</scope>
+		</dependency>
+		<dependency>
+		    <groupId>org.mockito</groupId>
+		    <artifactId>mockito-core</artifactId>
+		    <version>4.6.1</version>
+		    <scope>test</scope>
+		</dependency>
     </dependencies>
 </project>
diff --git a/src/main/java/org/apache/sling/extensions/webconsolesecurityprovider/internal/ServicesListener.java b/src/main/java/org/apache/sling/extensions/webconsolesecurityprovider/internal/ServicesListener.java
index e52daad..5c60215 100644
--- a/src/main/java/org/apache/sling/extensions/webconsolesecurityprovider/internal/ServicesListener.java
+++ b/src/main/java/org/apache/sling/extensions/webconsolesecurityprovider/internal/ServicesListener.java
@@ -18,7 +18,6 @@
  * under the License.
  */
 
-
 import java.util.Dictionary;
 import java.util.Hashtable;
 
@@ -31,16 +30,30 @@
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.cm.ManagedService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * The <code>ServicesListener</code> listens for the required services
- * and registers the security provider when required services are available
+ * and registers the security provider when required services are available.
+ *
+ * It supports 3 modes, which can be forced by the value of the framework property "sling.webconsole.authType"
+ * <ul>
+ *   <li> "jcrAuth": always authenticate against the JCR repository even if Sling Authentication is possible.</li>
+ *   <li> "slingAuth": always use SlingAuthentication
+ *   <li> no value (default) : Use SlingAuthentication if available, fallback to JCR repository
+ *   <li> If an invalid value is specifed, the value is ignored and the default is used
+ * </ul>
  */
 public class ServicesListener {
 
     private static final String AUTH_SUPPORT_CLASS = "org.apache.sling.auth.core.AuthenticationSupport";
     private static final String AUTHENTICATOR_CLASS = "org.apache.sling.api.auth.Authenticator";
     private static final String REPO_CLASS = "javax.jcr.Repository";
+    
+    protected static final String WEBCONSOLE_AUTH_TYPE = "sling.webconsole.authType";
+    protected static final String JCR_AUTH = "jcrAuth";
+    protected static final String SLING_AUTH = "slingAuth";
 
     /** The bundle context. */
     private final BundleContext bundleContext;
@@ -54,10 +67,16 @@
     /** The listener for the authenticator. */
     private final Listener authListener;
 
-    private enum State {
+    enum State {
         NONE,
-        PROVIDER,
-        PROVIDER2
+        PROVIDER_JCR,
+        PROVIDER_SLING
+    }
+
+    enum AuthType {
+        DEFAULT,
+        JCR,
+        SLING
     }
 
     /** State */
@@ -68,12 +87,19 @@
 
     /** The registration for the provider2 */
     private ServiceRegistration<?> provider2Reg;
+    
+    /** Auth type */
+    final AuthType authType;
+
+    /** Logger */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
     /**
      * Start listeners
      */
     public ServicesListener(final BundleContext bundleContext) {
         this.bundleContext = bundleContext;
+        this.authType = getAuthType();
         this.authSupportListener = new Listener(AUTH_SUPPORT_CLASS);
         this.repositoryListener = new Listener(REPO_CLASS);
         this.authListener = new Listener(AUTHENTICATOR_CLASS);
@@ -82,56 +108,77 @@
         this.authListener.start();
     }
 
+    AuthType getAuthType() {
+        final String webConsoleAuthType = bundleContext.getProperty(WEBCONSOLE_AUTH_TYPE);
+        if ( webConsoleAuthType != null ) {
+            if ( webConsoleAuthType.equals(JCR_AUTH) ) {
+                return AuthType.JCR;
+            } else if ( webConsoleAuthType.equals(SLING_AUTH) ) {
+                return AuthType.SLING;
+            }
+            logger.error("Ignoring invalid auth type for webconsole security provider {}",  this.authType);
+        }
+        return AuthType.DEFAULT;
+    }
+
+    State getTargetState(final boolean slingAvailable, final boolean jcrAvailable) {
+        if ( !slingAvailable && !jcrAvailable ) {
+            return State.NONE;
+        }
+        if ( this.authType == AuthType.JCR && jcrAvailable ) {
+            return State.PROVIDER_JCR;
+        }
+        if ( this.authType == AuthType.SLING && slingAvailable ) {
+            return State.PROVIDER_SLING;
+        }
+        if ( this.authType == AuthType.DEFAULT ) {
+            return slingAvailable ? State.PROVIDER_SLING : State.PROVIDER_JCR;
+        }
+        return State.NONE;
+    }
+
     /**
      * Notify of service changes from the listeners.
      */
     public synchronized void notifyChange() {
         // check if all services are available
+        
         final Object authSupport = this.authSupportListener.getService();
         final Object authenticator = this.authListener.getService();
-        final boolean hasAuthServices = authSupport != null && authenticator != null;
         final Object repository = this.repositoryListener.getService();
-        if ( registrationState == State.NONE ) {
-            if ( hasAuthServices ) {
-                registerProvider2(authSupport, authenticator);
-            } else if ( repository != null ) {
-                registerProvider(repository);
+
+        final State targetState = this.getTargetState(authSupport != null && authenticator != null, repository != null);
+        if ( this.registrationState != targetState ) {
+            if ( targetState != State.PROVIDER_JCR ) {
+                this.unregisterProviderJcr();
+            } 
+            if ( targetState != State.PROVIDER_SLING ) {
+                this.unregisterProviderSling();
             }
-        } else if ( registrationState == State.PROVIDER ) {
-            if ( hasAuthServices ) {
-                registerProvider2(authSupport, authenticator);
-                unregisterProvider();
-            } else if ( repository == null ) {
-                unregisterProvider();
-                this.registrationState = State.NONE;
+            if ( targetState == State.PROVIDER_JCR ) {
+                this.registerProviderJcr(repository);
+            } else if ( targetState == State.PROVIDER_SLING ) {
+                this.registerProviderSling(authSupport, authenticator);
             }
-        } else {
-            if ( authSupport == null ) {
-                if ( repository != null ) {
-                    registerProvider(repository);
-                } else {
-                    this.registrationState = State.NONE;
-                }
-                unregisterProvider2();
-            }
+            this.registrationState = targetState;
         }
     }
 
-    private void unregisterProvider2() {
+    private void unregisterProviderSling() {
         if ( this.provider2Reg != null ) {
             this.provider2Reg.unregister();
             this.provider2Reg = null;
         }
     }
 
-    private void unregisterProvider() {
+    private void unregisterProviderJcr() {
         if ( this.providerReg != null ) {
             this.providerReg.unregister();
             this.providerReg = null;
         }
     }
 
-    private void registerProvider2(final Object authSupport, final Object authenticator) {
+    private void registerProviderSling(final Object authSupport, final Object authenticator) {
         final Dictionary<String, Object> props = new Hashtable<String, Object>();
         props.put(Constants.SERVICE_PID, SlingWebConsoleSecurityProvider.class.getName());
         props.put(Constants.SERVICE_DESCRIPTION, "Apache Sling Web Console Security Provider 2");
@@ -140,10 +187,9 @@
         this.provider2Reg = this.bundleContext.registerService(
             new String[] {ManagedService.class.getName(), WebConsoleSecurityProvider.class.getName()},
                           new SlingWebConsoleSecurityProvider2(authSupport, authenticator), props);
-        this.registrationState = State.PROVIDER2;
     }
 
-    private void registerProvider(final Object repository) {
+    private void registerProviderJcr(final Object repository) {
         final Dictionary<String, Object> props = new Hashtable<String, Object>();
         props.put(Constants.SERVICE_PID, SlingWebConsoleSecurityProvider.class.getName());
         props.put(Constants.SERVICE_DESCRIPTION, "Apache Sling Web Console Security Provider");
@@ -151,7 +197,6 @@
         props.put("webconsole.security.provider.id", "org.apache.sling.extensions.webconsolesecurityprovider");
         this.providerReg = this.bundleContext.registerService(
             new String[] {ManagedService.class.getName(), WebConsoleSecurityProvider.class.getName()}, new SlingWebConsoleSecurityProvider(repository), props);
-        this.registrationState = State.PROVIDER;
     }
 
     /**
@@ -161,8 +206,8 @@
         this.repositoryListener.deactivate();
         this.authSupportListener.deactivate();
         this.authListener.deactivate();
-        this.unregisterProvider();
-        this.unregisterProvider2();
+        this.unregisterProviderJcr();
+        this.unregisterProviderSling();
     }
 
     /**
diff --git a/src/test/java/org/apache/sling/extensions/webconsolesecurityprovider/internal/ServiceListenerTest.java b/src/test/java/org/apache/sling/extensions/webconsolesecurityprovider/internal/ServiceListenerTest.java
new file mode 100644
index 0000000..b59b190
--- /dev/null
+++ b/src/test/java/org/apache/sling/extensions/webconsolesecurityprovider/internal/ServiceListenerTest.java
@@ -0,0 +1,216 @@
+package org.apache.sling.extensions.webconsolesecurityprovider.internal;
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import javax.jcr.Repository;
+
+import org.apache.felix.webconsole.WebConsoleSecurityProvider;
+import org.apache.sling.api.auth.Authenticator;
+import org.apache.sling.auth.core.AuthenticationSupport;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.osgi.framework.BundleContext;
+
+public class ServiceListenerTest {
+
+    @Rule
+    public OsgiContext context = new OsgiContext();
+    
+    @Mock
+    Repository repository;
+    
+    @Mock
+    AuthenticationSupport authenticationSupport;
+    
+    @Mock
+    Authenticator authenticator;
+    
+    
+    ServicesListener listener;
+    
+    @Before
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+
+    }
+    
+    @After
+    public void shutdown() {
+        listener.deactivate();
+    }
+    
+    @Test
+    public void testDefaultAuth() {
+        listener = new ServicesListener(wrapForValidProperties(context.bundleContext()));
+        assertNoSecurityProviderRegistered();
+        
+        context.registerService(Repository.class,repository);
+        listener.notifyChange();
+        assertRepositoryRegistered();
+
+        context.registerService(AuthenticationSupport.class, authenticationSupport);
+        listener.notifyChange();
+        assertRepositoryRegistered();
+        
+        context.registerService(Authenticator.class, authenticator);
+        listener.notifyChange();
+        assertSlingAuthRegistered();
+    }
+    
+    @Test
+    public void testWithSlingAuth() {
+        try {
+            System.setProperty(ServicesListener.WEBCONSOLE_AUTH_TYPE, ServicesListener.SLING_AUTH);
+            listener = new ServicesListener(wrapForValidProperties(context.bundleContext()));
+            assertNoSecurityProviderRegistered();
+            
+            context.registerService(Repository.class,repository);
+            listener.notifyChange();
+            assertNoSecurityProviderRegistered();
+
+            context.registerService(AuthenticationSupport.class, authenticationSupport);
+            listener.notifyChange();
+            assertNoSecurityProviderRegistered();
+            
+            context.registerService(Authenticator.class, authenticator);
+            listener.notifyChange();
+            assertSlingAuthRegistered();
+        } finally {
+            System.getProperties().remove(ServicesListener.WEBCONSOLE_AUTH_TYPE);
+        }
+    }
+
+    @Test
+    public void testWithForcedJcrAuth() {
+        try {
+            System.setProperty(ServicesListener.WEBCONSOLE_AUTH_TYPE, ServicesListener.JCR_AUTH);
+            listener = new ServicesListener(wrapForValidProperties(context.bundleContext()));
+            assertNoSecurityProviderRegistered();
+            
+            // no matter what is registered, always the auth against the repo needs to be there
+            
+            context.registerService(Repository.class,repository);
+            listener.notifyChange();
+            assertRepositoryRegistered();
+    
+            context.registerService(AuthenticationSupport.class, authenticationSupport);
+            listener.notifyChange();
+            assertRepositoryRegistered();
+            
+            context.registerService(Authenticator.class, authenticator);
+            listener.notifyChange();
+            assertRepositoryRegistered();
+        } finally {
+            System.getProperties().remove(ServicesListener.WEBCONSOLE_AUTH_TYPE);
+        }
+    }
+
+    @Test
+    public void testGetAuthType() {
+        try {
+            listener = new ServicesListener(wrapForValidProperties(context.bundleContext()));
+            assertEquals(ServicesListener.AuthType.DEFAULT, listener.getAuthType());
+
+            System.setProperty(ServicesListener.WEBCONSOLE_AUTH_TYPE, ServicesListener.JCR_AUTH);
+            listener = new ServicesListener(wrapForValidProperties(context.bundleContext()));
+            assertEquals(ServicesListener.AuthType.JCR, listener.getAuthType());
+
+            System.setProperty(ServicesListener.WEBCONSOLE_AUTH_TYPE, ServicesListener.SLING_AUTH);
+            listener = new ServicesListener(wrapForValidProperties(context.bundleContext()));
+            assertEquals(ServicesListener.AuthType.SLING, listener.getAuthType());
+
+            System.setProperty(ServicesListener.WEBCONSOLE_AUTH_TYPE, "invalid");
+            listener = new ServicesListener(wrapForValidProperties(context.bundleContext()));
+            assertEquals(ServicesListener.AuthType.DEFAULT, listener.getAuthType());
+        } finally {
+            System.getProperties().remove(ServicesListener.WEBCONSOLE_AUTH_TYPE);
+        }
+    }
+
+    @Test
+    public void testGetTargetState() {
+        try {
+            listener = new ServicesListener(wrapForValidProperties(context.bundleContext()));
+            assertEquals(ServicesListener.State.NONE, listener.getTargetState(false, false));
+            assertEquals(ServicesListener.State.PROVIDER_JCR, listener.getTargetState(false, true));
+            assertEquals(ServicesListener.State.PROVIDER_SLING, listener.getTargetState(true, false));
+            assertEquals(ServicesListener.State.PROVIDER_SLING, listener.getTargetState(true, true));
+
+            System.setProperty(ServicesListener.WEBCONSOLE_AUTH_TYPE, ServicesListener.JCR_AUTH);
+            listener = new ServicesListener(wrapForValidProperties(context.bundleContext()));
+            assertEquals(ServicesListener.State.NONE, listener.getTargetState(false, false));
+            assertEquals(ServicesListener.State.PROVIDER_JCR, listener.getTargetState(false, true));
+            assertEquals(ServicesListener.State.NONE, listener.getTargetState(true, false));
+            assertEquals(ServicesListener.State.PROVIDER_JCR, listener.getTargetState(true, true));
+
+            System.setProperty(ServicesListener.WEBCONSOLE_AUTH_TYPE, ServicesListener.SLING_AUTH);
+            listener = new ServicesListener(wrapForValidProperties(context.bundleContext()));
+            assertEquals(ServicesListener.State.NONE, listener.getTargetState(false, false));
+            assertEquals(ServicesListener.State.NONE, listener.getTargetState(false, true));
+            assertEquals(ServicesListener.State.PROVIDER_SLING, listener.getTargetState(true, false));
+            assertEquals(ServicesListener.State.PROVIDER_SLING, listener.getTargetState(true, true));
+        } finally {
+            System.getProperties().remove(ServicesListener.WEBCONSOLE_AUTH_TYPE);
+        }
+    }
+
+    // until https://issues.apache.org/jira/browse/SLING-11505 is implemented
+    private BundleContext wrapForValidProperties(BundleContext bc) {
+        BundleContext spy = Mockito.spy(bc);
+        Mockito.when(spy.getProperty(Mockito.anyString())).thenAnswer(invocation -> {
+            String key = (String) invocation.getArguments()[0];
+            return System.getProperty(key);
+        });
+        return spy;
+    }
+
+    // Helpers
+    
+    private void assertRepositoryRegistered() { 
+        assertTrue("Expected to have the repository registered",getSecurityProvider() instanceof SlingWebConsoleSecurityProvider);
+    }
+    
+    private void assertSlingAuthRegistered() {
+        assertTrue("Expected to have SlingAuth registered",getSecurityProvider() instanceof SlingWebConsoleSecurityProvider2); 
+    }
+    
+    private void assertNoSecurityProviderRegistered () {
+        assertNull(getSecurityProvider());
+    }
+    
+    private WebConsoleSecurityProvider getSecurityProvider() {
+        return context.getService(WebConsoleSecurityProvider.class);
+    }
+    
+  
+
+    
+    
+    
+}