FELIX-6390 Refactor the default authentication mechanism of the (#71)

webconsole to be a WebConsoleSecurityProvider2
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/BasicWebConsoleSecurityProvider.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/BasicWebConsoleSecurityProvider.java
new file mode 100644
index 0000000..c17e530
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/BasicWebConsoleSecurityProvider.java
@@ -0,0 +1,173 @@
+/*
+ * 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.felix.webconsole.internal.servlet;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.webconsole.WebConsoleSecurityProvider2;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.http.HttpContext;
+
+/**
+ * Basic implementation of WebConsoleSecurityProvider to replace logic that
+ * was previously in OsgiManagerHttpContext
+ */
+public class BasicWebConsoleSecurityProvider implements WebConsoleSecurityProvider2 {
+
+    static final String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
+
+    static final String HEADER_AUTHORIZATION = "Authorization";
+
+    static final String AUTHENTICATION_SCHEME_BASIC = "Basic";
+
+    private final String username;
+
+    private final Password password;
+
+    private final String realm;
+
+    private BundleContext bundleContext;
+
+    public BasicWebConsoleSecurityProvider(BundleContext bundleContext, String username, String password,
+            String realm) {
+        super();
+        this.bundleContext = bundleContext;
+        this.username = username;
+        this.password = new Password(password);
+        this.realm = realm;
+    }
+
+    public Object authenticate(String username, String password) {
+        if ( this.username.equals( username ) && this.password.matches( password.getBytes() ) )
+        {
+            if (bundleContext.getProperty(OsgiManager.FRAMEWORK_PROP_SECURITY_PROVIDERS) == null) {
+                // Only allow username and password authentication if no mandatory security providers are registered
+                return true;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * All users authenticated with the repository are granted access for all roles in the Web Console.
+     */
+    @Override
+    public boolean authorize(Object user, String role) {
+        return true;
+    }
+
+    @Override
+    public boolean authenticate(HttpServletRequest request, HttpServletResponse response) {
+        // Return immediately if the header is missing
+        String authHeader = request.getHeader( HEADER_AUTHORIZATION );
+        if ( authHeader != null && authHeader.length() > 0 )
+        {
+
+            // Get the authType (Basic, Digest) and authInfo (user/password)
+            // from
+            // the header
+            authHeader = authHeader.trim();
+            int blank = authHeader.indexOf( ' ' );
+            if ( blank > 0 )
+            {
+                String authType = authHeader.substring( 0, blank );
+                String authInfo = authHeader.substring( blank ).trim();
+
+                // Check whether authorization type matches
+                if ( authType.equalsIgnoreCase( AUTHENTICATION_SCHEME_BASIC ) )
+                {
+                    try
+                    {
+                        byte[][] userPass = base64Decode( authInfo );
+                        final String username = toString( userPass[0] );
+
+                        // authenticate
+                        if ( authenticate( username, toString(userPass[1]) ) != null )
+                        {
+                            // as per the spec, set attributes
+                            request.setAttribute( HttpContext.AUTHENTICATION_TYPE, HttpServletRequest.BASIC_AUTH );
+                            request.setAttribute( HttpContext.REMOTE_USER, username );
+
+                            // set web console user attribute
+                            request.setAttribute( WebConsoleSecurityProvider2.USER_ATTRIBUTE, username );
+
+                            // succeed
+                            return true;
+                        }
+                    }
+                    catch ( Exception e )
+                    {
+                        // Ignore
+                    }
+                }
+            }
+        }
+
+        // request authentication
+        try
+        {
+            response.setHeader( HEADER_WWW_AUTHENTICATE, AUTHENTICATION_SCHEME_BASIC + " realm=\"" + this.realm + "\"" );
+            response.setStatus( HttpServletResponse.SC_UNAUTHORIZED );
+            response.setContentLength( 0 );
+            response.flushBuffer();
+        }
+        catch ( IOException ioe )
+        {
+            // failed sending the response ... cannot do anything about it
+        }
+
+        // inform HttpService that authentication failed
+        return false;
+    }
+
+    static byte[][] base64Decode( String srcString )
+    {
+        byte[] transformed = Base64.decodeBase64( srcString );
+        for ( int i = 0; i < transformed.length; i++ )
+        {
+            if ( transformed[i] == ':' )
+            {
+                byte[] user = new byte[i];
+                byte[] pass = new byte[transformed.length - i - 1];
+                System.arraycopy( transformed, 0, user, 0, user.length );
+                System.arraycopy( transformed, i + 1, pass, 0, pass.length );
+                return new byte[][]
+                    { user, pass };
+            }
+        }
+
+        return new byte[][]
+            { transformed, new byte[0] };
+    }
+
+    static String toString( final byte[] src )
+    {
+        try
+        {
+            return new String( src, "ISO-8859-1" );
+        }
+        catch ( UnsupportedEncodingException uee )
+        {
+            return new String( src );
+        }
+    }
+
+}
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
index 9a3569e..767da5b 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
@@ -230,6 +230,9 @@
 
     private String webManagerRoot;
 
+    // not-null when the BasicWebConsoleSecurityProvider service is registered
+    private ServiceRegistration<WebConsoleSecurityProvider> basicSecurityServiceRegistration;
+
     // true if the OsgiManager is registered as a Servlet with the HttpService
     private boolean httpServletRegistered;
 
@@ -958,11 +961,22 @@
         // register the servlet and resources
         try
         {
-            HttpContext httpContext = new OsgiManagerHttpContext(bundleContext, httpService,
-                securityProviderTracker, userId, password, realm);
+            HttpContext httpContext = new OsgiManagerHttpContext(httpService,
+                securityProviderTracker, realm);
 
             Dictionary<String, String> servletConfig = toStringConfig(config);
 
+            if (basicSecurityServiceRegistration == null) {
+                //register this component
+                BasicWebConsoleSecurityProvider service = new BasicWebConsoleSecurityProvider(bundleContext,
+                        userId, password, realm);
+                Dictionary<String, Object> serviceProperties = new Hashtable<>(); // NOSONAR
+                // this is a last resort service, so use a low service ranking to prefer all other services over this one
+                serviceProperties.put(Constants.SERVICE_RANKING, Integer.MIN_VALUE);
+                basicSecurityServiceRegistration = bundleContext.registerService(WebConsoleSecurityProvider.class,
+                        service, serviceProperties);
+            }
+
             if (!httpServletRegistered) {
                 // register this servlet and take note of this
                 httpService.registerServlet(this.webManagerRoot, this, servletConfig,
@@ -1002,6 +1016,16 @@
         if (httpService == null)
             return;
 
+        if (basicSecurityServiceRegistration != null) {
+            try {
+                basicSecurityServiceRegistration.unregister();
+            } catch (Throwable t) {
+                log(LogService.LOG_WARNING,
+                        "unbindHttpService: Failed unregistering basic WebConsoleSecurityProvider", t);
+            }
+            basicSecurityServiceRegistration = null;
+        }
+
         if (httpResourcesRegistered)
         {
             try
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
index c7d472f..f8a4e49 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
@@ -17,16 +17,19 @@
 package org.apache.felix.webconsole.internal.servlet;
 
 
+import static org.apache.felix.webconsole.internal.servlet.BasicWebConsoleSecurityProvider.AUTHENTICATION_SCHEME_BASIC;
+import static org.apache.felix.webconsole.internal.servlet.BasicWebConsoleSecurityProvider.HEADER_AUTHORIZATION;
+import static org.apache.felix.webconsole.internal.servlet.BasicWebConsoleSecurityProvider.HEADER_WWW_AUTHENTICATE;
+
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.net.URL;
+
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.felix.webconsole.User;
 import org.apache.felix.webconsole.WebConsoleSecurityProvider;
 import org.apache.felix.webconsole.WebConsoleSecurityProvider2;
-import org.osgi.framework.BundleContext;
 import org.osgi.service.http.HttpContext;
 import org.osgi.service.http.HttpService;
 import org.osgi.util.tracker.ServiceTracker;
@@ -35,33 +38,17 @@
 final class OsgiManagerHttpContext implements HttpContext
 {
 
-    private static final String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
-
-    private static final String HEADER_AUTHORIZATION = "Authorization";
-
-    private static final String AUTHENTICATION_SCHEME_BASIC = "Basic";
-
-    private final BundleContext bundleContext;
-
     private final HttpContext base;
 
     private final ServiceTracker<WebConsoleSecurityProvider, WebConsoleSecurityProvider> tracker;
 
-    private final String username;
-
-    private final Password password;
-
     private final String realm;
 
-
-    OsgiManagerHttpContext(final BundleContext bundleContext,
-        final HttpService httpService, final ServiceTracker<WebConsoleSecurityProvider, WebConsoleSecurityProvider> tracker, final String username,
-        final String password, final String realm )
+    OsgiManagerHttpContext(final HttpService httpService,
+            final ServiceTracker<WebConsoleSecurityProvider, WebConsoleSecurityProvider> tracker,
+            final String realm)
     {
-        this.bundleContext = bundleContext;
         this.tracker = tracker;
-        this.username = username;
-        this.password = new Password(password);
         this.realm = realm;
         this.base = httpService.createDefaultHttpContext();
     }
@@ -162,8 +149,8 @@
                 {
                     try
                     {
-                        byte[][] userPass = base64Decode( authInfo );
-                        final String username = toString( userPass[0] );
+                        byte[][] userPass = BasicWebConsoleSecurityProvider.base64Decode( authInfo );
+                        final String username = BasicWebConsoleSecurityProvider.toString( userPass[0] );
 
                         // authenticate
                         if ( authenticate( provider, username, userPass[1] ) )
@@ -204,52 +191,11 @@
         return false;
     }
 
-    private static byte[][] base64Decode( String srcString )
-    {
-        byte[] transformed = Base64.decodeBase64( srcString );
-        for ( int i = 0; i < transformed.length; i++ )
-        {
-            if ( transformed[i] == ':' )
-            {
-                byte[] user = new byte[i];
-                byte[] pass = new byte[transformed.length - i - 1];
-                System.arraycopy( transformed, 0, user, 0, user.length );
-                System.arraycopy( transformed, i + 1, pass, 0, pass.length );
-                return new byte[][]
-                    { user, pass };
-            }
-        }
-
-        return new byte[][]
-            { transformed, new byte[0] };
-    }
-
-
-    private static String toString( final byte[] src )
-    {
-        try
-        {
-            return new String( src, "ISO-8859-1" );
-        }
-        catch ( UnsupportedEncodingException uee )
-        {
-            return new String( src );
-        }
-    }
-
-
     private boolean authenticate( WebConsoleSecurityProvider provider, String username, byte[] password )
     {
         if ( provider != null )
         {
-            return provider.authenticate( username, toString( password ) ) != null;
-        }
-        if ( this.username.equals( username ) && this.password.matches( password ) )
-        {
-            if (bundleContext.getProperty(OsgiManager.FRAMEWORK_PROP_SECURITY_PROVIDERS) == null) {
-                // Only allow username and password authentication if no mandatory security providers are registered
-                return true;
-            }
+            return provider.authenticate( username, BasicWebConsoleSecurityProvider.toString( password ) ) != null;
         }
         return false;
     }
diff --git a/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java b/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java
index 0b49dbd..70fae7e 100644
--- a/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java
+++ b/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java
@@ -33,14 +33,15 @@
     public void testAuthenticate() throws Exception {
         BundleContext bc = Mockito.mock(BundleContext.class);
         HttpService svc = Mockito.mock(HttpService.class);
-        OsgiManagerHttpContext ctx = new OsgiManagerHttpContext(bc, svc, null, "foo", "bar", "blah");
+        OsgiManagerHttpContext ctx = new OsgiManagerHttpContext(svc, null, "blah");
 
         Method authenticateMethod = OsgiManagerHttpContext.class.getDeclaredMethod(
                 "authenticate", new Class [] {WebConsoleSecurityProvider.class, String.class, byte[].class});
         authenticateMethod.setAccessible(true);
 
-        assertEquals(true, authenticateMethod.invoke(ctx, null, "foo", "bar".getBytes()));
-        assertEquals(false, authenticateMethod.invoke(ctx, null, "foo", "blah".getBytes()));
+        BasicWebConsoleSecurityProvider lastResortSp = new BasicWebConsoleSecurityProvider(bc, "foo", "bar", "blah");
+        assertEquals(true, authenticateMethod.invoke(ctx, lastResortSp, "foo", "bar".getBytes()));
+        assertEquals(false, authenticateMethod.invoke(ctx, lastResortSp, "foo", "blah".getBytes()));
 
         WebConsoleSecurityProvider sp = new TestSecurityProvider();
         assertEquals(true, authenticateMethod.invoke(ctx, sp, "xxx", "yyy".getBytes()));
@@ -54,7 +55,7 @@
         Mockito.when(bc.getProperty(OsgiManager.FRAMEWORK_PROP_SECURITY_PROVIDERS)).thenReturn("a");
 
         HttpService svc = Mockito.mock(HttpService.class);
-        OsgiManagerHttpContext ctx = new OsgiManagerHttpContext(bc, svc, null, "foo", "bar", "blah");
+        OsgiManagerHttpContext ctx = new OsgiManagerHttpContext(svc, null, "blah");
 
         Method authenticateMethod = OsgiManagerHttpContext.class.getDeclaredMethod(
                 "authenticate", new Class [] {WebConsoleSecurityProvider.class, String.class, byte[].class});