001package org.nuxeo.elasticsearch.http.readonly;/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Benoit Delbosc
016 */
017
018import java.io.UnsupportedEncodingException;
019import java.net.URLDecoder;
020import java.util.HashMap;
021import java.util.Map;
022
023import javax.validation.constraints.NotNull;
024
025import org.json.JSONException;
026import org.nuxeo.ecm.core.api.CoreSession;
027import org.nuxeo.ecm.core.api.NuxeoPrincipal;
028import org.nuxeo.elasticsearch.http.readonly.filter.RequestValidator;
029import org.nuxeo.elasticsearch.http.readonly.filter.SearchRequestFilter;
030
031/**
032 * Rewrite an Elsaticsearch search request to add security filter.
033 *
034 * URI Search are turned into Request body search.
035 *
036 * @since 7.3
037 */
038public abstract class AbstractSearchRequestFilterImpl implements SearchRequestFilter {
039
040    protected static final String MATCH_ALL = "{\"query\": {\"match_all\": {}}}";
041    protected static final String QUERY_STRING = "{\"query\":{\"query_string\":{\"query\":\"%s\",\"default_field\":\"%s\",\"default_operator\":\"%s\"}}}";
042    protected static final String BACKSLASH_MARKER = "_@@_";
043
044    protected String payload;
045    protected String rawQuery;
046    protected String types;
047    protected String indices;
048    protected NuxeoPrincipal principal;
049    protected String url;
050    protected String filteredPayload;
051
052    public AbstractSearchRequestFilterImpl() {
053
054    }
055
056    public void init(CoreSession session, String indices, String types, String rawQuery, String payload) {
057        RequestValidator validator = new RequestValidator();
058        this.indices = validator.getIndices(indices);
059        this.types = validator.getTypes(indices, types);
060        this.principal = (NuxeoPrincipal) session.getPrincipal();
061        this.rawQuery = rawQuery;
062        this.payload = payload;
063        if (payload == null && !principal.isAdministrator()) {
064            // here we turn the UriSearch query_string into a body search
065            extractPayloadFromQuery();
066        }
067    }
068
069    public String getTypes() {
070        return types;
071    }
072
073    public String getIndices() {
074        return indices;
075    }
076
077    @Override
078    public String toString() {
079        if (payload == null || payload.isEmpty()) {
080            return "Uri Search: " + getUrl() + " user: " + principal;
081        }
082        try {
083            return "Body Search: " + getUrl() + " user: " + principal + " payload: " + getPayload();
084        } catch (JSONException e) {
085            return "Body Search: " + getUrl() + " user: " + principal + " invalid JSON payload: " + e.getMessage();
086        }
087    }
088
089    public @NotNull String getUrl() {
090        if (url == null) {
091            url = "/" + indices + "/" + types + "/_search";
092            if (rawQuery != null) {
093                url += "?" + rawQuery;
094            }
095        }
096        return url;
097    }
098
099    public abstract String getPayload() throws JSONException;
100
101    protected Map<String, String> getQueryMap() {
102        String[] params = rawQuery.split("&");
103        Map<String, String> map = new HashMap<>();
104        for (String param : params) {
105            String name = param.split("=")[0];
106            if (param.contains("=")) {
107                map.put(name, param.split("=")[1]);
108            } else {
109                map.put(name, "");
110            }
111        }
112        return map;
113    }
114
115    protected void setRawQuery(Map<String, String> map) {
116        StringBuilder sb = new StringBuilder();
117        for (Map.Entry<String, String> entry : map.entrySet()) {
118            if (sb.length() > 0) {
119                sb.append("&");
120            }
121            if (entry.getValue().isEmpty()) {
122                sb.append(entry.getKey());
123            } else {
124                sb.append(String.format("%s=%s", entry.getKey(), entry.getValue()));
125            }
126        }
127        rawQuery = sb.toString();
128    }
129
130    protected void extractPayloadFromQuery() {
131        Map<String, String> qm = getQueryMap();
132        String queryString = qm.remove("q");
133        if (queryString == null) {
134            payload = MATCH_ALL;
135            return;
136        }
137        try {
138            queryString = URLDecoder.decode(queryString, "UTF-8");
139        } catch (UnsupportedEncodingException e) {
140            throw new IllegalArgumentException("Invalid URI Search query_string encoding: " + e.getMessage());
141        }
142        String defaultField = qm.remove("df");
143        if (defaultField == null) {
144            defaultField = "_all";
145        }
146        String defaultOperator = qm.remove("default_operator");
147        if (defaultOperator == null) {
148            defaultOperator = "OR";
149        }
150        payload = String.format(QUERY_STRING, queryString, defaultField, defaultOperator);
151        setRawQuery(qm);
152    }
153}