Source: src/jsSecurity.js

/*globals ObjectRow,Environment,sqlFun ,SqlFormatter*/
/**
 * Manages data storing authorizations
 * @module Security
 */

/* jshint -W116 */
const dsSpace = require('./../client/components/metadata/jsDataSet');
const DataRowState = dsSpace.dataRowState;
const DataSet = dsSpace.DataSet;
const QParser = require('./jsDataQueryParser').JsDataQueryParser;
const Q = require('./../client/components/metadata/jsDataQuery');
const _ = require("lodash");

/**
 * if defaultIsDeny the meaning is allow and not deny. If deny is not set then the meaning is allow
 * Otherwise the meaning is not deny or allow. If allow is not set then the meaning is not deny
 * @typedef {Object} ConditionRow
 * @property {object} idcustomgroup id of the user group this condition belongs to
 * @property {string} tablename
 * @property {string} op I/U/D/S/P
 * @property {boolean} defaultIsDeny if true default is deny all, otherwise is allow all
 * @property {sqlFun} [denyCondition]
 * @property {sqlFun} [allowCondition]
 */


/**
 *
 * @typedef Deferred
 */
const    Deferred = require("JQDeferred");


/**
 * Class that manages a set of conditions
 * @class Security
 * @method Security
 * @constructor
 */
function Security(){
 this.groupOperations= [];
 /* {{sqlFun[]}} */
 this.tableOpConditions = {};
 /* Dictionary.<string,Array.<sqlFun>>*/
 this.filteredConditions= {};
}


Security.prototype= {
    constructor: Security,

    /**
     * Adds condition for a specified table/operation combination, merging to existent.
     * Every condition
     * @param {string}tableName
     * @param {string} op
     * @param {ConditionRow} condition
     */
    addTableOpCondition:function(tableName,op, condition){
        let key = tableName+'#'+op;
        let list=this.tableOpConditions[key];
        if (list===undefined){
            list = [];
            this.tableOpConditions[key]=list;
        }
        this.tableOpConditions[key].push(condition);
    },

    /**
     * Get all table/operation conditions for any environment
     * @param {string} tableName
     * @param {string} op
     * @return {Array.<ConditionRow>}
     */
    getTableOpConditions:function(tableName,op){
        let key = tableName+'#'+op;
        let list=this.tableOpConditions[key];
        if (list===undefined){
            return [];
        }
        return  list;
    },

    /**
     * Gets security conditions for an operation in the specified environment context.
     * Those are filtered basing on usergrouplist of the environment, that has to include
     *  idcustomgroup property value of the ConditionRow. If environment does not contain idcustomgroup
     *   field, then all matching table/op conditions are taken
     *
     * @param tableName
     * @param {string} op
     * @param  {Environment} env
     * @return {ConditionRow[]}
     */
    getConditions: function (tableName, op,env){
        /*{object []} */
        let idgroups= env.usr.usergrouplist;
        let key = tableName+'#'+op+'#'+idgroups.join('§');
        if (this.filteredConditions[key]!==undefined){
            return this.filteredConditions[key];
        }
        let conditions= _.filter(this.getTableOpConditions(tableName,op),
                    Q.or(Q.isnull("idcustomgroup"),Q.isIn("idcustomgroup",idgroups))
                );
        this.filteredConditions[key]=conditions;
        return  conditions;
    }

};
/**
 * Evaluates the SecurityCondition about a combination of tableName/opKind in the specified environment
 * Requires environment.sys("idcustomgroup") to have been already evaluated
 * @param {string} tableName
 * @param {string} opKind
 * @param {Environment} environment
 * @return {sqlFun}
 */
Security.prototype.securityCondition= function (tableName, opKind, environment){
    let conditions= this.getConditions(tableName,opKind,environment.sys("idcustomgroup"));
    let thereIsAnyCondition= false;
    let clauses=[];
    let defaultIsDeny ;
    conditions.forEach(r=>{
         thereIsAnyCondition = thereIsAnyCondition || (r.allow || r.deny);

        if (r.defaultIsDeny){
            //default is DENY, allow indicates the rows allowed
            defaultIsDeny=true;
            //current operation default is deny
            if (!r.allow) return;       //skip this, it is always false
            if (r.allow.isTrue){
                clauses.push(r.allow); //overall result will be true
                return;
            }
            if (!r.deny) {
                clauses.push(r.allow);
                return;
            }
            clauses.push(Q.and(r.allow,Q.not(r.deny)));
        }
        else {
            //default is ALLOW, deny indicates the rows prohibited
            defaultIsDeny=false;
            if (r.allow && r.allow.isTrue)return;//exception is always true so the overall
            if (r.deny){
                if (r.allow){
                    clauses.push(r.allow);
                    clauses.push(Q.not(r.deny)); //they will be or-ed in the result
                }
                else {
                    clauses.push(Q.not(r.deny));
                }
            }
        }
    });
    if (clauses.length===0){
        return  Q.constant(true);
    }
    if (clauses.length===1){
        return  clauses[0];
    }
    return  Q.or(clauses);
};

/**
 *
 * @param {ObjectRow} r
 * @param {Environment} env
 */
Security.prototype.canPost= function(r, env){
    let opkind=null;
    let DR = r.getRow();
    if (DR.table.skipSecurity()){
      return true;
    }

    switch (DR.state){
        case DataRowState.added:
            opkind= "I";
            break;
        case DataRowState.modified:
            opkind= "U";
            break;
        case DataRowState.deleted:
            opkind="D";
            break;
    }
    let filter = this.securityCondition(DR.table.name, opkind, env);
    return filter(r,env);
};

/**
 * Reads all data from customgroupoperation and compiles expression strings into sqlFun.
 * Creates ConditionRow items from rows stored on db, in order to create a Security object
 * @param {DataAccess} conn
 * @constructor
 * @returns {Promise<Security>}
 */
function SecurityProvider(conn){
     return conn.select({
        tableName:"customgroupoperation"
    })
        .then(t=>{
            let S = new Security();
            t.forEach(r=>{
                if (r.allowcondition){
                    if (r.allowcondition.startsWith("AND(")){
                        r.allowcondition = r.allowcondition.substr(3);
                    }
                    r.allow= QParser.prototype.from(r.allowcondition);
                }
                if (r.denycondition){
                    if (r.denycondition.startsWith("AND(")){
                        r.denycondition = r.allowcondition.substr(3);
                    }
                    r.deny= QParser.prototype.from(r.denycondition);
                }
                r.defaultIsDeny = (r.defaultisdeny.toUpperCase()==="S");
                S.addTableOpCondition(r.tablename,r.operation,r);
            });
            return S;
        });

}

module.exports = SecurityProvider;