SOLR-14440 Cert Auth plugin (#1463)

This commit is contained in:
Mike Drob 2020-05-01 12:47:12 -05:00 committed by GitHub
parent 217c2faa2c
commit 7b289d6185
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 244 additions and 20 deletions

View File

@ -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
----------------------

View File

@ -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());
}
}
@ -684,28 +683,36 @@ public class SolrMetricManager {
return registry(registry).histogram(name, histogramSupplier);
}
/**
* @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) {

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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') {

View File

@ -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>