com.novartis.opensource.yada.plugin.Gatekeeper.java Source code

Java tutorial

Introduction

Here is the source code for com.novartis.opensource.yada.plugin.Gatekeeper.java

Source

/**
 * Copyright 2016 Novartis Institutes for BioMedical Research Inc.
 * Licensed 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 com.novartis.opensource.yada.plugin;

import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;

import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONObject;

import com.novartis.opensource.yada.Finder;
import com.novartis.opensource.yada.JSONParams;
import com.novartis.opensource.yada.JSONParamsEntry;
import com.novartis.opensource.yada.YADAConnectionException;
import com.novartis.opensource.yada.YADAFinderException;
import com.novartis.opensource.yada.YADAQuery;
import com.novartis.opensource.yada.YADAQueryConfigurationException;
import com.novartis.opensource.yada.YADARequest;
import com.novartis.opensource.yada.plugin.AbstractPreprocessor;
import com.novartis.opensource.yada.plugin.YADASecurityException;
import com.novartis.opensource.yada.util.YADAUtils;

/**
 * A Preprocess plugin to evaluate user authorization for query execution.
 * 
 * @author David Varon
 * @since 7.0.0
 * 
 */
public class Gatekeeper extends AbstractPreprocessor {

    /**
     * Local logger handle
     */
    private static final Logger LOG = Logger.getLogger(Gatekeeper.class);
    /**
     * Constant equal to {@value}
     */
    protected static final String DEFAULT_AUTH_TOKEN_PROPERTY = "security.token";
    /**
     * Constant equal to {@value}
     */
    protected static final String EXECUTION_POLICY_COLUMNS = "execution.policy.columns";
    /**
     * Constant equal to {@value}
     */
    protected static final String EXECUTION_POLICY_INDICES = "execution.policy.indices";
    /**
     * Constant equal to {@value}
     */
    protected static final String EXECUTION_POLICY_INDEXES = "execution.policy.indexes";
    /**
     * Constant equal to {@value}
     */
    protected static final String CONTENT_POLICY_PREDICATE = "content.policy.predicate";
    /** 
     * Constant equal to {@value}
     * @since 8.1.0
     */
    protected static final String RX_COL_INJECTION = "(([a-zA-Z0-9_]+):)?(get[A-Z][a-zA-Z0-9_]+\\([A-Za-z0-9_]*\\))";
    /** 
     * Constant equal to {@value}
     * @since 8.1.0
     */
    protected static final String RX_IDX_INJECTION = "(([0-9]+):)?(get[A-Z][a-zA-Z0-9_]+\\([A-Za-z0-9_]*\\))";

    /**
     * Validates the request host, user, security params, and security query
     * execution results
     * 
     * @throws YADAPluginException
     *           , YADASecurityException
     * @see com.novartis.opensource.yada.plugin.AbstractPreprocessor#engage(com.novartis.opensource.yada.YADARequest,
     *      com.novartis.opensource.yada.YADAQuery)
     */
    @Override
    public void engage(YADARequest yReq, YADAQuery yq) throws YADAPluginException, YADASecurityException {
        super.engage(yReq, yq);
        try {
            validateYADARequest();
        } catch (Exception e) {
            String msg = "Unable to process security spec";
            throw new YADASecurityException(msg, e);
        }
    }

    /**
     * Overrides {@link TokenValidator#validate()}. Default sets token to value of
     * {@link #DEFAULT_AUTH_TOKEN_PROPERTY} system property.
     * 
     * @throws YADASecurityException
     *           when the {@link #DEFAULT_AUTH_TOKEN_PROPERTY} is not set
     */
    @Override
    public void validateToken() throws YADASecurityException {
        String token = System.getProperty(DEFAULT_AUTH_TOKEN_PROPERTY);
        if (token == null || token.equals(""))
            throw new YADASecurityException(
                    "Unauthorized. " + DEFAULT_AUTH_TOKEN_PROPERTY + " system property not set.");
        setToken(token);
    }

    /**
     * Returns {@code true} if {@link #WHITELIST} or {@link #BLACKLIST} is stored
     * in the {@code YSEC_PARAMS} table corresponding to the security target
     * 
     * @param policy
     *          the value of the {@code YSEC_PARAM_NAME} field in the
     *          {@code YSEC_PARAMS} table
     * @return {@code true} if {@link #WHITELIST} or {@link #BLACKLIST} is set
     */
    protected boolean hasValidPolicy(String policy) {
        return isWhitelist(policy) || isBlacklist(policy);
    }

