All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.solr.security.AuditEvent Maven / Gradle / Ivy

There is a newer version: 9.6.1
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.security;

import javax.servlet.http.HttpServletRequest;
import java.lang.invoke.MethodHandles;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.servlet.ServletUtils;
import org.apache.solr.servlet.SolrRequestParsers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import static org.apache.solr.security.AuditEvent.EventType.ANONYMOUS;
import static org.apache.solr.security.AuditEvent.EventType.ERROR;

/**
 * Audit event that takes request and auth context as input to be able to audit log custom things.
 * This interface may change in next release and is marked experimental
 * @since 8.1.0
 * @lucene.experimental
 */
public class AuditEvent {
  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
  private String baseUrl;
  private String nodeName;
  private String message;
  private Level level;
  private Date date;
  private String username;
  private String session;
  private String clientIp;
  private List collections;
  private Map context;
  private Map headers;
  private Map> solrParams = new HashMap<>();
  private String solrHost;
  private int solrPort;
  private String solrIp;
  private String resource;
  private String httpMethod;
  private String httpQueryString;
  private EventType eventType;
  private AuthorizationResponse autResponse;
  private RequestType requestType;
  private double qTime = -1;
  private int status = -1;
  private Throwable exception;

  /* Predefined event types. Custom types can be made through constructor */
  public enum EventType {
    AUTHENTICATED("Authenticated", "User successfully authenticated", Level.INFO, -1),
    REJECTED("Rejected", "Authentication request rejected", Level.WARN, 401),
    ANONYMOUS("Anonymous", "Request proceeds with unknown user", Level.INFO, -1),
    ANONYMOUS_REJECTED("AnonymousRejected", "Request from unknown user rejected", Level.WARN, 401),
    AUTHORIZED("Authorized", "Authorization succeeded", Level.INFO, -1),
    UNAUTHORIZED("Unauthorized", "Authorization failed", Level.WARN, 403),
    COMPLETED("Completed", "Request completed", Level.INFO, 200),
    ERROR("Error", "Request was not executed due to an error", Level.ERROR, 500);
    
    public final String message;
    public String explanation;
    public final Level level;
    public int status;

    EventType(String message, String explanation, Level level, int status) {
      this.message = message;
      this.explanation = explanation;
      this.level = level;
      this.status = status;
    }
  }

  /**
   * Empty event, must be filled by user using setters.
   * Message and Loglevel will be initialized from EventType but can
   * be overridden with setters afterwards.
   * @param eventType a predefined or custom EventType
   */
  public AuditEvent(EventType eventType) {
    this.date = new Date();
    this.eventType = eventType;
    this.status = eventType.status;
    this.level = eventType.level;
    this.message = eventType.message;
  }

  public AuditEvent(EventType eventType, HttpServletRequest httpRequest) {
    this(eventType, null, httpRequest);
  }
  
  // Constructor for testing and deserialization only
  protected AuditEvent() { }
  
  /**
   * Event based on an HttpServletRequest, typically used during authentication. 
   * Solr will fill in details such as ip, http method etc from the request, and
   * username if Principal exists on the request.
   * @param eventType a predefined or custom EventType
   * @param httpRequest the request to initialize from
   */
  public AuditEvent(EventType eventType, Throwable exception, HttpServletRequest httpRequest) {
    this(eventType);
    this.solrHost = httpRequest.getLocalName();
    this.solrPort = httpRequest.getLocalPort();
    this.solrIp = httpRequest.getLocalAddr();
    this.clientIp = httpRequest.getRemoteAddr();
    this.httpMethod = httpRequest.getMethod();
    this.httpQueryString = httpRequest.getQueryString();
    this.headers = getHeadersFromRequest(httpRequest);
    this.baseUrl = httpRequest.getRequestURL().toString();
    this.nodeName = MDC.get(ZkStateReader.NODE_NAME_PROP);
    SolrRequestParsers.parseQueryString(httpQueryString).forEach(sp -> {
      this.solrParams.put(sp.getKey(), Arrays.asList(sp.getValue()));
    });

    setResource(ServletUtils.getPathAfterContext(httpRequest));
    setRequestType(findRequestType());

    if (exception != null) setException(exception);

    Principal principal = httpRequest.getUserPrincipal();
    if (principal != null) {
      this.username = httpRequest.getUserPrincipal().getName();
    } else if (eventType.equals(EventType.AUTHENTICATED)) {
      this.eventType = ANONYMOUS;
      this.message = ANONYMOUS.message;
      this.level = ANONYMOUS.level;
      log.debug("Audit event type changed from AUTHENTICATED to ANONYMOUS since no Principal found on request");
    }
  }

