This is an automated email from the ASF dual-hosted git repository.
gus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new 2cd2e4e5f83 SOLR-18048 Move authentication into a Servlet Filter
(#4120)
2cd2e4e5f83 is described below
commit 2cd2e4e5f832c8522150c65ed30f0bf4e584ad3f
Author: Gus Heck <[email protected]>
AuthorDate: Mon Mar 16 23:01:11 2026 -0400
SOLR-18048 Move authentication into a Servlet Filter (#4120)
---
changelog/unreleased/SOLR-18048.yml | 8 +
.../java/org/apache/solr/core/CoreContainer.java | 16 ++
.../apache/solr/security/AuthenticationPlugin.java | 6 +-
.../apache/solr/security/AuthorizationUtils.java | 32 +---
.../apache/solr/servlet/AuthenticationFilter.java | 203 +++++++++++++++++++++
.../java/org/apache/solr/servlet/HttpSolrCall.java | 64 ++-----
.../solr/servlet/RequiredSolrRequestFilter.java | 10 +-
.../solr/servlet/SolrAuthenticationException.java | 19 --
.../apache/solr/servlet/SolrDispatchFilter.java | 175 +-----------------
.../org/apache/solr/embedded/JettySolrRunner.java | 9 +-
solr/webapp/web/WEB-INF/web.xml | 13 +-
11 files changed, 290 insertions(+), 265 deletions(-)
diff --git a/changelog/unreleased/SOLR-18048.yml
b/changelog/unreleased/SOLR-18048.yml
new file mode 100644
index 00000000000..834d1905d7e
--- /dev/null
+++ b/changelog/unreleased/SOLR-18048.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Authentication checks have been moved to a Servlet Filter
+type: other # added, changed, fixed, deprecated, removed, dependency_update,
security, other
+authors:
+ - name: Gus Heck
+links:
+ - name: SOLR-18048
+ url: https://issues.apache.org/jira/browse/SOLR-18048
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index f6bf1dfb36a..a19f77a2e23 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -147,6 +147,7 @@ import org.apache.solr.search.SolrCache;
import org.apache.solr.search.SolrFieldCacheBean;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.security.AllowListUrlChecker;
+import org.apache.solr.security.AuditEvent;
import org.apache.solr.security.AuditLoggerPlugin;
import org.apache.solr.security.AuthenticationPlugin;
import org.apache.solr.security.AuthorizationPlugin;
@@ -2510,4 +2511,19 @@ public class CoreContainer {
}
});
}
+
+ /**
+ * Audit an event if our audit plugin is installed and wants to audit this
type of event.
+ *
+ * @param event the event to audit.
+ * @param eventType a Supplier to defer event creation and avoid gc load
when auditing is not
+ * enabled. Lambdas are preferred for this since they are easily inlined.
+ */
+ public void audit(Supplier<AuditEvent> event, AuditEvent.EventType
eventType) {
+ if (getAuditLoggerPlugin() != null &&
getAuditLoggerPlugin().shouldLog(eventType)) {
+ // The lambda should get optimized out, and produce no GC load:
+ //
https://medium.com/@reetesh043/how-lambda-expressions-work-internally-in-java-f2a6f0e0bc68
+ getAuditLoggerPlugin().doAudit(event.get());
+ }
+ }
}
diff --git
a/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java
b/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java
index 99c1b091eac..aaa24087ff8 100644
--- a/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java
@@ -69,8 +69,8 @@ public abstract class AuthenticationPlugin implements
SolrInfoBean {
* @param request the http request
* @param response the http response
* @param filterChain the servlet filter chain
- * @return false if the request not be processed by Solr (not continue),
i.e. the response and
- * status code have already been sent.
+ * @return false if the request should not be processed by Solr (not
continue), i.e. the response
+ * and status code have already been sent.
* @throws Exception any exception thrown during the authentication, e.g.
* PrivilegedActionException
*/
@@ -79,7 +79,7 @@ public abstract class AuthenticationPlugin implements
SolrInfoBean {
throws Exception;
/**
- * This method is called by SolrDispatchFilter in order to initiate
authentication. It does some
+ * This method is called by AuthenticationFilter in order to initiate
authentication. It does some
* standard metrics counting.
*/
public final boolean authenticate(
diff --git
a/solr/core/src/java/org/apache/solr/security/AuthorizationUtils.java
b/solr/core/src/java/org/apache/solr/security/AuthorizationUtils.java
index a316dbdf65d..3ba6b07c557 100644
--- a/solr/core/src/java/org/apache/solr/security/AuthorizationUtils.java
+++ b/solr/core/src/java/org/apache/solr/security/AuthorizationUtils.java
@@ -21,7 +21,9 @@ import static
org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
import static
org.apache.solr.common.params.CollectionParams.CollectionAction.CREATE;
import static
org.apache.solr.common.params.CollectionParams.CollectionAction.DELETE;
import static
org.apache.solr.common.params.CollectionParams.CollectionAction.RELOAD;
-import static org.apache.solr.servlet.HttpSolrCall.shouldAudit;
+import static org.apache.solr.security.AuditEvent.EventType.ERROR;
+import static org.apache.solr.security.AuditEvent.EventType.REJECTED;
+import static org.apache.solr.security.AuditEvent.EventType.UNAUTHORIZED;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -73,11 +75,7 @@ public class AuthorizationUtils {
servletReq.getHeader("Authorization"),
servletReq.getUserPrincipal());
}
- if (shouldAudit(cores, AuditEvent.EventType.REJECTED)) {
- cores
- .getAuditLoggerPlugin()
- .doAudit(new AuditEvent(AuditEvent.EventType.REJECTED, servletReq,
context));
- }
+ cores.audit(() -> new AuditEvent(REJECTED, servletReq, context),
REJECTED);
return new AuthorizationFailure(
statusCode, "Authentication failed, Response code: " + statusCode);
}
@@ -89,32 +87,22 @@ public class AuthorizationUtils {
context,
authResponse.getMessage()); // nowarn
}
- if (shouldAudit(cores, AuditEvent.EventType.UNAUTHORIZED)) {
- cores
- .getAuditLoggerPlugin()
- .doAudit(new AuditEvent(AuditEvent.EventType.UNAUTHORIZED,
servletReq, context));
- }
+ cores.audit(() -> new AuditEvent(UNAUTHORIZED, servletReq, context),
UNAUTHORIZED);
return new AuthorizationFailure(
statusCode, "Unauthorized request, Response code: " + statusCode);
}
if (!(statusCode == HttpStatus.ACCEPTED_202) && !(statusCode ==
HttpStatus.OK_200)) {
log.warn(
"ERROR {} during authentication: {}", statusCode,
authResponse.getMessage()); // nowarn
- if (shouldAudit(cores, AuditEvent.EventType.ERROR)) {
- cores
- .getAuditLoggerPlugin()
- .doAudit(new AuditEvent(AuditEvent.EventType.ERROR, servletReq,
context));
- }
+ cores.audit(() -> new AuditEvent(ERROR, servletReq, context), ERROR);
return new AuthorizationFailure(
statusCode, "ERROR during authorization, Response code: " +
statusCode);
}
-
// No failures! Audit if necessary and return
- if (shouldAudit(cores, AuditEvent.EventType.AUTHORIZED)) {
- cores
- .getAuditLoggerPlugin()
- .doAudit(new AuditEvent(AuditEvent.EventType.AUTHORIZED, servletReq,
context));
- }
+ cores.audit(
+ () -> new AuditEvent(AuditEvent.EventType.AUTHORIZED, servletReq,
context),
+ AuditEvent.EventType.AUTHORIZED);
+
return null;
}
diff --git
a/solr/core/src/java/org/apache/solr/servlet/AuthenticationFilter.java
b/solr/core/src/java/org/apache/solr/servlet/AuthenticationFilter.java
new file mode 100644
index 00000000000..e842d555d28
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/servlet/AuthenticationFilter.java
@@ -0,0 +1,203 @@
+/*
+ * 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.servlet;
+
+import static org.apache.solr.common.SolrException.ErrorCode.SERVER_ERROR;
+import static org.apache.solr.security.AuditEvent.EventType.ANONYMOUS;
+import static org.apache.solr.security.AuditEvent.EventType.AUTHENTICATED;
+import static org.apache.solr.security.AuditEvent.EventType.REJECTED;
+
+import io.opentelemetry.api.trace.Span;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.security.AuditEvent;
+import org.apache.solr.security.AuthenticationPlugin;
+import org.apache.solr.security.PKIAuthenticationPlugin;
+import org.apache.solr.security.PublicKeyHandler;
+import org.apache.solr.util.tracing.TraceUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A servlet filter to handle authentication. Anything this filter wraps that
needs to be served
+ * without authentication (such as UI) must be resolved and returned by a
filter preceding this one,
+ * typically by forwarding to the default servlet or another servlet. Also,
any tracing, auditing
+ * and ratelimiting decisions or setup usually want to be resolved ahead of
this filter. Similarly,
+ * any potentially expensive operations should occur after this filter to
eliminate denial of
+ * service by unauthenticated users.
+ */
+public class AuthenticationFilter extends CoreContainerAwareHttpFilter {
+
+ private static final Logger log =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ public static final String PLUGIN_DID_NOT_SET_ERROR_STATUS =
+ "{} threw SolrAuthenticationException without setting request status >=
400";
+
+ @Override
+ public void init(FilterConfig config) throws ServletException {
+ super.init(config);
+ }
+
+ @Override
+ protected void doFilter(HttpServletRequest req, HttpServletResponse res,
FilterChain chain)
+ throws IOException, ServletException {
+
+ CoreContainer cc = getCores();
+ AuthenticationPlugin authenticationPlugin = cc.getAuthenticationPlugin();
+ String requestPath = ServletUtils.getPathAfterContext(req);
+
+ /////////////////////////////////////////////////////////
+ //////// Check cases where auth is not required. ////////
+ /////////////////////////////////////////////////////////
+
+ // Is authentication configured?
+ if (authenticationPlugin == null) {
+ cc.audit(() -> new AuditEvent(ANONYMOUS, req), ANONYMOUS);
+ chain.doFilter(req, res);
+ return;
+ }
+
+ // /admin/info/key must be always open. see SOLR-9188
+ if (PublicKeyHandler.PATH.equals(requestPath)) {
+ log.debug("Pass through PKI authentication endpoint");
+ chain.doFilter(req, res);
+ return;
+ }
+
+ if (isAdminUI(requestPath)) {
+ log.debug("Pass through Admin UI entry point");
+ chain.doFilter(req, res);
+ return;
+ }
+
+ /////////////////////////////////////////////////////////////////
+ //////// if we make it here, authentication is required. ////////
+ /////////////////////////////////////////////////////////////////
+
+ // internode (internal) requests have their own PKI auth plugin
+ if (isInternodePKI(req, cc)) {
+ authenticationPlugin = cc.getPkiAuthenticationSecurityBuilder();
+ }
+
+ boolean authSuccess = false;
+ try {
+ authSuccess =
+ authenticate(req, res, new AuditSuccessChainWrapper(cc, chain),
authenticationPlugin);
+ } finally {
+ if (!authSuccess) {
+ cc.audit(() -> new AuditEvent(REJECTED, req), REJECTED);
+ res.flushBuffer();
+ if (res.getStatus() < 400) {
+ log.error(PLUGIN_DID_NOT_SET_ERROR_STATUS,
authenticationPlugin.getClass());
+ res.sendError(401, "Authentication Plugin rejected credentials.");
+ }
+ }
+ }
+ }
+
+ /**
+ * The plugin called by this method will call doFilter(req, res) for us
(which is why we pass in
+ * the filter chain object as a parameter).
+ */
+ private boolean authenticate(
+ HttpServletRequest req,
+ HttpServletResponse res,
+ FilterChain chain,
+ AuthenticationPlugin authenticationPlugin) {
+ try {
+
+ // It is imperative that authentication plugins obey the contract in the
javadoc for
+ // org.apache.solr.security.AuthenticationPlugin.doAuthenticate, and
either return
+ // false or throw an exception if they cannot validate the user's
credentials.
+ // Any plugin that doesn't do this is broken and should be fixed.
+
+ logAuthAttempt(req);
+ return authenticationPlugin.authenticate(req, res, chain);
+ } catch (Exception e) {
+ log.info("Error authenticating", e);
+ throw new SolrException(SERVER_ERROR, "Error during request
authentication, ", e);
+ }
+ }
+
+ private static boolean isAdminUI(String requestPath) {
+ return "/solr/".equals(requestPath) || "/".equals(requestPath);
+ }
+
+ private boolean isInternodePKI(HttpServletRequest req, CoreContainer cores) {
+ String header = req.getHeader(PKIAuthenticationPlugin.HEADER);
+ String headerV2 = req.getHeader(PKIAuthenticationPlugin.HEADER_V2);
+ return (header != null || headerV2 != null)
+ && cores.getPkiAuthenticationSecurityBuilder() != null;
+ }
+
+ private void logAuthAttempt(HttpServletRequest req) {
+ // moved this to a method because spotless formatting is so horrible, and
makes the log
+ // message look like a big deal... but it's just taking up space
+ if (log.isDebugEnabled()) {
+ log.debug(
+ "Request to authenticate: {}, domain: {}, port: {}",
+ req,
+ req.getLocalName(),
+ req.getLocalPort());
+ }
+ }
+
+ @SuppressWarnings("ClassCanBeRecord")
+ private static class AuditSuccessChainWrapper implements FilterChain {
+
+ private final FilterChain chain;
+ private final CoreContainer cc;
+
+ private AuditSuccessChainWrapper(CoreContainer cc, FilterChain chain) {
+ this.cc = cc;
+ this.chain = chain;
+ }
+
+ @Override
+ public void doFilter(ServletRequest rq, ServletResponse rsp)
+ throws IOException, ServletException {
+ // this is a hack. The authentication plugin should accept a callback
+ // to be executed before doFilter is called if authentication succeeds
+ cc.audit(() -> new AuditEvent(AUTHENTICATED, (HttpServletRequest) rq),
AUTHENTICATED);
+ Span span = TraceUtils.getSpan((HttpServletRequest) rq);
+ setPrincipalForTracing((HttpServletRequest) rq, span);
+ chain.doFilter(rq, rsp);
+ }
+
+ private void setPrincipalForTracing(HttpServletRequest request, Span span)
{
+ if (log.isDebugEnabled()) {
+ log.debug("User principal: {}", request.getUserPrincipal());
+ }
+ final String principalName;
+ if (request.getUserPrincipal() != null) {
+ principalName = request.getUserPrincipal().getName();
+ } else {
+ principalName = null;
+ }
+ TraceUtils.setUser(span, String.valueOf(principalName));
+ }
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
index 5e2fc97d163..81d439dd2ec 100644
--- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
+++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
@@ -19,6 +19,8 @@ package org.apache.solr.servlet;
import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
import static org.apache.solr.common.cloud.ZkStateReader.CORE_NAME_PROP;
import static org.apache.solr.common.cloud.ZkStateReader.NODE_NAME_PROP;
+import static org.apache.solr.security.AuditEvent.EventType.COMPLETED;
+import static org.apache.solr.security.AuditEvent.EventType.ERROR;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.ADMIN;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.FORWARD;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.PASSTHROUGH;
@@ -422,9 +424,7 @@ public class HttpSolrCall {
if (solrDispatchFilter.abortErrorMessage != null) {
sendError(500, solrDispatchFilter.abortErrorMessage);
- if (shouldAudit(EventType.ERROR)) {
- cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.ERROR,
getReq()));
- }
+ cores.audit(() -> new AuditEvent(ERROR, getReq()), ERROR);
return RETURN;
}
@@ -482,21 +482,11 @@ public class HttpSolrCall {
SolrRequestInfo.setRequestInfo(new SolrRequestInfo(solrReq,
solrRsp, action));
mustClearSolrRequestInfo = true;
executeCoreRequest(solrRsp);
- if (shouldAudit(cores)) {
- EventType eventType =
- solrRsp.getException() == null ? EventType.COMPLETED :
EventType.ERROR;
- if (shouldAudit(cores, eventType)) {
- cores
- .getAuditLoggerPlugin()
- .doAudit(
- new AuditEvent(
- eventType,
- req,
- getAuthCtx(),
- solrReq.getRequestTimer().getTime(),
- solrRsp.getException()));
- }
- }
+ Exception exception = solrRsp.getException();
+ EventType eventType = exception == null ? COMPLETED : ERROR;
+ double time = solrReq.getRequestTimer().getTime();
+ cores.audit(
+ () -> new AuditEvent(eventType, req, getAuthCtx(), time,
exception), eventType);
HttpCacheHeaderUtil.checkHttpCachingVeto(solrRsp, resp, reqMethod);
Iterator<Map.Entry<String, String>> headers =
solrRsp.httpHeaders();
while (headers.hasNext()) {
@@ -513,9 +503,7 @@ public class HttpSolrCall {
return action;
}
} catch (Throwable ex) {
- if (shouldAudit(EventType.ERROR)) {
- cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.ERROR,
ex, req));
- }
+ cores.audit(() -> new AuditEvent(ERROR, ex, req), ERROR);
sendError(ex);
// walk the entire cause chain to search for an Error
Throwable t = ex;
@@ -583,22 +571,6 @@ public class HttpSolrCall {
return coreOrColName;
}
- public boolean shouldAudit() {
- return shouldAudit(cores);
- }
-
- public boolean shouldAudit(AuditEvent.EventType eventType) {
- return shouldAudit(cores, eventType);
- }
-
- public static boolean shouldAudit(CoreContainer cores) {
- return cores.getAuditLoggerPlugin() != null;
- }
-
- public static boolean shouldAudit(CoreContainer cores, AuditEvent.EventType
eventType) {
- return shouldAudit(cores) &&
cores.getAuditLoggerPlugin().shouldLog(eventType);
- }
-
private boolean shouldAuthorize() {
if (PublicKeyHandler.PATH.equals(path)) return false;
// admin/info/key is the path where public key is exposed . it is always
unsecured
@@ -741,20 +713,10 @@ public class HttpSolrCall {
ResponseWritersRegistry.getWriter(solrReq.getParams().get(CommonParams.WT));
if (respWriter == null) respWriter = getResponseWriter();
writeResponse(solrResp, respWriter, Method.getMethod(req.getMethod()));
- if (shouldAudit()) {
- EventType eventType = solrResp.getException() == null ?
EventType.COMPLETED : EventType.ERROR;
- if (shouldAudit(eventType)) {
- cores
- .getAuditLoggerPlugin()
- .doAudit(
- new AuditEvent(
- eventType,
- req,
- getAuthCtx(),
- solrReq.getRequestTimer().getTime(),
- solrResp.getException()));
- }
- }
+ Exception ex = solrResp.getException();
+ EventType eventType = ex == null ? COMPLETED : ERROR;
+ double time = solrReq.getRequestTimer().getTime();
+ cores.audit(() -> new AuditEvent(eventType, req, getAuthCtx(), time, ex),
eventType);
}
/**
diff --git
a/solr/core/src/java/org/apache/solr/servlet/RequiredSolrRequestFilter.java
b/solr/core/src/java/org/apache/solr/servlet/RequiredSolrRequestFilter.java
index 50ec9decdc9..7cdddfb2c5b 100644
--- a/solr/core/src/java/org/apache/solr/servlet/RequiredSolrRequestFilter.java
+++ b/solr/core/src/java/org/apache/solr/servlet/RequiredSolrRequestFilter.java
@@ -16,12 +16,15 @@
*/
package org.apache.solr.servlet;
+import static org.apache.solr.servlet.ServletUtils.closeShield;
+
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
+import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.SuppressForbidden;
import org.apache.solr.logging.MDCLoggingContext;
import org.apache.solr.logging.MDCSnapshot;
@@ -72,7 +75,12 @@ public class RequiredSolrRequestFilter extends
CoreContainerAwareHttpFilter {
// put the core container in request attribute
// This is required for the LoadAdminUiServlet class. Removing it will
cause 404
req.setAttribute(CORE_CONTAINER_REQUEST_ATTRIBUTE, getCores());
- chain.doFilter(req, res);
+
+ // we want to prevent any attempts to close our request or response
prematurely
+ chain.doFilter(closeShield(req), closeShield(res));
+ } catch (SolrException e) {
+ // this must never escape without setting the code and reporting the
message.
+ res.sendError(e.code(), e.getMessage());
} finally {
// cleanups for above stuff
MDCLoggingContext.reset();
diff --git
a/solr/core/src/java/org/apache/solr/servlet/SolrAuthenticationException.java
b/solr/core/src/java/org/apache/solr/servlet/SolrAuthenticationException.java
deleted file mode 100644
index 04db031bb06..00000000000
---
a/solr/core/src/java/org/apache/solr/servlet/SolrAuthenticationException.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * 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.servlet;
-
-public class SolrAuthenticationException extends Exception {}
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 0d0bef9b6ce..300c54ac562 100644
--- a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
+++ b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
@@ -16,7 +16,6 @@
*/
package org.apache.solr.servlet;
-import static org.apache.solr.servlet.ServletUtils.closeShield;
import static org.apache.solr.util.tracing.TraceUtils.getSpan;
import jakarta.servlet.FilterChain;
@@ -27,8 +26,6 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
import org.apache.solr.api.V2HttpCall;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
@@ -36,25 +33,15 @@ import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.NodeRoles;
import org.apache.solr.handler.api.V2ApiUtils;
-import org.apache.solr.security.AuditEvent;
-import org.apache.solr.security.AuditEvent.EventType;
-import org.apache.solr.security.AuthenticationPlugin;
-import org.apache.solr.security.PKIAuthenticationPlugin;
-import org.apache.solr.security.PublicKeyHandler;
-import org.apache.solr.util.tracing.TraceUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
- * This filter looks at the incoming URL maps them to handlers defined in
solrconfig.xml
+ * This filter instantiates an instance of {@link HttpSolrCall}, invokes it
and then based on the
+ * action returned handles retry/forward/passthrough dispatch decisions.
*
* @since solr 1.2
*/
-// todo: get rid of this class entirely! Request dispatch is the container's
responsibility. Much of
-// what we have here should be several separate but composable servlet
Filters, wrapping multiple
-// servlets that are more focused in scope. This should become possible now
that we have a
-// ServletContextListener for startup/shutdown of CoreContainer that sets up a
service from which
-// things like CoreContainer can be requested. (or better yet injected)
public class SolrDispatchFilter extends CoreContainerAwareHttpFilter {
private static final Logger log =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@@ -108,7 +95,6 @@ public class SolrDispatchFilter extends
CoreContainerAwareHttpFilter {
if (log.isTraceEnabled()) {
log.trace("SolrDispatchFilter.init(): {}",
this.getClass().getClassLoader());
}
-
} catch (Throwable t) {
// catch this so our filter still works
log.error("Could not start Dispatch Filter.", t);
@@ -124,8 +110,7 @@ public class SolrDispatchFilter extends
CoreContainerAwareHttpFilter {
public void doFilter(HttpServletRequest request, HttpServletResponse
response, FilterChain chain)
throws IOException, ServletException {
// internal version of doFilter that tracks if we are in a retry
-
- dispatch(chain, closeShield(request), closeShield(response), false);
+ dispatch(chain, request, response, false);
}
/*
@@ -136,48 +121,19 @@ public class SolrDispatchFilter extends
CoreContainerAwareHttpFilter {
In late 2025 SOLR-18040 moved request wrappers to independent ServletFilters
such as PathExclusionFilter see web.xml for a full, up-to-date list
- This class is moving toward only handling dispatch, please think twice
- before adding anything else to it.
+ This class is only handling dispatch, please think twice before adding
anything else to it.
*/
private void dispatch(
FilterChain chain, HttpServletRequest request, HttpServletResponse
response, boolean retry)
throws IOException, ServletException {
-
- AtomicReference<HttpServletRequest> wrappedRequest = new
AtomicReference<>();
- try {
- authenticateRequest(request, response, wrappedRequest);
- } catch (SolrAuthenticationException e) {
- // it seems our auth system expects the plugin to set the status on the
request.
- // If this hasn't happened make sure it does happen now rather than
throwing an
- // exception, that formerly went on to be ignored in
- // org.apache.solr.servlet.ServletUtils.traceHttpRequestExecution2
- if (response.getStatus() < 400) {
- log.error(
- "Authentication Plugin threw SolrAuthenticationException without
setting request status >= 400");
- response.sendError(401, "Authentication Plugin rejected credentials.");
- }
- return; // Nothing more to do, chain.doFilter(req,res) doesn't get
called.
- }
- if (wrappedRequest.get() != null) {
- request = wrappedRequest.get();
- }
-
var span = getSpan(request);
- if (getCores().getAuthenticationPlugin() != null) {
- if (log.isDebugEnabled()) {
- log.debug("User principal: {}", request.getUserPrincipal());
- }
- final String principalName;
- if (request.getUserPrincipal() != null) {
- principalName = request.getUserPrincipal().getName();
- } else {
- principalName = null;
- }
- TraceUtils.setUser(span, String.valueOf(principalName));
- }
-
HttpSolrCall call = getHttpSolrCall(request, response, retry);
+
+ // this flag LOOKS like it should be in RequiredSolrRequestFilter, but
+ // the value set here is drives PKIAuthenticationPlugin.isSolrThread
+ // which gets used BEFORE this is set for some reason.
+ // BUG? Important timing?
ExecutorUtil.setServerThreadFlag(Boolean.TRUE);
try {
Action result = call.call();
@@ -215,7 +171,6 @@ public class SolrDispatchFilter extends
CoreContainerAwareHttpFilter {
protected HttpSolrCall getHttpSolrCall(
HttpServletRequest request, HttpServletResponse response, boolean retry)
{
String path = ServletUtils.getPathAfterContext(request);
-
CoreContainer cores;
try {
cores = getCores();
@@ -225,118 +180,6 @@ public class SolrDispatchFilter extends
CoreContainerAwareHttpFilter {
return solrCallFactory.createInstance(this, path, cores, request,
response, retry);
}
- // TODO: make this a servlet filter
- private void authenticateRequest(
- HttpServletRequest request,
- HttpServletResponse response,
- final AtomicReference<HttpServletRequest> wrappedRequest)
- throws IOException, SolrAuthenticationException {
- boolean requestContinues;
- final AtomicBoolean isAuthenticated = new AtomicBoolean(false);
- CoreContainer cores;
- try {
- cores = getCores();
- } catch (UnavailableException e) {
- throw new SolrException(ErrorCode.SERVER_ERROR, "Core Container
Unavailable");
- }
- AuthenticationPlugin authenticationPlugin =
cores.getAuthenticationPlugin();
- if (authenticationPlugin == null) {
- if (shouldAudit(EventType.ANONYMOUS)) {
- cores.getAuditLoggerPlugin().doAudit(new
AuditEvent(EventType.ANONYMOUS, request));
- }
- return;
- } else {
- // /admin/info/key must be always open. see SOLR-9188
- String requestPath = ServletUtils.getPathAfterContext(request);
- if (PublicKeyHandler.PATH.equals(requestPath)) {
- log.debug("Pass through PKI authentication endpoint");
- return;
- }
- // /solr/ (Admin UI) must be always open to allow displaying Admin UI
with login page
- if ("/solr/".equals(requestPath) || "/".equals(requestPath)) {
- log.debug("Pass through Admin UI entry point");
- return;
- }
- String header = request.getHeader(PKIAuthenticationPlugin.HEADER);
- String headerV2 = request.getHeader(PKIAuthenticationPlugin.HEADER_V2);
- if ((header != null || headerV2 != null)
- && cores.getPkiAuthenticationSecurityBuilder() != null)
- authenticationPlugin = cores.getPkiAuthenticationSecurityBuilder();
- try {
- if (log.isDebugEnabled()) {
- log.debug(
- "Request to authenticate: {}, domain: {}, port: {}",
- request,
- request.getLocalName(),
- request.getLocalPort());
- }
- // For legacy reasons, upon successful authentication this wants to
call the chain's next
- // filter, which obfuscates the layout of the code since one usually
expects to be able to
- // find the call to doFilter() in the implementation of
jakarta.servlet.Filter. Supplying a
- // trivial impl here to keep existing code happy while making the flow
clearer. Chain will
- // be called after this method completes. Eventually auth all moves to
its own filter
- // (hopefully). Most auth plugins simply return true after calling
this anyway, so they
- // obviously don't care.
- //
- // The Hadoop Auth Plugin was removed in SOLR-17540, however leaving
the below reference
- // for future readers, as there may be an option to simplify this
logic.
- //
- // Kerberos plugins seem to mostly use it to satisfy the api of a
- // wrapped instance of javax.servlet.Filter and neither of those seem
to be doing anything
- // fancy with the filter chain, so this would seem to be a hack
brought on by the fact that
- // our auth code has been forced to be code within dispatch filter,
rather than being a
- // filter itself. The HadoopAuthPlugin has a suspicious amount of code
after the call to
- // doFilter() which seems to imply that anything in this chain can get
executed before
- // authentication completes, and I can't figure out how that's a good
idea in the first
- // place.
- requestContinues =
- authenticationPlugin.authenticate(
- request,
- response,
- (req, rsp) -> {
- isAuthenticated.set(true);
- wrappedRequest.set((HttpServletRequest) req);
- });
- } catch (Exception e) {
- log.info("Error authenticating", e);
- throw new SolrException(ErrorCode.SERVER_ERROR, "Error during request
authentication, ", e);
- }
- }
- // requestContinues is an optional short circuit, thus we still need to
check isAuthenticated.
- // This is because the AuthenticationPlugin doesn't always have enough
information to determine
- // if it should short circuit, e.g. the Kerberos Authentication Filter
will send an error and
- // not call later filters in chain, but doesn't throw an exception. We
could force each Plugin
- // to implement isAuthenticated to simplify the check here, but that just
moves the complexity
- // to multiple code paths.
- if (!requestContinues || !isAuthenticated.get()) {
- response.flushBuffer();
- if (shouldAudit(EventType.REJECTED)) {
- cores.getAuditLoggerPlugin().doAudit(new
AuditEvent(EventType.REJECTED, request));
- }
- throw new SolrAuthenticationException();
- }
- if (shouldAudit(EventType.AUTHENTICATED)) {
- cores.getAuditLoggerPlugin().doAudit(new
AuditEvent(EventType.AUTHENTICATED, request));
- }
- // Auth Success
- }
-
- /**
- * Check if audit logging is enabled and should happen for given event type
- *
- * @param eventType the audit event
- */
- private boolean shouldAudit(AuditEvent.EventType eventType) {
- CoreContainer cores;
- try {
- cores = getCores();
- } catch (UnavailableException e) {
- throw new SolrException(ErrorCode.SERVER_ERROR, "Core Container
Unavailable");
- }
- return cores.getAuditLoggerPlugin() != null
- && cores.getAuditLoggerPlugin().shouldLog(eventType);
- }
-
/** internal API */
public interface HttpSolrCallFactory {
default HttpSolrCall createInstance(
diff --git
a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
index e4224e44e6c..7bf21d54964 100644
--- a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
+++ b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
@@ -60,6 +60,7 @@ import org.apache.solr.common.util.TimeSource;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.metrics.SolrMetricManager;
+import org.apache.solr.servlet.AuthenticationFilter;
import org.apache.solr.servlet.CoreContainerProvider;
import org.apache.solr.servlet.PathExclusionFilter;
import org.apache.solr.servlet.RateLimitFilter;
@@ -117,6 +118,7 @@ public class JettySolrRunner {
volatile FilterHolder pathExcludeFilter;
volatile FilterHolder requiredFilter;
volatile FilterHolder rateLimitFilter;
+ volatile FilterHolder authFilter;
volatile FilterHolder dispatchFilter;
private FilterHolder tracingFilter;
@@ -423,10 +425,14 @@ public class JettySolrRunner {
rateLimitFilter =
root.getServletHandler().newFilterHolder(Source.EMBEDDED);
rateLimitFilter.setHeldClass(RateLimitFilter.class);
- // Ratelimit Requests
+ // Trace Requests
tracingFilter =
root.getServletHandler().newFilterHolder(Source.EMBEDDED);
tracingFilter.setHeldClass(TracingFilter.class);
+ // Authenticate Requests
+ authFilter = root.getServletHandler().newFilterHolder(Source.EMBEDDED);
+ authFilter.setHeldClass(AuthenticationFilter.class);
+
// This is our main workhorse
dispatchFilter =
root.getServletHandler().newFilterHolder(Source.EMBEDDED);
dispatchFilter.setHeldClass(SolrDispatchFilter.class);
@@ -436,6 +442,7 @@ public class JettySolrRunner {
root.addFilter(requiredFilter, "/*", EnumSet.of(DispatcherType.REQUEST));
root.addFilter(rateLimitFilter, "/*",
EnumSet.of(DispatcherType.REQUEST));
root.addFilter(tracingFilter, "/*", EnumSet.of(DispatcherType.REQUEST));
+ root.addFilter(authFilter, "/*", EnumSet.of(DispatcherType.REQUEST));
root.addFilter(dispatchFilter, "/*", EnumSet.of(DispatcherType.REQUEST));
// Default servlet as a fall-through
diff --git a/solr/webapp/web/WEB-INF/web.xml b/solr/webapp/web/WEB-INF/web.xml
index 2b5791b9b6a..287321efb16 100644
--- a/solr/webapp/web/WEB-INF/web.xml
+++ b/solr/webapp/web/WEB-INF/web.xml
@@ -45,12 +45,12 @@
</filter-mapping>
<filter>
- <filter-name>MDCLoggingFilter</filter-name>
+ <filter-name>SolrFilter</filter-name>
<filter-class>org.apache.solr.servlet.RequiredSolrRequestFilter</filter-class>
</filter>
<filter-mapping>
- <filter-name>MDCLoggingFilter</filter-name>
+ <filter-name>SolrFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
@@ -74,6 +74,15 @@
<url-pattern>/*</url-pattern>
</filter-mapping>
+ <filter>
+ <filter-name>AuthenticationFilter</filter-name>
+ <filter-class>org.apache.solr.servlet.AuthenticationFilter</filter-class>
+ </filter>
+
+ <filter-mapping>
+ <filter-name>AuthenticationFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
<filter>
<filter-name>SolrRequestFilter</filter-name>
<filter-class>org.apache.solr.servlet.SolrDispatchFilter</filter-class>