    /**
     * Retrieves and processes the security query, and validates the results per
     * the security specification
     * 
     * @param spec
     *          the security specification for the requested query
     * @throws YADASecurityException
     *           when there is an issue retrieving or processing the security
     *           query           
     */
    @Override
    public void applyExecutionPolicy() throws YADASecurityException {

        //TODO the security query executes for every iteration of the qname 
        // in the current request. a flag needs to be set somewhere to indicate
        // clearance has already been granted.  This can't be in YADAQuery because of caching.

        //TODO needs to support app targets as well as qname targets

        //TODO tests for auth failure, i.e., unauthorized
        //TODO tests for ignoring attempted plugin overrides
        //TODO make it impossible to execute a protector query as a primary query without a server-side flag set, or 
        //  perhaps some authorization (i.e., for testing, maybe with a content policy)  
        //  This will close an attack vector.
        //TODO support dependency injection for other methods in addition to token for execution policy

        List<SecurityPolicyRecord> spec = getSecurityPolicyRecords(EXECUTION_POLICY_CODE);
        List<SecurityPolicyRecord> prunedSpec = new ArrayList<>();
        // process security spec

        // query can be standard or json
        //  if json, need name of column to map to token
        //  if standard, need list of relevant indices

        String policyColumns = getArgumentValue(EXECUTION_POLICY_COLUMNS);
        String policyIndices = getArgumentValue(EXECUTION_POLICY_INDICES);
        policyIndices = policyIndices == null ? getArgumentValue(EXECUTION_POLICY_INDEXES) : policyIndices;
        String polColParams_rx = "^((" + RX_IDX_INJECTION + "|[\\d]+)\\s?)+$";
        String polColJSONParams_rx = "^((" + RX_COL_INJECTION + "|[A-Za-z0-9_]+)\\s?)+$";
        String result = "";
        int index = -1;
        String injectedIndex = "";
        boolean policyHasParams = false;
        boolean policyHasJSONParams = false;
        boolean reqHasParams = getYADARequest().getParams() == null || getYADARequest().getParams().length == 0
                ? false
                : true;
        boolean reqHasJSONParams = YADAUtils.hasJSONParams(getYADARequest());

        for (SecurityPolicyRecord secRec : spec) {
            // Are params required for security query?
            if (policyIndices != null && policyIndices.matches(polColParams_rx)) {
                policyHasParams = true;
            }

            if (policyColumns != null && policyColumns.matches(polColJSONParams_rx)) {
                policyHasJSONParams = true;
            }

            // request and policy must have syntax compatibility, i.e., matching param syntax, or no params
            if ((policyHasParams && !reqHasJSONParams) || (policyHasJSONParams && !reqHasParams)
                    || (!policyHasParams && reqHasJSONParams) || (!policyHasJSONParams && reqHasParams)
                    || !(policyHasParams || reqHasParams || policyHasJSONParams || reqHasJSONParams)) {
                // confirm sec spec is config properly
                if (hasValidPolicy(secRec.getType())) // whitelist or blacklist
                {
                    // confirm sec spec is mapped to requested query
                    try {
                        new Finder().getQuery(secRec.getA11nQname());
                    } catch (YADAFinderException e) {
                        String msg = "Unauthorized. Authorization qname not found.";
                        throw new YADASecurityException(msg);
                    } catch (YADAConnectionException | YADAQueryConfigurationException e) {
                        String msg = "Unauthorized. Unable to check for security query. This could be a temporary issue.";
                        throw new YADASecurityException(msg, e);
                    }
                    // security query exists
                } else {
                    String msg = "Unauthorized, due to policy misconfiguration. Must be \"blacklist\" or \"whitelist.\"";
                    throw new YADASecurityException(msg);
                }
                prunedSpec.add(secRec);
            }
        }

        // kill the query if there aren't any compatible specs
        if (prunedSpec.size() == 0) {
            String msg = "Unauthorized. Request parameter syntax is incompatible with policy.";
            throw new YADASecurityException(msg);
        }

        // process the relevant specs
        for (SecurityPolicyRecord secRec : prunedSpec) // policy code (E,C), policy type (white,black), target (qname), A11nqname
        {
            String a11nQname = secRec.getA11nQname();
            String policyType = secRec.getType();

            // policy has params and req has compatible params 
            if (policyHasParams && !reqHasJSONParams) {
                @SuppressWarnings("null")
                String[] polCols = policyIndices.split("\\s");
                StringBuilder polVals = new StringBuilder();

                if (reqHasParams) {
                    for (int i = 0; i < polCols.length; i++) {
                        // handle as params
                        // 1. get params from query
                        List<String> vals = getYADAQuery().getVals(0);
                        try {
                            index = Integer.parseInt(polCols[i]);
                        } catch (NumberFormatException e) {
                            injectedIndex = polCols[i];
                        }
                        // 2. pass user column
                        if (polVals.length() > 0)
                            polVals.append(",");

                        if (injectedIndex.equals("") && index > -1) {
                            if (index >= vals.size())
                                polVals.append((String) getToken());
                            else
                                polVals.append(vals.get(index));
                        } else {
                            Pattern rxInjection = Pattern.compile(RX_IDX_INJECTION);
                            Matcher m1 = rxInjection.matcher(injectedIndex);
                            if (m1.matches() && m1.groupCount() == 3) // injection
                            {
                                // parse regex: this is where the method value is injected
                                String colIdx = m1.group(2);
                                String colval = m1.group(3);

                                // find and execute injected method
                                String method = colval.substring(0, colval.indexOf('('));
                                String arg = colval.substring(colval.indexOf('(') + 1, colval.indexOf(')'));
                                Object val = null;
                                try {
                                    if (arg.equals(""))
                                        val = getClass().getMethod(method).invoke(this, new Object[] {});
                                    else
                                        val = getClass().getMethod(method, new Class[] { java.lang.String.class })
                                                .invoke(this, new Object[] { arg });
                                } catch (NoSuchMethodException | SecurityException | IllegalAccessException
                                        | IllegalArgumentException | InvocationTargetException e) {
                                    String msg = "Unathorized. Injected method invocation failed.";
                                    throw new YADASecurityException(msg, e);
                                }

                                // add/replace item in dataRow 
                                polVals.append(val);
                            }
                        }
                        index = -1;
                        injectedIndex = "";
                    }
                    // 3. execute the security query
                    result = YADAUtils.executeYADAGet(new String[] { a11nQname },
                            new String[] { polVals.toString() });
                } else {
                    for (int i = 0; i < polCols.length; i++) {
                        polVals.append((String) getToken());
                    }
                    result = YADAUtils.executeYADAGet(new String[] { a11nQname },
                            new String[] { polVals.toString() });
                }
            }
            // policy has JSONParams and req has compatible JSONParams
            else if (policyHasJSONParams && reqHasJSONParams) {
                LOG.warn("Could not parse column value into integer -- it's probably a String");
                // handle as JSONParams
                // 1. get JSONParams from query (params)
                LinkedHashMap<String, String[]> dataRow = getYADAQuery().getDataRow(0);
                // 2. add user column if necessary
                @SuppressWarnings("null")
                String[] polCols = policyColumns.split("\\s");
                for (String colspec : polCols) {
                    // dataRow can look like, e.g.: {COL1:val1,COL2:val2}
                    // polCols can look like, e.g.: COL2 APP:getValue(TARGET)

                    Pattern rxInjection = Pattern.compile(RX_COL_INJECTION);
                    Matcher m1 = rxInjection.matcher(colspec);
                    if (m1.matches() && m1.groupCount() == 3) // injection
                    {
                        // parse regex: this is where the method value is injected
                        String colname = m1.group(2);
                        String colval = m1.group(3);

                        // find and execute injected method
                        String method = colval.substring(0, colval.indexOf('('));
                        String arg = colval.substring(colval.indexOf('(') + 1, colval.indexOf(')'));
                        Object val = null;
                        try {
                            if (arg.equals(""))
                                val = getClass().getMethod(method).invoke(this, new Object[] {});
                            else
                                val = getClass().getMethod(method, new Class[] { java.lang.String.class })
                                        .invoke(this, new Object[] { arg });
                        } catch (NoSuchMethodException | SecurityException | IllegalAccessException
                                | IllegalArgumentException | InvocationTargetException e) {
                            String msg = "Unathorized. Injected method invocation failed.";
                            throw new YADASecurityException(msg, e);
                        }

                        // add/replace item in dataRow 
                        dataRow.put(colname, new String[] { (String) val });
                    } else {
                        if (!dataRow.containsKey(colspec)) // no injection AND no parameter
                        {
                            String msg = "Unathorized. Injected method invocation failed.";
                            throw new YADASecurityException(msg);
                        }
                    }
                }

                // 3. execute the security query
                JSONParamsEntry jpe = new JSONParamsEntry();
                // dataRow now contains injected values () or passed values
                // if values were injected, they've overwritten the passed in version
                jpe.addData(dataRow);
                JSONParams jp = new JSONParams(a11nQname, jpe);
                result = YADAUtils.executeYADAGetWithJSONParamsNoStats(jp);
            } else {
                // no parameters to pass to execution.policy query
                result = YADAUtils.executeYADAGet(new String[] { a11nQname }, new String[0]);
            }
            // parse result
            int count = new JSONObject(result).getJSONObject("RESULTSET").getInt("records");

            // Reject if necessary
            if ((isWhitelist(policyType) && count == 0) || (isBlacklist(policyType) && count > 0))
                throw new YADASecurityException("Unauthorized.");
        }

        this.clearSecurityPolicy();
    }

    /**
     * Modifies the original query by appending a dynamic predicate
     * <p>Recall the {@link Service#engagePreprocess} method
     * will recall {@link QueryManager#endowQuery} to 
     * reconform the code after this {@link Preprocess} 
     * disengages.
     * 
     * 
     * @throws YADASecurityException when token retrieval fails
     */
    @Override
    public void applyContentPolicy() throws YADASecurityException {

        // TODO make it impossible to reset args and preargs dynamically if pl class implements SecurityPolicy
        //   this will close an attack vector

        String SPACE = " ";
        StringBuilder contentPolicy = new StringBuilder();
        Pattern rxInjection = Pattern.compile(RX_COL_INJECTION);
        String rawPolicy = getArgumentValue(CONTENT_POLICY_PREDICATE);
        Matcher m1 = rxInjection.matcher(rawPolicy);
        int start = 0;

        // field = getToken
        // field = getCookie(string)
        // field = getHeader(string)
        // field = getUser()
        // field = getRandom(string)

        if (!m1.find()) {
            String msg = "Unathorized. Injected method invocation failed.";
            throw new YADASecurityException(msg);
        }

        m1.reset();

        while (m1.find()) {
            int rxStart = m1.start();
            int rxEnd = m1.end();

            contentPolicy.append(rawPolicy.substring(start, rxStart));

            String frag = rawPolicy.substring(rxStart, rxEnd);
            String method = frag.substring(0, frag.indexOf('('));
            String arg = frag.substring(frag.indexOf('(') + 1, frag.indexOf(')'));
            Object val = null;
            try {
                if (arg.equals(""))
                    val = getClass().getMethod(method).invoke(this, new Object[] {});
                else
                    val = getClass().getMethod(method, new Class[] { java.lang.String.class }).invoke(this,
                            new Object[] { arg });
            } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
                    | InvocationTargetException e) {
                String msg = "Unathorized. Injected method invocation failed.";
                throw new YADASecurityException(msg, e);
            }
            contentPolicy.append((String) val + SPACE);

            start = rxEnd;
        }

