notes on locking down solr - krickert/search-api GitHub Wiki

Here's a detailed implementation outline and complete example of how to secure Solr using custom security plugins and a PostFilter, integrated with an external ACL (Access Control List) service via a cookie token passed from your API.

Architecture Overview:

  • Solr Security Plugin: Authenticates requests based on cookie headers and integrates ACL filtering.
  • ACL Micronaut Service: Provides ACL lists for authenticated users from sources (GitLab, Confluence, SharePoint).
  • Custom Update Handler: Validates ACL fields on documents during indexing.
  • Custom Query Parser or PostFilter: Filters results based on ACL.

① ACL Service Interface (Micronaut, generic)

Define a generic ACL service interface first.

public interface ACLService {
    /**
     * Resolve ACLs for user token.
     * @param token the session token from cookie.
     * @return Set of ACL strings.
     */
    Set<String> getUserACLs(String token);
}

Example JSON REST Response:

{
    "acls": ["gitlab/project1", "confluence/space2", "sharepoint/site3"]
}

② Custom Solr Security Plugin (AuthenticationPlugin)

Create a plugin extending AuthenticationPlugin to handle authentication via cookies and set the user principal.

import org.apache.solr.security.AuthenticationPlugin;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

public class CookieTokenAuthenticationPlugin extends AuthenticationPlugin {
    private ACLService aclService;

    public CookieTokenAuthenticationPlugin(ACLService aclService) {
        this.aclService = aclService;
    }

    @Override
    public void init(Map<String, Object> pluginConfig) {}

    @Override
    public boolean doAuthenticate(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
        String cookieToken = extractTokenFromCookie(request);

        if (cookieToken == null || aclService.getUserACLs(cookieToken).isEmpty()) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
            return false;
        }

        request.setAttribute(AuthenticationPlugin.USER, cookieToken);
        filterChain.doFilter(request, response);
        return true;
    }

    private String extractTokenFromCookie(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("auth_token".equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }

    @Override
    public void close() {}
}

③ Custom Update Request Processor (Validate ACL Field)

Enforce ACL validation on every document.

import org.apache.solr.update.processor.UpdateRequestProcessor;
import org.apache.solr.update.processor.UpdateRequestProcessorFactory;
import org.apache.solr.common.SolrInputDocument;

public class ACLValidationProcessorFactory extends UpdateRequestProcessorFactory {
    @Override
    public UpdateRequestProcessor getInstance(SolrQueryRequest req, SolrQueryResponse rsp, UpdateRequestProcessor next) {
        return new UpdateRequestProcessor(next) {
            @Override
            public void processAdd(AddUpdateCommand cmd) throws IOException {
                SolrInputDocument doc = cmd.getSolrInputDocument();
                if (doc.getFieldValue("acl") == null) {
                    throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "ACL field is required.");
                }
                super.processAdd(cmd);
            }
        };
    }
}

Add to solrconfig.xml:

<updateRequestProcessorChain name="aclValidationChain">
    <processor class="com.example.ACLValidationProcessorFactory" />
    <processor class="solr.RunUpdateProcessorFactory" />
</updateRequestProcessorChain>

④ ACL PostFilter for Query Filtering

import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.*;
import org.apache.solr.search.*;

import java.io.IOException;
import java.util.Set;

public class ACLPostFilter extends ExtendedQueryBase implements PostFilter {
    private final Set<String> userACLs;

    public ACLPostFilter(Set<String> userACLs) {
        this.userACLs = userACLs;
    }

    @Override
    public DelegatingCollector getFilterCollector(IndexSearcher searcher) {
        return new DelegatingCollector() {
            private SortedDocValues aclValues;

            @Override
            protected void doSetNextReader(LeafReaderContext context) throws IOException {
                aclValues = DocValues.getSorted(context.reader(), "acl");
                super.doSetNextReader(context);
            }

            @Override
            public void collect(int doc) throws IOException {
                if (aclValues.advanceExact(doc)) {
                    String docAcl = aclValues.lookupOrd(aclValues.ordValue()).utf8ToString();
                    if (userACLs.contains(docAcl)) {
                        super.collect(doc);
                    }
                }
            }
        };
    }

    @Override
    public boolean getCache() {
        return false;
    }

    @Override
    public boolean getCacheSep() {
        return false;
    }

    @Override
    public int hashCode() {
        return userACLs.hashCode();
    }

    @Override
    public boolean equals(Object other) {
        return other instanceof ACLPostFilter && userACLs.equals(((ACLPostFilter) other).userACLs);
    }
}

⑤ Custom Query Parser (simplified)

For easy client-side integration, create a custom parser.

import org.apache.solr.search.QParser;
import org.apache.solr.search.QParserPlugin;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.SolrQueryRequest;

public class ACLQueryParserPlugin extends QParserPlugin {
    private ACLService aclService;

    public ACLQueryParserPlugin(ACLService aclService) {
        this.aclService = aclService;
    }

    @Override
    public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
        String token = (String) req.getContext().get("user");
        Set<String> userACLs = aclService.getUserACLs(token);
        return new QParser(qstr, localParams, params, req) {
            @Override
            public Query parse() throws SyntaxError {
                Query userQuery = subQuery(qstr, null).getQuery();
                ACLPostFilter aclFilter = new ACLPostFilter(userACLs);
                return new BooleanQuery.Builder()
                        .add(userQuery, BooleanClause.Occur.MUST)
                        .add(aclFilter, BooleanClause.Occur.FILTER)
                        .build();
            }
        };
    }
}

Register in solrconfig.xml:

<queryParser name="aclParser" class="com.example.ACLQueryParserPlugin"/>

⑥ Example Usage in Solr Query

Client sends queries with token in the cookie:

GET /solr/collection/select?q={!aclParser}search+terms HTTP/1.1
Cookie: auth_token=abc123xyz

Deployment Checklist:

  • Package the ACL service as a Micronaut microservice.
  • Deploy the Solr plugins into Solr's lib directory.
  • Update solrconfig.xml to activate plugins and parsers.
  • Ensure ACL data is consistently updated and cached for performance.

Recommendations & Next Steps:

  • Implement caching (Redis, Caffeine) in the ACL service to optimize lookups.
  • Extend this example for real-world ACL sources (GitLab API, Confluence REST, SharePoint Graph API).
  • Enhance logging and monitoring for security events (unauthorized attempts, ACL lookup failures).
  • Add unit and integration tests for robust coverage.

This detailed example should give you a comprehensive foundation to implement a secure Solr environment leveraging your existing expertise and infrastructure.

⚠️ **GitHub.com Fallback** ⚠️