You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by md...@apache.org on 2020/05/01 17:47:21 UTC

[lucene-solr] branch master updated: SOLR-14440 Cert Auth plugin (#1463)

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

mdrob pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git


The following commit(s) were added to refs/heads/master by this push:
     new 7b289d61 SOLR-14440 Cert Auth plugin (#1463)
7b289d61 is described below

commit 7b289d6185f30b316d07d5ae5755cfc70c97921d
Author: Mike Drob <md...@apache.org>
AuthorDate: Fri May 1 12:47:12 2020 -0500

    SOLR-14440 Cert Auth plugin (#1463)
---
 solr/CHANGES.txt                                   |  2 +-
 .../org/apache/solr/metrics/SolrMetricManager.java | 47 ++++++++-----
 .../org/apache/solr/security/CertAuthPlugin.java   | 51 ++++++++++++++
 .../apache/solr/servlet/SolrDispatchFilter.java    |  2 +-
 .../apache/solr/security/CertAuthPluginTest.java   | 79 ++++++++++++++++++++++
 .../authentication-and-authorization-plugins.adoc  |  2 +-
 .../src/cert-authentication-plugin.adoc            | 61 +++++++++++++++++
 solr/solr-ref-guide/src/securing-solr.adoc         |  1 +
 solr/webapp/web/js/angular/controllers/login.js    |  2 +-
 solr/webapp/web/partials/login.html                | 17 +++++
 10 files changed, 244 insertions(+), 20 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 3282f7c..50f2614 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -10,7 +10,7 @@ Consult the LUCENE_CHANGES.txt file for additional, low level, changes in this r
 
 New Features
 ---------------------
-(No changes)
+* SOLR-14440: Introduce new Certificate Authentication Plugin to load Principal from certificate subject. (Mike Drob)
 
 Improvements
 ----------------------
diff --git a/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java b/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
index 14843ba..517f8dd 100644
--- a/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
+++ b/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
@@ -527,11 +527,12 @@ public class SolrMetricManager {
    */
   public enum ResolutionStrategy {
     /**
-     * The existing metric will be kept and the new metric will be ignored
+     * The existing metric will be kept and the new metric will be ignored. If no metric exists, then the new metric
+     * will be registered.
      */
     IGNORE,
     /**
-     * The existing metric will be removed and replaced with the new metric
+     * The existing metric will be removed and replaced with the new metric.
      */
     REPLACE,
     /**
@@ -556,13 +557,11 @@ public class SolrMetricManager {
       Map<String, Metric> existingMetrics = metricRegistry.getMetrics();
       for (Map.Entry<String, Metric> entry : metrics.getMetrics().entrySet()) {
         String fullName = mkName(entry.getKey(), metricPath);
-        if (existingMetrics.containsKey(fullName)) {
-          if (strategy == ResolutionStrategy.REPLACE) {
-            metricRegistry.remove(fullName);
-          } else if (strategy == ResolutionStrategy.IGNORE) {
-            continue;
-          } // strategy == ERROR will fail when we try to register later
-        }
+        if (strategy == ResolutionStrategy.REPLACE) {
+          metricRegistry.remove(fullName);
+        } else if (strategy == ResolutionStrategy.IGNORE && existingMetrics.containsKey(fullName)) {
+          continue;
+        } // strategy == ERROR will fail when we try to register
         metricRegistry.register(fullName, entry.getValue());
       }
     }
@@ -685,27 +684,35 @@ public class SolrMetricManager {
   }
 
   /**
+   * @deprecated use {@link #registerMetric(SolrMetricsContext, String, Metric, ResolutionStrategy, String, String...)}
+   */
+  @Deprecated
+  public void registerMetric(SolrMetricsContext context, String registry, Metric metric, boolean force, String metricName, String... metricPath) {
+    registerMetric(context, registry, metric, force ? ResolutionStrategy.REPLACE : ResolutionStrategy.IGNORE, metricName, metricPath);
+  }
+
+  /**
    * Register an instance of {@link Metric}.
    *
    * @param registry   registry name
    * @param metric     metric instance
-   * @param force      if true then an already existing metric with the same name will be replaced.
-   *                   When false and a metric with the same name already exists an exception
-   *                   will be thrown.
+   * @param strategy   the conflict resolution strategy to use if the named metric already exists.
    * @param metricName metric name, either final name or a fully-qualified name
    *                   using dotted notation
    * @param metricPath (optional) additional top-most metric name path elements
    */
-  public void registerMetric(SolrMetricsContext context, String registry, Metric metric, boolean force, String metricName, String... metricPath) {
+  public void registerMetric(SolrMetricsContext context, String registry, Metric metric, ResolutionStrategy strategy, String metricName, String... metricPath) {
     MetricRegistry metricRegistry = registry(registry);
     String fullName = mkName(metricName, metricPath);
     if (context != null) {
       context.registerMetricName(fullName);
     }
     synchronized (metricRegistry) { // prevent race; register() throws if metric is already present
-      if (force) { // must remove any existing one if present
+      if (strategy == ResolutionStrategy.REPLACE) { // must remove any existing one if present
         metricRegistry.remove(fullName);
-      }
+      } else if (strategy == ResolutionStrategy.IGNORE && metricRegistry.getMetrics().containsKey(fullName)) {
+        return;
+      } // strategy == ERROR will fail when we try to register
       metricRegistry.register(fullName, metric);
     }
   }
@@ -740,8 +747,16 @@ public class SolrMetricManager {
     }
   }
 
+  /**
+   * @deprecated use {@link #registerGauge(SolrMetricsContext, String, Gauge, String, ResolutionStrategy, String, String...)}
+   */
+  @Deprecated
   public void registerGauge(SolrMetricsContext context, String registry, Gauge<?> gauge, String tag, boolean force, String metricName, String... metricPath) {
-    registerMetric(context, registry, new GaugeWrapper(gauge, tag), force, metricName, metricPath);
+    registerGauge(context, registry, gauge, tag, force ? ResolutionStrategy.REPLACE : ResolutionStrategy.ERROR, metricName, metricPath);
+  }
+
+  public void registerGauge(SolrMetricsContext context, String registry, Gauge<?> gauge, String tag, ResolutionStrategy strategy, String metricName, String... metricPath) {
+    registerMetric(context, registry, new GaugeWrapper(gauge, tag), strategy, metricName, metricPath);
   }
 
   public int unregisterGauges(String registryName, String tagSegment) {
diff --git a/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java
new file mode 100644
index 0000000..765aa89
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java
@@ -0,0 +1,51 @@
+/*
+ * 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.solr.security;
+
+import org.apache.http.HttpHeaders;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.security.cert.X509Certificate;
+import java.util.Map;
+
+/**
+ * An authentication plugin that sets principal based on the certificate subject
+ */
+public class CertAuthPlugin extends AuthenticationPlugin {
+    @Override
+    public void init(Map<String, Object> pluginConfig) {
+
+    }
+
+    @Override
+    public boolean doAuthenticate(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
+        X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
+        if (certs == null || certs.length == 0) {
+            numMissingCredentials.inc();
+            response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Certificate");
+            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "require certificate");
+            return false;
+        }
+
+        HttpServletRequest wrapped = wrapWithPrincipal(request, certs[0].getSubjectX500Principal());
+        numAuthenticated.inc();
+        filterChain.doFilter(wrapped, response);
+        return true;
+    }
+}
diff --git a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
index eb2f74a..3cee4a1 100644
--- a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
+++ b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
@@ -226,7 +226,7 @@ public class SolrDispatchFilter extends BaseSolrFilter {
           }
         });
       });
-      metricManager.registerGauge(null, registryName, sysprops, metricTag, true, "properties", "system");
+      metricManager.registerGauge(null, registryName, sysprops, metricTag, SolrMetricManager.ResolutionStrategy.IGNORE, "properties", "system");
     } catch (Exception e) {
       log.warn("Error registering JVM metrics", e);
     }
