Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog/unreleased/SOLR-18048.yml
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions solr/core/src/java/org/apache/solr/core/CoreContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Tracer;
import jakarta.inject.Singleton;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.nio.file.Files;
Expand Down Expand Up @@ -147,6 +148,7 @@
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;
Expand Down Expand Up @@ -2510,4 +2512,29 @@ public void addKeyVal(Object map, Object key, Object val) throws IOException {
}
});
}

/**
* Check if audit logging is enabled and should happen for given event type
*
* @param eventType the audit event
*/
public boolean shouldAudit(AuditEvent.EventType eventType) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see callers to what you added. We should be conservative in adding yet more methods to CoreContainer.

return this.getAuditLoggerPlugin() != null && this.getAuditLoggerPlugin().shouldLog(eventType);
}

/**
* Audit an event
*
* @param type The type of event to audit.
* @param req The request being audited
*/
public void audit(AuditEvent.EventType type, HttpServletRequest req) {
if (shouldAudit(type)) {
getAuditLoggerPlugin().doAudit(new AuditEvent(type, req));
}
}

public boolean isAuthEnabled() {
return getAuthenticationPlugin() != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -79,7 +79,7 @@ public abstract boolean doAuthenticate(
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(
Expand Down
188 changes: 188 additions & 0 deletions solr/core/src/java/org/apache/solr/servlet/AuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* 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.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;

public class AuthenticationFilter extends CoreContainerAwareHttpFilter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, pleaese say some basic things in javadoc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, spent all my energy on the in-code comments and forgot this :) Will do.


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(ANONYMOUS, req);
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(REJECTED, req);
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());
}
}



private record AuditSuccessChainWrapper(CoreContainer cc, FilterChain chain)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

records are cool but this doesn't seem like an appropriate use since conceptually this is a "FilterChain" not some pojo-ish thing. I'm assuming equals & hashCode aren't needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want, we can go back to the inner class I had before I converted it (automatically in Intellij) but that's just a lot of wasted lines of code (declaring the fields holding the CoreContainer and FilterChain)? I like it for it's conciseness.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure you did it to save some LOC (and not much) but I have a principled stance against it for reasons stated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implements FilterChain {

@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(AUTHENTICATED, (HttpServletRequest) rq);
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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*/
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;
Expand Down Expand Up @@ -72,7 +74,9 @@ protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterC
// 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));
} finally {
// cleanups for above stuff
MDCLoggingContext.reset();
Expand Down

This file was deleted.

Loading
Loading