From 0ed52751ecca8b127c0ddff844e0bf947a53cd81 Mon Sep 17 00:00:00 2001 From: heitzjm Date: Fri, 29 May 2026 02:24:01 +0200 Subject: [PATCH 1/4] CertAuthPlugin : add RequestAttributeParam --- .../org/apache/solr/security/CertAuthPlugin.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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 091191fd4092..04bdacc7cfe2 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); @@ -61,6 +65,14 @@ public void init(Map pluginConfig) { CertPrincipalResolver.class, DEFAULT_PRINCIPAL_RESOLVER, "principalResolver"); + requestAttribute = DEFAULT_REQUEST_ATTRIBUTE; + Object configuredRequestAttribute= pluginConfig.get(PARAM_REQUEST_ATTRIBUTE); + if(configuredRequestAttribute!=null + && configuredRequestAttribute instanceof String + && !StrUtils.isNullOrEmpty((String) configuredRequestAttribute)) { + requestAttribute=(String)(configuredRequestAttribute); + } + log.debug("Using {} as request attribute",requestAttribute); } @SuppressWarnings("unchecked") @@ -102,7 +114,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"); } From 96514dd7d73ed6359eb5c860a149b9598dbb9c35 Mon Sep 17 00:00:00 2001 From: heitzjm Date: Fri, 29 May 2026 19:37:09 +0200 Subject: [PATCH 2/4] SOLR-18270 : certAuthPlugin : requestAttribute documentation --- .../pages/cert-authentication-plugin.adoc | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 318091a61064..2ee7f5b30465 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`: From 6921a5ec081a17aa61f90976198e50c0a0394eab Mon Sep 17 00:00:00 2001 From: heitzjm Date: Fri, 29 May 2026 19:37:51 +0200 Subject: [PATCH 3/4] SOLR-18270 : changelog --- ...OLR-18270-cert-auth-plugin-request-attribute-param.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 changelog/unreleased/SOLR-18270-cert-auth-plugin-request-attribute-param.yml 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 000000000000..78100a50f8aa --- /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 From 4405dea20821a15712bd982444abc1e4d9bfc2e9 Mon Sep 17 00:00:00 2001 From: heitzjm Date: Sat, 30 May 2026 17:19:17 +0200 Subject: [PATCH 4/4] SOLR-18270 : test implementation --- .../apache/solr/security/CertAuthPlugin.java | 21 ++- .../CertAuthPluginRequestAttributeTest.java | 149 ++++++++++++++++++ 2 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 solr/core/src/test/org/apache/solr/security/CertAuthPluginRequestAttributeTest.java 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 04bdacc7cfe2..50bf1fa50c09 100644 --- a/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/CertAuthPlugin.java @@ -56,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 = @@ -65,13 +77,8 @@ public void init(Map pluginConfig) { CertPrincipalResolver.class, DEFAULT_PRINCIPAL_RESOLVER, "principalResolver"); - requestAttribute = DEFAULT_REQUEST_ATTRIBUTE; - Object configuredRequestAttribute= pluginConfig.get(PARAM_REQUEST_ATTRIBUTE); - if(configuredRequestAttribute!=null - && configuredRequestAttribute instanceof String - && !StrUtils.isNullOrEmpty((String) configuredRequestAttribute)) { - requestAttribute=(String)(configuredRequestAttribute); - } + requestAttribute = computeRequestAttribute(pluginConfig); + log.debug("Using {} as request attribute",requestAttribute); } 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 000000000000..3e80e24a065f --- /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; + } +}