diff --git a/changelog/unreleased/SOLR-18270-cert-auth-plugin-request-attribute-param.yml b/changelog/unreleased/SOLR-18270-cert-auth-plugin-request-attribute-param.yml new file mode 100644 index 00000000000..78100a50f8a --- /dev/null +++ b/changelog/unreleased/SOLR-18270-cert-auth-plugin-request-attribute-param.yml @@ -0,0 +1,8 @@ +title: Provide a parameter to be able to configure the lookup request attribute used for SSL authentication +type: fixed +authors: + name: Jean-Marie HEITZ + nick: heitzjm +links: + - name: SOLR-18270 + url: https://issues.apache.org/jira/projects/SOLR/issues/SOLR-18270 diff --git a/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java index 091191fd409..50bf1fa50c0 100644 --- a/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java @@ -38,11 +38,15 @@ public class CertAuthPlugin extends AuthenticationPlugin { private static final String PARAM_PRINCIPAL_RESOLVER = "principalResolver"; private static final String PARAM_CLASS = "class"; private static final String PARAM_PARAMS = "params"; + private static final String PARAM_REQUEST_ATTRIBUTE= "requestAttribute"; + + private static final String DEFAULT_REQUEST_ATTRIBUTE="jakarta.servlet.request.X509Certificate"; private static final CertPrincipalResolver DEFAULT_PRINCIPAL_RESOLVER = certificate -> certificate.getSubjectX500Principal(); protected final CoreContainer coreContainer; private CertPrincipalResolver principalResolver; + private String requestAttribute; public CertAuthPlugin() { this(null); @@ -52,6 +56,18 @@ public CertAuthPlugin(CoreContainer coreContainer) { this.coreContainer = coreContainer; } + private String computeRequestAttribute(Map pluginConfig) + { + String ret=DEFAULT_REQUEST_ATTRIBUTE; + Object configuredRequestAttribute= pluginConfig.get(PARAM_REQUEST_ATTRIBUTE); + if(configuredRequestAttribute!=null + && configuredRequestAttribute instanceof String + && !StrUtils.isNullOrEmpty((String) configuredRequestAttribute)) { + ret=(String)(configuredRequestAttribute); + } + return ret; + } + @Override public void init(Map pluginConfig) { principalResolver = @@ -61,6 +77,9 @@ public void init(Map pluginConfig) { CertPrincipalResolver.class, DEFAULT_PRINCIPAL_RESOLVER, "principalResolver"); + requestAttribute = computeRequestAttribute(pluginConfig); + + log.debug("Using {} as request attribute",requestAttribute); } @SuppressWarnings("unchecked") @@ -102,7 +121,7 @@ public boolean doAuthenticate( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception { X509Certificate[] certs = - (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); + (X509Certificate[]) request.getAttribute(requestAttribute); if (certs == null || certs.length == 0) { return sendError(response, "require certificate"); } diff --git a/solr/core/src/test/org/apache/solr/security/CertAuthPluginRequestAttributeTest.java b/solr/core/src/test/org/apache/solr/security/CertAuthPluginRequestAttributeTest.java new file mode 100644 index 00000000000..3e80e24a065 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/security/CertAuthPluginRequestAttributeTest.java @@ -0,0 +1,149 @@ +/* + * 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 com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import io.opentelemetry.api.common.Attributes; +import io.prometheus.metrics.model.snapshots.CounterSnapshot; +import io.prometheus.metrics.model.snapshots.Labels; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.core.SolrCore; +import org.apache.solr.metrics.SolrMetricsContext; +import org.apache.solr.util.SolrMetricTestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runners.Parameterized; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import javax.security.auth.x500.X500Principal; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +public class CertAuthPluginRequestAttributeTest extends SolrTestCaseJ4 { + private CertAuthPlugin plugin; + private SolrMetricsContext solrMetricsContext; + @Parameterized.Parameter(0) + public Map initMap; + @Parameterized.Parameter(1) + public String expectedAttributeLookup; + + @ParametersFactory() + public static Collection data() { + String defaultValue = "jakarta.servlet.request.X509Certificate"; + String requestAttributeParam = "requestAttribute"; + HashMap mapWithNullValue=new HashMap<>(); + mapWithNullValue.put(requestAttributeParam,null); + return Arrays.asList(new Object[][] { + {Map.of(), defaultValue}, + {mapWithNullValue, defaultValue}, + {Map.of(requestAttributeParam, ""), defaultValue}, + { + Map.of(requestAttributeParam, "javax.servlet.request.X509Certificate"), + "javax.servlet.request.X509Certificate"} + }); + } + + public CertAuthPluginRequestAttributeTest(Map initMap, String expectedAttributeLookup) { + this.initMap = initMap; + this.expectedAttributeLookup = expectedAttributeLookup; + } + + @BeforeClass + public static void setupMockito() { + SolrTestCaseJ4.assumeWorkingMockito(); + } + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + CoreContainer coreContainer = + SolrTestCaseJ4.createCoreContainer( + "collection1", "data", "solrconfig-basic.xml", "schema.xml"); + String registryName = "solr.core.collection1"; + plugin = new CertAuthPlugin(); + solrMetricsContext = new SolrMetricsContext(coreContainer.getMetricManager(), registryName); + plugin.initializeMetrics(solrMetricsContext, Attributes.empty()); + plugin.init(initMap); + + } + + @Override + @After + public void tearDown() throws Exception { + plugin.close(); + solrMetricsContext.close(); + deleteCore(); + super.tearDown(); + } + + @Test + public void testAuthenticateOkAndSpyRequestedAttributes() throws Exception { + X500Principal principal = new X500Principal("CN=NAME"); + final X509Certificate certificate = mock(X509Certificate.class); + HttpServletRequest request = mock(HttpServletRequest.class); + final ArrayList requestedAttributes=new ArrayList<>(); + when(certificate.getSubjectX500Principal()).thenReturn(principal); + when(request.getAttribute(any())).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + requestedAttributes.add(invocation.getArgument(0)); + return new X509Certificate[] {certificate}; + } + }); + + + FilterChain chain = + (req, rsp) -> assertEquals(principal, ((HttpServletRequest) req).getUserPrincipal()); + assertTrue(plugin.doAuthenticate(request, null, chain)); + assertEquals(new String[]{expectedAttributeLookup},requestedAttributes.toArray()); + + Labels labels = + Labels.of( + "otel_scope_name", + "org.apache.solr", + "category", + "SECURITY", + "plugin_name", + "CertAuthPlugin"); + long missingCredentialsCount = + getLongMetricValue("solr_authentication_num_authenticated", labels); + assertEquals(1L, missingCredentialsCount); + } + + private long getLongMetricValue(String metricName, Labels labels) { + SolrCore core = h.getCore(); + CounterSnapshot.CounterDataPointSnapshot metric = + SolrMetricTestUtils.getCounterDatapoint(core, metricName, labels); + return (metric != null) ? (long) metric.getValue() : 0L; + } +} diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cert-authentication-plugin.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cert-authentication-plugin.adoc index 318091a6106..2ee7f5b3046 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cert-authentication-plugin.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cert-authentication-plugin.adoc @@ -42,6 +42,26 @@ This plugin provides no additional checking beyond what has been configured via It is best practice to verify the actual contents of certificates issued by your trusted certificate authority before configuring authorization based on the contents. +=== Certificate Lookup + +It is expected that the web application server adds an attribute to the request to specify the client certificate(s). + +Historically, this attribute name was `javax.servlet.request.X509Certificate`. However, the attribute name changed in some web application servers to `jakarta.servlet.request.X509Certificate` with the migration from Java EE to Jakarta EE. Therefore, a `requestAttribute` has been introduced to let administrators configure the attribute name the CertAuthPlugin should look up. + +The default value is `jakarta.servlet.request.X509Certificate`. + +An example : + +[source,json] +---- +{ + "authentication": { + "class":"solr.CertAuthPlugin", + "requestAttribute":"javax.servlet.request.X509Certificate" + } +} +---- + == User Principal Extraction This plugin will configure the user `principal` for the request based on the contents of the client certificate. Solr provides three methods for extracting the `principal`: