com.novartis.opensource.yada.plugin.Gatekeeper Maven / Gradle / Ivy
/**
* 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.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 0.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";
/**
* 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 spec = getSecurityPolicyRecords(EXECUTION_POLICY_CODE);
List 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 = "^([\\d]+\\s?)+$";
String polColJSONParams_rx = "^([A-Za-z0-9_]+\\s?)+$";
String result = "";
int index = -1;
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;
}
// (hasParams || hasJSONParams) == true or false
// 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");
//String[] polVals = new String[polCols.length];
StringBuilder polVals = new StringBuilder();
if(reqHasParams)
{
for (int i = 0; i < polCols.length; i++)
{
// handle as params
// 1. get params from query
List vals = getYADAQuery().getVals(0);
index = Integer.parseInt(polCols[i]);
// 2. pass user column
if(polVals.length() > 0)
polVals.append(",");
if(index >= vals.size())
polVals.append((String)getToken());
else
polVals.append(vals.get(index));
}
// 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 dataRow = getYADAQuery().getDataRow(0);
// 2. add user column if necessary
@SuppressWarnings("null")
String[] polCols = policyColumns.split("\\s");
for(String colname : polCols)
{
if(!dataRow.containsKey(colname))
{
dataRow.put(colname, new String[] {(String)getToken()});
break;
}
}
// 3. execute the security query
JSONParamsEntry jpe = new JSONParamsEntry();
jpe.addData(dataRow);
JSONParams jp = new JSONParams(a11nQname, jpe);
result = YADAUtils.executeYADAGetWithJSONParams(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
* 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();
String RX_INJECTION = "get[A-Z][a-zA-Z0-9_]+\\([A-Za-z0-9_]*\\)";
Pattern rxInjection = Pattern.compile(RX_INJECTION);
String rawPolicy = getArgumentValue(CONTENT_POLICY_PREDICATE);
Matcher m1 = rxInjection.matcher(rawPolicy);
int start = 0;
// field = getToken
// field = getCookie(string)
// field = getHeader(string)
// 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
* @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);
}
}