  /**
   * Event based on request and AuthorizationContext. Solr will fill in details
   * such as collections, ip, http method etc from the context.
   * @param eventType a predefined or custom EventType
   * @param httpRequest the request to initialize from
   * @param authorizationContext the context to initialize from
   */
  public AuditEvent(EventType eventType, HttpServletRequest httpRequest, AuthorizationContext authorizationContext) {
    this(eventType, httpRequest);
    this.collections = authorizationContext.getCollectionRequests()
        .stream().map(r -> r.collectionName).collect(Collectors.toList());
    setResource(authorizationContext.getResource());
    this.requestType = RequestType.convertType(authorizationContext.getRequestType());
    authorizationContext.getParams().forEach(p -> {
      this.solrParams.put(p.getKey(), Arrays.asList(p.getValue()));
    });
  }

  /**
   * Event to log completed requests. Takes time and status. Solr will fill in details
   * such as collections, ip, http method etc from the HTTP request and context.
   *
   * @param eventType            a predefined or custom EventType
   * @param httpRequest          the request to initialize from
   * @param authorizationContext the context to initialize from
   * @param qTime                query time
   * @param exception            exception from query response, or null if OK
   */
  public AuditEvent(EventType eventType, HttpServletRequest httpRequest, AuthorizationContext authorizationContext, double qTime, Throwable exception) {
    this(eventType, httpRequest, authorizationContext);
    setQTime(qTime);
    setException(exception);
  }

  private HashMap getHeadersFromRequest(HttpServletRequest httpRequest) {
    HashMap h = new HashMap<>();
    Enumeration headersEnum = httpRequest.getHeaderNames();
    while (headersEnum != null && headersEnum.hasMoreElements()) {
      String name = headersEnum.nextElement();
      h.put(name, httpRequest.getHeader(name));
    }
    return h;
  }

  public enum Level {
    INFO,  // Used for normal successful events
    WARN,  // Used when a user is blocked etc
    ERROR  // Used when there is an exception or error during auth / authz
  }

  public enum RequestType {
    ADMIN, SEARCH, UPDATE, STREAMING, UNKNOWN;
    
    static RequestType convertType(AuthorizationContext.RequestType ctxReqType) {
      switch (ctxReqType) {
        case ADMIN:
          return RequestType.ADMIN;
        case READ:
          return RequestType.SEARCH;
        case WRITE:
          return RequestType.UPDATE;
        default:
          return RequestType.UNKNOWN;
      }
    }
  }

  /**
   * The human readable message about this event
   */
  public String getMessage() {
    return message;
  }

  /**
   * Level of this event. Can be INFO, WARN or ERROR
   * @return {@link Level} enum
   */
  public Level getLevel() {
    return level;
  }

  /**
   * Date that the event happened
   */
  public Date getDate() {
    return date;
  }

  /**
   * Username of logged in user, or null if no authenticated user
   */
  public String getUsername() {
    return username;
  }

  /**
   * Session identifier
   */
  public String getSession() {
    return session;
  }

  /**
   * IP address of the client doing the request
   */
  public String getClientIp() {
    return clientIp;
  }

  /**
   * A general purpose context map with potential extra information about the event
   */
  public Map getContext() {
    return context;
  }

  /**
   * List of collection names involved in request
   */
  public List getCollections() {
    return collections;
  }

  /**
   * Identifies the resource being operated on. This is not the same as URL path.
   * For queries the resource is relative to collection name, e.g. /select or /update.
   * For other events the resource may be /api/node/health or /admin/collection
   */
  public String getResource() {
    return resource;
  }

  /**
   * The HTTP method. E.g. GET, POST, PUT
   */
  public String getHttpMethod() {
    return httpMethod;
  }

  /**
   * Query part of URL or null if query part
   */
  public String getHttpQueryString() {
    return httpQueryString;
  }

  /**
   * EventType tells the outcome of the event such as REJECTED, UNAUTHORIZED or ERROR
   * @return {@link EventType} enum
   */
  public EventType getEventType() {
    return eventType;
  }

  /**
   * Host name of the Solr node logging the event
   */
  public String getSolrHost() {
    return solrHost;
  }

  /**
   * IP address of the Solr node logging the event
   */
  public String getSolrIp() {
    return solrIp;
  }

  /**
   * Port number of the Solr node logging the event
   */
  public int getSolrPort() {
    return solrPort;
  }

  /**
   * Map of all HTTP request headers belonging to the request
   */
  public Map getHeaders() {
    return headers;
  }

  /**
   * Map of all Solr request parameters attached to the request. Pulled from url
   */
  public Map> getSolrParams() {
    return solrParams;
  }

  /**
   * Gets first value of a certain Solr request parameter
   * @param key name of request parameter to retrieve
   * @return String value of the first value, regardless of number of valies
   */
  public String getSolrParamAsString(String key) {
    List v = getSolrParams().get(key);
    if (v != null && v.size() > 0) {
      return String.valueOf((v).get(0));
    }
    return null;
  }

  /**
   * The authorization response object from authorization plugin, or null authz has not happened
   */
  public AuthorizationResponse getAutResponse() {
    return autResponse;
  }

  /**
   * Node name of Solr node, on the internal format host:port_context, e.g. 10.0.0.1:8983_solr
   */
  public String getNodeName() {
    return nodeName;
  }

  /**
   * Determines the type of request. Can be ADMIN, SEARCH, UPDATE, STREAMING, UNKNOWN
   * @return {@link RequestType} enum
   */
  public RequestType getRequestType() {
    return requestType;
  }

  /**
   * HTTP status code of event, i.e. 200 = OK, 401 = unauthorized
   */
  public int getStatus() {
    return status;
  }

  /**
   * Request time in milliseconds for completed requests
   */
  public double getQTime() {
    return qTime;
  }

  /**
   * In case of ERROR event, find the exception causing the error
   */
  public Throwable getException() {
    return exception;
  }

  /**
   * Get baseUrl as StringBuffer for back compat with previous version
   * @deprecated Please use {@link #getBaseUrl()} instead
   * @return StringBuffer of the base url without query part
   */
  @Deprecated
  @JsonIgnore
  public StringBuffer getRequestUrl() {
    return new StringBuffer(baseUrl);
  }

  /**
   * Full URL of the original request. This is {@link #baseUrl} + "?" + {@link #httpQueryString}.
   * Returns null if not set
   */
  public String getUrl() {
    if (baseUrl == null) return null;
    return baseUrl + (httpQueryString != null ? "?" + httpQueryString : "");
  }

  /**
   * First part of URL of the request, but not including request parameters, or null if not set
   */
  public String getBaseUrl() {
    return baseUrl;
  }

  // Setters, builder style
  
  public AuditEvent setBaseUrl(String baseUrl) {
    this.baseUrl = baseUrl;
    return this;
  }

  public AuditEvent setSession(String session) {
    this.session = session;
    return this;
  }

  public AuditEvent setClientIp(String clientIp) {
    this.clientIp = clientIp;
    return this;
  }

  public AuditEvent setContext(Map context) {
    this.context = context;
    return this;
  }

  public AuditEvent setContextEntry(String key, Object value) {
    this.context.put(key, value);
    return this;
  }

  public AuditEvent setMessage(String message) {
    this.message = message;
    return this;
  }

  public AuditEvent setLevel(Level level) {
    this.level = level;
    return this;
  }

  public AuditEvent setDate(Date date) {
    this.date = date;
    return this;
  }

