blob: 63bc4cd9c75bf55b280d4f9774cb9cfb9b24db09 [file] [log] [blame]
/*
* 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<String> collections;
private Map<String, Object> context;
private Map<String, String> headers;
private Map<String, List<String>> 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());
if (authorizationContext.getParams() != null) {
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<String, String> getHeadersFromRequest(HttpServletRequest httpRequest) {
HashMap<String, String> h = new HashMap<>();
Enumeration<String> 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<String, Object> getContext() {
return context;
}
/**
* List of collection names involved in request
*/
public List<String> 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<String, String> getHeaders() {
return headers;
}
/**
* Map of all Solr request parameters attached to the request. Pulled from url
*/
public Map<String, List<String>> 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<String> 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<String, Object> 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<String> 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<String, String> headers) {
this.headers = headers;
return this;
}
public AuditEvent setSolrParams(Map<String, List<String>> 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<String> 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<String> STREAMING_PATH_REGEXES = Collections.singletonList(".*/stream.*");
private static final List<String> INDEXING_PATH_REGEXES = Collections.singletonList(".*/update.*");
private static final List<String> SEARCH_PATH_REGEXES = Arrays.asList(".*/select.*", ".*/query.*");
private static final List<Pattern> ADMIN_PATH_PATTERNS = ADMIN_PATH_REGEXES.stream().map(Pattern::compile).collect(Collectors.toList());
private static final List<Pattern> STREAMING_PATH_PATTERNS = STREAMING_PATH_REGEXES.stream().map(Pattern::compile).collect(Collectors.toList());
private static final List<Pattern> INDEXING_PATH_PATTERNS = INDEXING_PATH_REGEXES.stream().map(Pattern::compile).collect(Collectors.toList());
private static final List<Pattern> SEARCH_PATH_PATTERNS = SEARCH_PATH_REGEXES.stream().map(Pattern::compile).collect(Collectors.toList());
}