mirror of https://github.com/apache/lucene.git
SOLR-14440 Cert Auth plugin (#1463)
This commit is contained in:
parent
217c2faa2c
commit
7b289d6185
|
@ -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
|
||||
----------------------
|
||||
|
|
|
@ -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) {
|
||||
} else if (strategy == ResolutionStrategy.IGNORE && existingMetrics.containsKey(fullName)) {
|
||||
continue;
|
||||
} // strategy == ERROR will fail when we try to register later
|
||||
}
|
||||
} // 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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -77,6 +77,23 @@ 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>
|
||||
<div class="login-error" ng-show="statusText || authParamsError || error">
|
||||
|
|
Loading…
Reference in New Issue