        Expression parsedContentPolicy;
        try {
            parsedContentPolicy = CCJSqlParserUtil.parseCondExpression(contentPolicy.toString());
        } catch (JSQLParserException e) {
            String msg = "Unauthorized. Content policy is not valid.";
            throw new YADASecurityException(msg, e);
        }

        PlainSelect sql = (PlainSelect) ((Select) getYADAQuery().getStatement()).getSelectBody();
        Expression where = sql.getWhere();

        if (where != null) {
            AndExpression and = new AndExpression(where, parsedContentPolicy);
            sql.setWhere(and);
        } else {
            sql.setWhere(parsedContentPolicy);
        }
        try {
            CCJSqlParserManager parserManager = new CCJSqlParserManager();
            sql = (PlainSelect) ((Select) parserManager.parse(new StringReader(sql.toString()))).getSelectBody();
        } catch (JSQLParserException e) {
            String msg = "Unauthorized. Content policy is not valid.";
            throw new YADASecurityException(msg, e);
        }

        getYADAQuery().setCoreCode(sql.toString());
        this.clearSecurityPolicy();
    }

    /**
     * Utility function for content policy
     * @return the auth token wrapped in single quotes
     * @throws YADASecurityException
     */
    public String getQToken() throws YADASecurityException {
        String quote = "'";
        return quote + getToken() + quote;
    }

    /**
     * Utility function for content policy
     * @return the auth token wrapped in single quotes
     * @throws YADASecurityException
     * @since 8.1.0
     */
    public String getQLoggedUser() throws YADASecurityException {
        String user = ((JSONArray) getSessionAttribute("YADA.user.privs")).getJSONObject(0).getString("UID");
        String quote = "'";
        return quote + user + quote;
    }

    /**
     * Utility function for content policy
     */

    /**
     * Utility function for content policy
     * @param cookie the desired HTTP request cookie
     * @return the value of {@code cookie} wrapped in single quotes
     * @throws YADASecurityException
     */
    public String getQCookie(String cookie) {
        String quote = "'";
        String val = super.getCookie(cookie);
        return quote + val + quote;
    }

    /**
     * Utility function for content policy
     * @param header the desired HTTP request header
     * @return the value of {@code header} wrapped in single quotes
     * @throws YADASecurityException
     */
    public String getQHeader(String header) {
        String quote = "'";
        String val = super.getHeader(header);
        return quote + val + quote;
    }

    /**
     * Sets the local {@link TokenValidator} to {@code this}
     */
    @Override
    public void setTokenValidator() throws YADASecurityException {
        setTokenValidator(this);
    }

}