001/*
002 * (C) Copyright 2015-2020 Nuxeo (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    /** @deprecated since 11.4, types have been removed since Elasticsearch 7.x */
050    @Deprecated(since = "11.4", forRemoval = true)
051    protected String types;
052    protected String indices;
053    protected NuxeoPrincipal principal;
054    protected String url;
055    protected String filteredPayload;
056
057    public AbstractSearchRequestFilterImpl() {
058
059    }
060
061    @Override
062    public void init(CoreSession session, String indices, String rawQuery, String payload) {
063        RequestValidator validator = new RequestValidator();
064        this.indices = validator.getIndices(indices);
065        this.principal = session.getPrincipal();
066        this.rawQuery = rawQuery;
067        this.payload = payload;
068        if (payload == null && !principal.isAdministrator()) {
069            // here we turn the UriSearch query_string into a body search
070            extractPayloadFromQuery();
071        }
072    }
073
074    @Override
075    public String getTypes() {
076        return types;
077    }
078
079    @Override
080    public String getIndices() {
081        return indices;
082    }
083
084    @Override
085    public String toString() {
086        if (payload == null || payload.isEmpty()) {
087            return "Uri Search: " + getUrl() + " user: " + principal;
088        }
089        try {
090            return "Body Search: " + getUrl() + " user: " + principal + " payload: " + getPayload();
091        } catch (JSONException e) {
092            return "Body Search: " + getUrl() + " user: " + principal + " invalid JSON payload: " + e.getMessage();
093        }
094    }
095
096    @Override
097    public @NotNull String getUrl() {
098        if (url == null) {
099            url = "/" + indices + "/_search";
100            if (rawQuery != null) {
101                url += "?" + rawQuery;
102            }
103        }
104        return url;
105    }
106
107    @Override
108    public abstract String getPayload() throws JSONException;
109
110    protected Map<String, String> getQueryMap() {
111        String[] params = rawQuery.split("&");
112        Map<String, String> map = new HashMap<>();
113        for (String param : params) {
114            String name = param.split("=")[0];
115            if (param.contains("=")) {
116                map.put(name, param.split("=")[1]);
117            } else {
118                map.put(name, "");
119            }
120        }
121        return map;
122    }
123
124    protected void setRawQuery(Map<String, String> map) {
125        StringBuilder sb = new StringBuilder();
126        for (Map.Entry<String, String> entry : map.entrySet()) {
127            if (sb.length() > 0) {
128                sb.append("&");
129            }
130            if (entry.getValue().isEmpty()) {
131                sb.append(entry.getKey());
132            } else {
133                sb.append(String.format("%s=%s", entry.getKey(), entry.getValue()));
134            }
135        }
136        rawQuery = sb.toString();
137    }
138
139    protected void extractPayloadFromQuery() {
140        Map<String, String> qm = getQueryMap();
141        String queryString = qm.remove("q");
142        if (queryString == null) {
143            payload = MATCH_ALL;
144            return;
145        }
146        try {
147            queryString = URLDecoder.decode(queryString, "UTF-8");
148        } catch (UnsupportedEncodingException e) {
149            throw new IllegalArgumentException("Invalid URI Search query_string encoding: " + e.getMessage());
150        }
151        String defaultField = qm.remove("df");
152        if (defaultField == null) {
153            defaultField = "_all";
154        }
155        String defaultOperator = qm.remove("default_operator");
156        if (defaultOperator == null) {
157            defaultOperator = "OR";
158        }
159        payload = String.format(QUERY_STRING, queryString, defaultField, defaultOperator);
160        setRawQuery(qm);
161    }
162}