001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Benoit Delbosc
018 */
019package org.nuxeo.elasticsearch.http.readonly;
020
021import java.io.UnsupportedEncodingException;
022import java.net.URLDecoder;
023import java.util.HashMap;
024import java.util.Map;
025
026import javax.validation.constraints.NotNull;
027
028import org.json.JSONException;
029import org.nuxeo.ecm.core.api.CoreSession;
030import org.nuxeo.ecm.core.api.NuxeoPrincipal;
031import org.nuxeo.elasticsearch.http.readonly.filter.RequestValidator;
032import org.nuxeo.elasticsearch.http.readonly.filter.SearchRequestFilter;
033
034/**
035 * Rewrite an Elsaticsearch search request to add security filter.
036 *
037 * URI Search are turned into Request body search.
038 *
039 * @since 7.3
040 */
041public abstract class AbstractSearchRequestFilterImpl implements SearchRequestFilter {
042
043    protected static final String MATCH_ALL = "{\"query\": {\"match_all\": {}}}";
044    protected static final String QUERY_STRING = "{\"query\":{\"query_string\":{\"query\":\"%s\",\"default_field\":\"%s\",\"default_operator\":\"%s\"}}}";
045    protected static final String BACKSLASH_MARKER = "_@@_";
046
047    protected String payload;
048    protected String rawQuery;
049    protected String types;
050    protected String indices;
051    protected NuxeoPrincipal principal;
052    protected String url;
053    protected String filteredPayload;
054
055    public AbstractSearchRequestFilterImpl() {
056
057    }
058
059    public void init(CoreSession session, String indices, String types, String rawQuery, String payload) {
060        RequestValidator validator = new RequestValidator();
061        this.indices = validator.getIndices(indices);
062        this.types = validator.getTypes(this.indices, types);
063        this.principal = (NuxeoPrincipal) session.getPrincipal();
064        this.rawQuery = rawQuery;
065        this.payload = payload;
066        if (payload == null && !principal.isAdministrator()) {
067            // here we turn the UriSearch query_string into a body search
068            extractPayloadFromQuery();
069        }
070    }
071
072    public String getTypes() {
073        return types;
074    }
075
076    public String getIndices() {
077        return indices;
078    }
079
080    @Override
081    public String toString() {
082        if (payload == null || payload.isEmpty()) {
083            return "Uri Search: " + getUrl() + " user: " + principal;
084        }
085        try {
086            return "Body Search: " + getUrl() + " user: " + principal + " payload: " + getPayload();
087        } catch (JSONException e) {
088            return "Body Search: " + getUrl() + " user: " + principal + " invalid JSON payload: " + e.getMessage();
089        }
090    }
091
092    public @NotNull String getUrl() {
093        if (url == null) {
094            url = "/" + indices + "/" + types + "/_search";
095            if (rawQuery != null) {
096                url += "?" + rawQuery;
097            }
098        }
099        return url;
100    }
101
102    public abstract String getPayload() throws JSONException;
103
104    protected Map<String, String> getQueryMap() {
105        String[] params = rawQuery.split("&");
106        Map<String, String> map = new HashMap<>();
107        for (String param : params) {
108            String name = param.split("=")[0];
109            if (param.contains("=")) {
110                map.put(name, param.split("=")[1]);
111            } else {
112                map.put(name, "");
113            }
114        }
115        return map;
116    }
117
118    protected void setRawQuery(Map<String, String> map) {
119        StringBuilder sb = new StringBuilder();
120        for (Map.Entry<String, String> entry : map.entrySet()) {
121            if (sb.length() > 0) {
122                sb.append("&");
123            }
124            if (entry.getValue().isEmpty()) {
125                sb.append(entry.getKey());
126            } else {
127                sb.append(String.format("%s=%s", entry.getKey(), entry.getValue()));
128            }
129        }
130        rawQuery = sb.toString();
131    }
132
133    protected void extractPayloadFromQuery() {
134        Map<String, String> qm = getQueryMap();
135        String queryString = qm.remove("q");
136        if (queryString == null) {
137            payload = MATCH_ALL;
138            return;
139        }
140        try {
141            queryString = URLDecoder.decode(queryString, "UTF-8");
142        } catch (UnsupportedEncodingException e) {
143            throw new IllegalArgumentException("Invalid URI Search query_string encoding: " + e.getMessage());
144        }
145        String defaultField = qm.remove("df");
146        if (defaultField == null) {
147            defaultField = "_all";
148        }
149        String defaultOperator = qm.remove("default_operator");
150        if (defaultOperator == null) {
151            defaultOperator = "OR";
152        }
153        payload = String.format(QUERY_STRING, queryString, defaultField, defaultOperator);
154        setRawQuery(qm);
155    }
156}