  public AuditEvent setUsername(String username) {
    this.username = username;
    return this;
  }

  public AuditEvent setCollections(List collections) {
    this.collections = collections;
    return this;
  }

  public AuditEvent setResource(String resource) {
    this.resource = normalizeResourcePath(resource);
    return this;
  }

  public AuditEvent setHttpMethod(String httpMethod) {
    this.httpMethod = httpMethod;
    return this;
  }

  public AuditEvent setHttpQueryString(String httpQueryString) {
    this.httpQueryString = httpQueryString;
    return this;
  }

  public AuditEvent setSolrHost(String solrHost) {
    this.solrHost = solrHost;
    return this;
  }

  public AuditEvent setSolrPort(int solrPort) {
    this.solrPort = solrPort;
    return this;
  }

  public AuditEvent setSolrIp(String solrIp) {
    this.solrIp = solrIp;
    return this;
  }

  public AuditEvent setHeaders(Map headers) {
    this.headers = headers;
    return this;
  }

  public AuditEvent setSolrParams(Map> solrParams) {
    this.solrParams = solrParams;
    return this;
  }

  public AuditEvent setAutResponse(AuthorizationResponse autResponse) {
    this.autResponse = autResponse;
    return this;
  }

  public AuditEvent setRequestType(RequestType requestType) {
    this.requestType = requestType;
    return this;
  }

  public AuditEvent setQTime(double qTime) {
    this.qTime = qTime;
    return this;
  }

  public AuditEvent setStatus(int status) {
    this.status = status;
    return this;
  }

  public AuditEvent setException(Throwable exception) {
    this.exception = exception;
    if (exception != null) {
      this.eventType = ERROR;
      this.level = ERROR.level;
      this.message = ERROR.message;
      if (exception instanceof SolrException)
        status = ((SolrException)exception).code();
    }
    return this;
  }

  private RequestType findRequestType() {
    if (resource == null) return RequestType.UNKNOWN;
    if (SEARCH_PATH_PATTERNS.stream().anyMatch(p -> p.matcher(resource).matches())) return RequestType.SEARCH;
    if (INDEXING_PATH_PATTERNS.stream().anyMatch(p -> p.matcher(resource).matches())) return RequestType.UPDATE;
    if (STREAMING_PATH_PATTERNS.stream().anyMatch(p -> p.matcher(resource).matches())) return RequestType.STREAMING;
    if (ADMIN_PATH_PATTERNS.stream().anyMatch(p -> p.matcher(resource).matches())) return RequestType.ADMIN;
    return RequestType.UNKNOWN;
  }

  protected String normalizeResourcePath(String resourcePath) {
    if (resourcePath == null) return "";
    return resourcePath.replaceFirst("^/____v2", "/api");
  }

  private static final List ADMIN_PATH_REGEXES = Arrays.asList(
      "^/admin/.*",
      "^/api/(c|collections)$",
      "^/api/(c|collections)/[^/]+/config$",
      "^/api/(c|collections)/[^/]+/schema$",
      "^/api/(c|collections)/[^/]+/shards.*",
      "^/api/cores.*$",
      "^/api/node.*$",
      "^/api/cluster.*$");

  private static final List STREAMING_PATH_REGEXES = Collections.singletonList(".*/stream.*");
  private static final List INDEXING_PATH_REGEXES = Collections.singletonList(".*/update.*");
  private static final List SEARCH_PATH_REGEXES = Arrays.asList(".*/select.*", ".*/query.*");

  private static final List ADMIN_PATH_PATTERNS = ADMIN_PATH_REGEXES.stream().map(Pattern::compile).collect(Collectors.toList());
  private static final List STREAMING_PATH_PATTERNS = STREAMING_PATH_REGEXES.stream().map(Pattern::compile).collect(Collectors.toList());
  private static final List INDEXING_PATH_PATTERNS = INDEXING_PATH_REGEXES.stream().map(Pattern::compile).collect(Collectors.toList());
  private static final List SEARCH_PATH_PATTERNS = SEARCH_PATH_REGEXES.stream().map(Pattern::compile).collect(Collectors.toList());
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy