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>


Reply via email to