/*
* This file is part of ReadonlyREST.
*
* ReadonlyREST is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ReadonlyREST is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ReadonlyREST. If not, see http://www.gnu.org/licenses/
*/
package org.elasticsearch.plugin.readonlyrest.es.requestcontext;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.CompositeIndicesRequest;
import org.elasticsearch.action.IndicesRequest;
import org.elasticsearch.cluster.metadata.AliasOrIndex;
import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.index.IndexNotFoundException;
import org.elasticsearch.plugin.readonlyrest.ESContext;
import org.elasticsearch.plugin.readonlyrest.acl.BlockHistory;
import org.elasticsearch.plugin.readonlyrest.acl.blocks.Block;
import org.elasticsearch.plugin.readonlyrest.acl.blocks.rules.RuleExitResult;
import org.elasticsearch.plugin.readonlyrest.acl.domain.HttpMethod;
import org.elasticsearch.plugin.readonlyrest.acl.domain.LoggedUser;
import org.elasticsearch.plugin.readonlyrest.acl.domain.MatcherWithWildcards;
import org.elasticsearch.plugin.readonlyrest.requestcontext.Delayed;
import org.elasticsearch.plugin.readonlyrest.requestcontext.IndicesRequestContext;
import org.elasticsearch.plugin.readonlyrest.requestcontext.RCUtils;
import org.elasticsearch.plugin.readonlyrest.requestcontext.RequestContext;
import org.elasticsearch.plugin.readonlyrest.requestcontext.Transactional;
import org.elasticsearch.plugin.readonlyrest.requestcontext.VariablesManager;
import org.elasticsearch.plugin.readonlyrest.utils.BasicAuthUtils;
import org.elasticsearch.plugin.readonlyrest.utils.BasicAuthUtils.BasicAuth;
import org.elasticsearch.plugin.readonlyrest.utils.ReflecUtils;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.threadpool.ThreadPool;
import java.net.InetSocketAddress;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Collectors;
/**
* Created by sscarduzio on 20/02/2016.
*/
public class RequestContextImpl extends Delayed implements RequestContext, IndicesRequestContext {
private final Logger logger;
private final RestRequest request;
private final String action;
private final ActionRequest actionRequest;
private final String id;
private final Map<String, String> requestHeaders;
private final ClusterService clusterService;
private final ESContext context;
private final Transactional<Set<String>> indices;
private final Transactional<Map<String, String>> responseHeaders;
private String content = null;
private Set<BlockHistory> history = Sets.newHashSet();
private boolean doesInvolveIndices = false;
private Transactional<Optional<LoggedUser>> loggedInUser;
private final VariablesManager variablesManager;
public RequestContextImpl(RestRequest request, String action, ActionRequest actionRequest, ClusterService clusterService,
IndexNameExpressionResolver indexResolver, ThreadPool threadPool, ESContext context) {
super("rc", context);
this.logger = context.logger(getClass());
this.request = request;
this.action = action;
this.actionRequest = actionRequest;
this.clusterService = clusterService;
this.context = context;
this.id = request.hashCode() + "-" + actionRequest.hashCode();
final Map<String, String> h = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
request.getHeaders().keySet().forEach(k -> {
if (request.getAllHeaderValues(k).isEmpty()) {
return;
}
h.put(k, request.getAllHeaderValues(k).iterator().next());
});
this.requestHeaders = h;
this.responseHeaders = new Transactional<Map<String, String>>("rc-resp-headers", context) {
@Override
public Map<String, String> initialize() {
return Maps.newHashMap();
}
@Override
public Map<String, String> copy(Map<String, String> initial) {
return Maps.newHashMap(initial);
}
@Override
public void onCommit(Map<String, String> hMap) {
hMap.keySet().forEach(k -> threadPool.getThreadContext().addResponseHeader(k, hMap.get(k)));
}
};
this. loggedInUser = new Transactional<Optional<LoggedUser>>("rc-loggedin-user", context) {
@Override
public Optional<LoggedUser> initialize() {
return Optional.empty();
}
@Override
public Optional<LoggedUser> copy(Optional<LoggedUser> initial) {
return initial.map((u) -> new LoggedUser(u.getId()));
}
@Override
public void onCommit(Optional<LoggedUser> value) {
value.ifPresent(loggedUser -> {
Map<String, String> theMap = responseHeaders.get();
theMap.put("X-RR-User", loggedUser.getId());
responseHeaders.mutate(theMap);
});
}
};
variablesManager = new VariablesManager(h, this, context);
doesInvolveIndices = actionRequest instanceof IndicesRequest || actionRequest instanceof CompositeIndicesRequest;
indices = RCTransactionalIndices.mkInstance(this, context);
// If we get to commit this transaction, put this header.
delay(() -> loggedInUser.get().ifPresent(loggedUser -> setResponseHeader("X-RR-User", loggedUser.getId())));
// Register transactional values to the main queue
responseHeaders.delegateTo(this);
loggedInUser.delegateTo(this);
indices.delegateTo(this);
}
public Logger getLogger() {
return logger;
}
public void addToHistory(Block block, Set<RuleExitResult> results) {
BlockHistory blockHistory = new BlockHistory(block.getName(), results);
history.add(blockHistory);
}
ActionRequest getUnderlyingRequest() {
return actionRequest;
}
public String getId() {
return id;
}
public boolean involvesIndices() {
return doesInvolveIndices;
}
public Boolean isReadRequest() {
return RCUtils.isReadRequest(action);
}
public String getRemoteAddress() {
String remoteHost = ((InetSocketAddress) request.getRemoteAddress()).getAddress().getHostAddress();
// Make sure we recognize localhost even when IPV6 is involved
if (RCUtils.isLocalHost(remoteHost)) {
remoteHost = RCUtils.LOCALHOST;
}
return remoteHost;
}
public String getContent() {
if (content == null) {
try {
content = request.content().utf8ToString();
} catch (Exception e) {
content = "";
}
}
return content;
}
public Optional<String> resolveVariable(String original){
return variablesManager.apply(original);
}
public Set<String> getAllIndicesAndAliases() {
return clusterService.state().metaData().getAliasAndIndexLookup().keySet();
}
public Set<String> getIndexMetadata(String s) {
SortedMap<String, AliasOrIndex> lookup = clusterService.state().metaData().getAliasAndIndexLookup();
return lookup.get(s).getIndices().stream().map(IndexMetaData::getIndexUUID).collect(Collectors.toSet());
}
public HttpMethod getMethod() {
switch (request.method()) {
case GET:
return HttpMethod.GET;
case POST:
return HttpMethod.POST;
case PUT:
return HttpMethod.PUT;
case DELETE:
return HttpMethod.DELETE;
case OPTIONS:
return HttpMethod.OPTIONS;
case HEAD:
return HttpMethod.HEAD;
default:
throw context.rorException("Unknown/unsupported http method");
}
}
public Set<String> getExpandedIndices() {
return getExpandedIndices(indices.getInitial());
}
public Set<String> getExpandedIndices(Set<String> ixsSet) {
if (doesInvolveIndices) {
// Index[] i = indexResolver.concreteIndices(clusterService.state(), IndicesOptions.lenientExpandOpen(), "a");
// String[] ixs = ixsSet.toArray(new String[ixsSet.size()]);
// String[] concreteIdxNames = indexResolver.concreteIndexNames(
// clusterService.state(),
// IndicesOptions.lenientExpandOpen(), ixs
// );
// return Sets.newHashSet(concreteIdxNames);
return new MatcherWithWildcards(ixsSet).filter(getAllIndicesAndAliases());
}
throw new ElasticsearchException("Cannot get expanded indices of a non-index request");
}
public Set<String> getIndices() {
if (!doesInvolveIndices) {
throw context.rorException("cannot get indices of a request that doesn't involve indices" + this);
}
return indices.getInitial();
}
public void setIndices(final Set<String> newIndices) {
if (!doesInvolveIndices) {
throw context.rorException("cannot set indices of a request that doesn't involve indices: " + this);
}
if (newIndices.size() == 0) {
if (isReadRequest()) {throw new ElasticsearchException(
"Attempted to set indices from [" + Joiner.on(",").join(indices.getInitial()) +
"] toempty set." +
", probably your request matched no index, or was rewritten to nonexistentindices (which would expand to empty set).");
}
else {
throw new ElasticsearchException(
"Attempted to set indices from [" + Joiner.on(",").join(indices.getInitial()) +
"] to empty set. " + "In ES, specifying no index is the same as full access, therefore this requestis forbidden." );
}
}
if (isReadRequest()) {
Set<String> expanded = getExpandedIndices(newIndices);
if (!expanded.isEmpty()) {
indices.mutate(expanded);
} else {
throw new IndexNotFoundException(
"rewritten indices not found: " + Joiner.on(",").join(newIndices)
, getIndices().iterator().next());
}
}
indices.mutate(newIndices);
}
public Boolean hasSubRequests() {
return !SubRequestContext.extractNativeSubrequests(actionRequest).isEmpty();
}
public Integer scanSubRequests(final ReflecUtils.CheckedFunction<IndicesRequestContext, Optional<IndicesRequestContext>> replacer) {
List<? extends IndicesRequest> subRequests = SubRequestContext.extractNativeSubrequests(actionRequest);
logger.info("found " + subRequests.size() + " subrequests");
// Composite request #TODO should we really prevent this?
if (!doesInvolveIndices) {
throw context.rorException("cannot replace indices of a composite request that doesn't involve indices: " + this);
}
Iterator<? extends IndicesRequest> it = subRequests.iterator();
while (it.hasNext()) {
IndicesRequestContext i = new SubRequestContext(this, it.next(), context);
final Optional<IndicesRequestContext> mutatedSubReqO;
// Empty optional = remove sub-request from the native list
try {
mutatedSubReqO = replacer.apply(i);
if (!mutatedSubReqO.isPresent()) {
it.remove();
continue;
}
} catch (IllegalAccessException e) {
e.printStackTrace();
throw new ElasticsearchSecurityException("error gathering indices to be replaced in sub-request " + i, e);
}
i = mutatedSubReqO.get();
// We are letting this pass, so let's commit it when we commit the sub-request.
i.delegateTo(this);
if (!i.getIndices().equals(i.getIndices())) {
i.setIndices(i.getIndices());
}
}
return subRequests.size();
}
public void setResponseHeader(String name, String value) {
Map<String, String> oldMap = responseHeaders.get();
oldMap.put(name, value);
responseHeaders.mutate(oldMap);
}
public Map<String, String> getHeaders() {
return this.requestHeaders;
}
public String getUri() {
return request.uri();
}
public String getAction() {
return action;
}
public Optional<LoggedUser> getLoggedInUser() {
return loggedInUser.get();
}
public void setLoggedInUser(LoggedUser user) {
loggedInUser.mutate(Optional.of(user));
}
@Override
public String toString() {
return toString(false);
}
private String toString(boolean skipIndices) {
String theIndices;
if (skipIndices || !doesInvolveIndices) {
theIndices = "<N/A>";
} else {
theIndices = Joiner.on(",").skipNulls().join(indices.get());
}
String content = getContent();
if (Strings.isNullOrEmpty(content)) {
content = "<N/A>";
}
String theHeaders;
if (!logger.isDebugEnabled()) {
theHeaders = Joiner.on(",").join(getHeaders().keySet());
} else {
theHeaders = getHeaders().toString();
}
String hist = Joiner.on(", ").join(history);
Optional<BasicAuth> optBasicAuth = BasicAuthUtils.getBasicAuthFromHeaders(getHeaders());
return "{ ID:" + id +
", TYP:" + actionRequest.getClass().getSimpleName() +
", USR:" + (loggedInUser.get().isPresent()
? loggedInUser.get().get()
: (optBasicAuth.isPresent() ? optBasicAuth.get().getUserName() + "(?)" : "[no basic auth header]")) +
", BRS:" + !Strings.isNullOrEmpty(requestHeaders.get("User-Agent")) +
", ACT:" + action +
", OA:" + getRemoteAddress() +
", IDX:" + theIndices +
", MET:" + request.method() +
", PTH:" + request.path() +
", CNT:" + (logger.isDebugEnabled() ? content : "<OMITTED, LENGTH=" + getContent().length() + ">") +
", HDR:" + theHeaders +
", HIS:" + hist +
" }";
}
}