diff --git a/solr/core/src/test/org/apache/solr/security/CertAuthPluginTest.java b/solr/core/src/test/org/apache/solr/security/CertAuthPluginTest.java
new file mode 100644
index 0000000..fb32a21
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/security/CertAuthPluginTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.solr.security;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import javax.security.auth.x500.X500Principal;
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.security.cert.X509Certificate;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class CertAuthPluginTest extends SolrTestCaseJ4 {
+    private CertAuthPlugin plugin;
+
+    @BeforeClass
+    public static void setupMockito() {
+        SolrTestCaseJ4.assumeWorkingMockito();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        plugin = new CertAuthPlugin();
+    }
+
+    @Test
+    public void testAuthenticateOk() throws Exception {
+        X500Principal principal = new X500Principal("CN=NAME");
+        X509Certificate certificate = mock(X509Certificate.class);
+        HttpServletRequest request = mock(HttpServletRequest.class);
+
+        when(certificate.getSubjectX500Principal()).thenReturn(principal);
+        when(request.getAttribute(any())).thenReturn(new X509Certificate[] { certificate });
+
+        FilterChain chain = (req, rsp) -> assertEquals(principal, ((HttpServletRequest) req).getUserPrincipal());
+        assertTrue(plugin.doAuthenticate(request, null, chain));
+
+        assertEquals(1, plugin.numAuthenticated.getCount());
+    }
+
+    @Test
+    public void testAuthenticateMissing() throws Exception {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getAttribute(any())).thenReturn(null);
+
+        HttpServletResponse response = mock(HttpServletResponse.class);
+
+        assertFalse(plugin.doAuthenticate(request, response, null));
+        verify(response).sendError(eq(401), anyString());
+
+        assertEquals(1, plugin.numMissingCredentials.getCount());
+    }
+}
diff --git a/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc b/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc
index b8f9a3d..4e4a8c3 100644
--- a/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc
+++ b/solr/solr-ref-guide/src/authentication-and-authorization-plugins.adoc
@@ -1,5 +1,5 @@
 = Configuring Authentication, Authorization and Audit Logging
-:page-children: basic-authentication-plugin, hadoop-authentication-plugin, kerberos-authentication-plugin,  jwt-authentication-plugin, rule-based-authorization-plugin, audit-logging
+:page-children: basic-authentication-plugin, hadoop-authentication-plugin, kerberos-authentication-plugin,  jwt-authentication-plugin, cert-authentication-plugin, rule-based-authorization-plugin, audit-logging
 // 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
diff --git a/solr/solr-ref-guide/src/cert-authentication-plugin.adoc b/solr/solr-ref-guide/src/cert-authentication-plugin.adoc
new file mode 100644
index 0000000..4b23cc9
--- /dev/null
+++ b/solr/solr-ref-guide/src/cert-authentication-plugin.adoc
@@ -0,0 +1,61 @@
+= Certificate Authentication Plugin
+// 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.
+
+Solr can support extracting the user principal out of the client's certificate with the use of the CertAuthPlugin.
+
+== Enable Certificate Authentication
+
+For Certificate authentication, the `security.json` file must have an `authentication` part which defines the class being used for authentication.
+
+An example `security.json` is shown below:
+
+[source,json]
+----
+{
+ "authentication": {
+  "class":"solr.CertAuthPlugin"
+ }
+}
+----
+
+=== Certificate Validation
+
+Parts of certificate validation, including verifying the trust chain and peer hostname/ip address will be done by the web servlet container before the request ever reaches the authentication plugin.
+These checks are described in the <<enabling-ssl.adoc#enabling-ssl,Enabling SSL>> section.
+
+This plugin provides no additional checking beyond what has been configured via SSL properties.
+
+=== User Principal Extraction
+
+This plugin will configure the user principal for the request based on the X500 subject present in the client certificate.
+Authorization plugins will need to accept and handle the full subject name, for example:
+
+[source]
+----
+CN=Solr User,OU=Engineering,O=Example Inc.,C=US
+----
+
+A list of possible tags that can be present in the subject name is available in https://tools.ietf.org/html/rfc5280#section-4.1.2.4[RFC-5280, Section 4.1.2.4]. Values may have spaces, punctuation, and other characters.
+
+It is best practice to verify the actual contents of certificates issued by your trusted certificate authority before configuring authorization based on the contents.
+
+== Using Certificate Auth with Clients (including SolrJ)
+
+With certificate authentication enabled, all client requests must include a valid certificate.
+This is identical to the <<enabling-ssl.adoc#example-client-actions,client requirements>> when using SSL.
+
diff --git a/solr/solr-ref-guide/src/securing-solr.adoc b/solr/solr-ref-guide/src/securing-solr.adoc
index c3e17e9..1d3baee 100644
--- a/solr/solr-ref-guide/src/securing-solr.adoc
+++ b/solr/solr-ref-guide/src/securing-solr.adoc
@@ -44,6 +44,7 @@ Authentication makes sure you know the identity of your users. The authenticatio
 * <<basic-authentication-plugin.adoc#basic-authentication-plugin,Basic Authentication Plugin>>
 * <<hadoop-authentication-plugin.adoc#hadoop-authentication-plugin,Hadoop Authentication Plugin>>
 * <<jwt-authentication-plugin.adoc#jwt-authentication-plugin,JWT Authentication Plugin>>
+* <<cert-authentication-plugin.adoc#cert-authentication-plugin,Certificate Authentication Plugin>>
 // end::list-of-authentication-plugins[]
 
 === Authorization Plugins
diff --git a/solr/webapp/web/js/angular/controllers/login.js b/solr/webapp/web/js/angular/controllers/login.js
index 8127c6f..b76ec1f 100644
--- a/solr/webapp/web/js/angular/controllers/login.js
+++ b/solr/webapp/web/js/angular/controllers/login.js
@@ -47,7 +47,7 @@ solrAdminApp.controller('LoginController',
           sessionStorage.setItem("auth.scheme", authScheme);
         }
 
-        var supportedSchemes = ['Basic', 'Bearer', 'Negotiate'];
+        var supportedSchemes = ['Basic', 'Bearer', 'Negotiate', 'Certificate'];
         $scope.authSchemeSupported = supportedSchemes.includes(authScheme);
 
         if (authScheme === 'Bearer') {
diff --git a/solr/webapp/web/partials/login.html b/solr/webapp/web/partials/login.html
index 29c8c71..c21f262 100644
--- a/solr/webapp/web/partials/login.html
+++ b/solr/webapp/web/partials/login.html
@@ -76,6 +76,23 @@ limitations under the License.
 WWW-Authenticate: {{wwwAuthHeader}}</pre>
     <hr/>
   </div>
+
+  <div ng-show="authScheme === 'Certificate'">
+    <h1>Certificate Authentication</h1>
+    <p>Your browser did not provide the required information to authenticate using PKI Certificates.
+      Please check that your computer has a valid PKI certificate for communicating with Solr,
+      and that your browser is properly configured to provide that certificate when required.
+      For more information, consult
+      <a href="https://lucene.apache.org/solr/guide/cert-authentication-plugin.html">
+        Solr's Certificate Authentication documentation
+      </a>.
+    </p>
+    The response from the server was:
+    <hr/>
+    <pre>HTTP 401 {{statusText}}
+WWW-Authenticate: {{wwwAuthHeader}}</pre>
+    <hr/>
+  </div>
   
   <div ng-show="authScheme === 'Bearer'">
     <h1>OpenID Connect (JWT) authentication</h1>