/**
* MOST Web Framework
* A JavaScript Web Framework
* http://themost.io
* Created by Kyriakos Barbounakis<k.barbounakis@gmail.com> on 2014-06-19.
*
* Copyright (c) 2014, Kyriakos Barbounakis k.barbounakis@gmail.com
Anthi Oikonomou anthioikonomou@gmail.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of MOST Web Framework nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @private
*/
var util=require('util'),
array = require('most-array'),
qry = require('most-query'),
async = require('async'),
types = require('./types'),
_ = require("lodash"),
dataCache = require('./data-cache'),
common = require('./data-common');
/**
* @class
* @constructor
* @private
* @ignore
*/
function EachSeriesCancelled() {
//
}
/**
* @class
* @constructor
*/
function DataPermissionEventArgs() {
/**
* The target data model
* @type {DataModel}
*/
this.model = null;
/**
* The underlying query expression
* @type {QueryExpression}
*/
this.query = null;
/**
* The permission mask
* @type {Number}
*/
this.mask = null;
/**
* The query type
* @type {String}
*/
this.type = null;
/**
* The query type
* @type {String}
*/
this.privilege = null;
/**
* The data queryable object that emits the event.
* @type {DataQueryable|*}
*/
this.emitter = null;
}
/**
* An enumeration of the available permission masks
* @enum {number}
*/
var PermissionMask = {
/**
* Read Access Mask (1)
*/
Read:1,
/**
* Create Access Mask (2)
*/
Create:2,
/**
* Update Access Mask (4)
*/
Update:4,
/**
* Delete Access Mask (8)
*/
Delete:8,
/**
* Execute Access Mask (16)
*/
Execute:16,
/**
* Full Access Mask (31)
*/
Owner:31
};
/**
* @class
* @constructor
*/
function DataPermissionEventListener() {
//
}
/**
* Occurs before creating or updating a data object.
* @param {DataEventArgs} e - An object that represents the event arguments passed to this operation.
* @param {Function} callback - A callback function that should be called at the end of this operation. The first argument may be an error if any occured.
*/
DataPermissionEventListener.prototype.beforeSave = function(e, callback)
{
DataPermissionEventListener.prototype.validate(e, callback);
};
/**
* Occurs before removing a data object.
* @param {DataEventArgs} e - An object that represents the event arguments passed to this operation.
* @param {Function} callback - A callback function that should be called at the end of this operation. The first argument may be an error if any occured.
* @returns {DataEventListener}
*/
DataPermissionEventListener.prototype.beforeRemove = function(e, callback)
{
DataPermissionEventListener.prototype.validate(e, callback);
};
/**
* Validates permissions against the event arguments provided.
* @param {DataEventArgs} e - An object that represents the event arguments passed to this operation.
* @param {Function} callback - A callback function that should be called at the end of this operation. The first argument may be an error if any occured.
*/
DataPermissionEventListener.prototype.validate = function(e, callback) {
var model = e.model,
context = e.model.context,
requestMask = 1,
workspace = 1;
//ensure silent operation
if (e.model && e.model.$silent) {
callback();
return;
}
if (e.state == 0)
requestMask = PermissionMask.Read;
else if (e.state==1)
requestMask = PermissionMask.Create;
else if (e.state==2)
requestMask = PermissionMask.Update;
else if (e.state==4)
requestMask = PermissionMask.Delete;
else if (e.state==16)
requestMask = PermissionMask.Execute;
else {
callback(new Error('Target object has an invalid state.'));
return;
}
//validate throwError
if (typeof e.throwError === 'undefined')
e.throwError = true;
context.user = context.user || { name:'anonymous',authenticationType:'None' };
//change: 2-May 2015
//description: Use unattended execution account as an escape permission check account
var authSettings = context.getConfiguration().getAuthSettings();
if (authSettings)
{
var unattendedExecutionAccount=authSettings.unattendedExecutionAccount;
if ((typeof unattendedExecutionAccount !== 'undefined'
|| unattendedExecutionAccount != null)
&& (unattendedExecutionAccount===context.user.name))
{
e.result = true;
callback();
return;
}
}
//get user key
var users = context.model('User'), permissions = context.model('Permission');
if (_.isNil(users)) {
//do nothing
callback();
return;
}
if (_.isNil(permissions)) {
//do nothing
callback();
return;
}
effectiveAccounts(context, function(err, accounts) {
if (err) { callback(err); return; }
var permEnabled = model.privileges.filter(function(x) { return !x.disabled; }, model.privileges).length>0;
//get all enabled privileges
var privileges = model.privileges.filter(function(x) { return !x.disabled && ((x.mask & requestMask) == requestMask) });
if (privileges.length==0) {
if (e.throwError) {
//if the target model has privileges but it has no privileges with the requested mask
if (permEnabled) {
//throw error
var error = new Error('Access denied.');
error.status = 401;
callback(error);
}
else {
//do nothing
callback(null);
}
}
else {
//set result to false (or true if model has no privileges at all)
e.result = !permEnabled;
//and exit
callback(null);
}
}
else {
var cancel = false;
e.result = false;
//enumerate privileges
async.eachSeries(privileges, function(item, cb) {
if (cancel) {
cb(null);
return;
}
//global
if (item.type=='global') {
if (typeof item.account !== 'undefined') {
//check if a privilege is assigned by the model
if (item.account==='*') {
//get permission and exit
cancel=true;
e.result = true;
cb(null);
return;
}
}
//try to find user has global permissions assigned
permissions.where('privilege').equal(model.name).
and('parentPrivilege').equal(null).
and('target').equal('0').
and('workspace').equal(workspace).
and('account').in(accounts.map(function(x) { return x.id; })).
and('mask').bit(requestMask).silent().count(function(err, count) {
if (err) {
cb(err);
}
else {
if (count>=1) {
cancel=true;
e.result = true;
}
cb(null);
}
});
}
else if (item.type=='parent') {
var mapping = model.inferMapping(item.property);
if (!mapping) {
cb(null);
return;
}
if (requestMask==PermissionMask.Create) {
permissions.where('privilege').equal(mapping.childModel).
and('parentPrivilege').equal(mapping.parentModel).
and('target').equal(e.target[mapping.childField]).
and('workspace').equal(workspace).
and('account').in(accounts.map(function(x) { return x.id; })).
and('mask').bit(requestMask).silent().count(function(err, count) {
if (err) {
cb(err);
}
else {
if (count>=1) {
cancel=true;
e.result = true;
}
cb(null);
}
});
}
else {
//get original value
model.where(model.primaryKey).equal(e.target[model.primaryKey]).select(mapping.childField).first(function(err, result) {
if (err) {
cb(err);
}
else if (result) {
permissions.where('privilege').equal(mapping.childModel).
and('parentPrivilege').equal(mapping.parentModel).
and('target').equal(result[mapping.childField]).
and('workspace').equal(workspace).
and('account').in(accounts.map(function(x) { return x.id; })).
and('mask').bit(requestMask).silent().count(function(err, count) {
if (err) {
cb(err);
}
else {
if (count>=1) {
cancel=true;
e.result = true;
}
cb(null);
}
});
}
else {
cb(null);
}
});
}
}
else if (item.type=='item') {
//if target object is a new object
if (requestMask==PermissionMask.Create) {
//do nothing
cb(null); return;
}
permissions.where('privilege').equal(model.name).
and('parentPrivilege').equal(null).
and('target').equal(e.target[model.primaryKey]).
and('workspace').equal(workspace).
and('account').in(accounts.map(function(x) { return x.id; })).
and('mask').bit(requestMask).silent().count(function(err, count) {
if (err) {
cb(err);
}
else {
if (count>=1) {
cancel=true;
e.result = true;
}
cb(null);
}
});
}
else if (item.type=='self') {
if (requestMask==PermissionMask.Create) {
var query = qry.query(model.viewAdapter);
var fields=[], field;
//cast target
var name, obj = e.target;
model.attributes.forEach(function(x) {
name = obj.hasOwnProperty(x.property) ? x.property : x.name;
if (obj.hasOwnProperty(name))
{
var mapping = model.inferMapping(name);
if (_.isNil(mapping)) {
field = {};
field[x.name] = { $value: obj[name] };
fields.push(field);
}
else if ((mapping.associationType==='association') && (mapping.childModel===model.name)) {
if (typeof obj[name] === 'object') {
//set associated key value (e.g. primary key value)
field = {};
field[x.name] = { $value: obj[name][mapping.parentField] };
fields.push(field);
}
else {
//set raw value
field = {};
field[x.name] = { $value: obj[name] };
fields.push(field);
}
}
}
});
//add fields
query.select(fields);
//set fixed query
query.$fixed = true;
model.filter(item.filter, function(err, q) {
if (err) {
cb(err);
}
else {
//set where from DataQueryable.query
query.$where = q.query.$prepared;
model.context.db.execute(query,null, function(err, result) {
if (err) {
cb(err);
}
else {
if (result.length==1) {
cancel=true;
e.result = true;
}
cb(null);
}
});
}
});
}
else {
//get privilege filter
model.filter(item.filter, function(err, q) {
if (err) {
cb(err);
}
else {
//prepare query and append primary key expression
q.where(model.primaryKey).equal(e.target[model.primaryKey]).silent().count(function(err, count) {
if (err) { cb(err); return; }
if (count>=1) {
cancel=true;
e.result = true;
}
cb(null);
})
}
});
}
}
else {
//do nothing (unknown permission)
cb(null);
}
}, function(err) {
if (err) {
callback(err);
}
else {
if (e.throwError && !e.result) {
var error = new types.AccessDeniedException();
error.model = model.name;
callback(error);
}
else {
callback(null);
}
}
});
}
});
};
/**
* @private
* @type {string}
*/
var ANONYMOUS_USER_CACHE_PATH = '/User/anonymous';
/**
* @param {DataContext} context
* @param {function(Error=,*=)} callback
* @private
*/
function anonymousUser(context, callback) {
queryUser(context, 'anonymous', function(err, result) {
if (err) {
callback(err);
}
else {
callback(null, result || { id:0, name:'anonymous', groups:[], enabled:false});
}
});
};
/**
*
* @param {DataContext} context
* @param {string} username
* @param {function(Error=,*=)} callback
* @private
*/
function queryUser(context, username, callback) {
try {
if (_.isNil(context)) {
return callback();
}
else {
//get user key
var users = context.model('User');
if (_.isNil(users)) {
return callback();
}
users.where('name').equal(username).silent().select('id', 'name').first(function(err, result) {
if (err) {
callback(err);
}
else {
//if anonymous user was not found
if (_.isNil(result)) {
return callback();
}
//get anonymous user object
var user = users.convert(result);
//get user groups
user.property('groups').select('id', 'name').silent().all(function(err, groups) {
if (err) {
callback(err);
return;
}
//set anonymous user groups
user.groups = groups || [];
//return user
callback(null, user);
});
}
});
}
}
catch (e) {
callback(e);
}
};
/**
* @param {DataContext} context
* @param {function(Error=,Array=)} callback
* @private
*/
function effectiveAccounts(context, callback) {
if (_.isNil(context)) {
//push no account
return callback(null, [ { id: 0 } ]);
}
/**
* Gets or sets an object that represents the user of the current data context.
* @property {*|{name: string, authenticationType: string}}
* @name DataContext#user
* @memberof DataContext
*/
context.user = context.user || { name:'anonymous',authenticationType:'None' };
context.user.name = context.user.name || 'anonymous';
//if the current user is anonymous
if (context.user.name === 'anonymous') {
//get anonymous user data
dataCache.current.ensure(ANONYMOUS_USER_CACHE_PATH, function(cb) {
anonymousUser(context, function(err, result) {
cb(err, result);
});
}, function(err, result) {
if (err) {
callback(err);
}
else {
var arr = [];
if (result) {
arr.push({ "id": result.id, "name": result.name });
result.groups = result.groups || [];
result.groups.forEach(function(x) { arr.push({ "id": x.id, "name": x.name }); });
}
if (arr.length==0)
arr.push({ id: 0 });
callback(null, arr);
}
});
}
else {
//try to get data from cache
var USER_CACHE_PATH = '/User/' + context.user.name;
dataCache.current.ensure(USER_CACHE_PATH, function(cb) {
queryUser(context, context.user.name, cb);
}, function(err, user) {
if (err) { callback(err); return; }
dataCache.current.ensure(ANONYMOUS_USER_CACHE_PATH, function(cb) {
anonymousUser(context, cb);
}, function(err, anonymous) {
if (err) { callback(err); return; }
var arr = [ ];
if (user) {
arr.push({ "id": user.id, "name": user.name });
if (util.isArray(user.groups))
user.groups.forEach(function(x) { arr.push({ "id": x.id, "name": x.name }); });
}
if (anonymous) {
arr.push({ "id": anonymous.id, "name": "anonymous" });
if (util.isArray(anonymous.groups))
anonymous.groups.forEach(function(x) { arr.push({ "id": x.id, "name": x.name }); });
}
if (arr.length==0)
arr.push({ id: 0 });
callback(null, arr);
});
});
}
}
/**
* Occurs before executing a data operation.
* @param {DataEventArgs} e - An object that represents the event arguments passed to this operation.
* @param {Function} callback - A callback function that should be called at the end of this operation. The first argument may be an error if any occured.
*/
DataPermissionEventListener.prototype.beforeExecute = function(e, callback)
{
if (_.isNil(e.model)) {
return callback();
}
//ensure silent query operation
if (e.emitter && e.emitter.$silent) {
callback();
return;
}
var model= e.model, context = e.model.context, requestMask = 1, workspace = 1, privilege = model.name, parentPrivilege=null;
//get privilege from event arguments if it's defined (e.g. the operation requests execute permission User.ChangePassword where
// privilege=ChangePassword and parentPrivilege=User)
if (e.privilege) {
//event argument is the privilege
privilege = e.privilege;
//and model is the parent privilege
parentPrivilege = model.name;
}
//do not check permissions if the target model has no privileges defined
if (model.privileges.filter(function(x) { return !x.disabled; }, model.privileges).length==0) {
callback(null);
return;
}
//infer permission mask
if (typeof e.mask !== 'undefined') {
requestMask = e.mask;
}
else {
if (e.query) {
//infer mask from query type
if (e.query.$select)
//read permissions
requestMask=1;
else if (e.query.$insert)
//create permissions
requestMask=2;
else if (e.query.$update)
//update permissions
requestMask=4;
else if (e.query.$delete)
//delete permissions
requestMask=8;
}
}
//ensure context user
context.user = context.user || { name:'anonymous',authenticationType:'None' };
//change: 2-May 2015
//description: Use unattended execution account as an escape permission check account
var authSettings = context.getConfiguration().getAuthSettings();
if (authSettings)
{
var unattendedExecutionAccount=authSettings.unattendedExecutionAccount;
if ((typeof unattendedExecutionAccount !== 'undefined'
|| unattendedExecutionAccount != null)
&& (unattendedExecutionAccount===context.user.name))
{
callback();
return;
}
}
if (e.query) {
//get user key
var users = context.model('User'), permissions = context.model('Permission');
if (_.isNil(users)) {
//do nothing
callback(null);
return;
}
if (_.isNil(permissions)) {
//do nothing
callback(null);
return;
}
//get model privileges
var modelPrivileges = model.privileges || [];
//if model has no privileges defined
if (modelPrivileges.length==0) {
//do nothing
callback(null);
//and exit
return;
}
//tuning up operation
//validate request mask permissions against all users privilege { mask:<requestMask>,disabled:false,account:"*" }
var allUsersPrivilege = modelPrivileges.find(function(x) {
return (((x.mask & requestMask)==requestMask) && !x.disabled && (x.account==='*'));
});
if (typeof allUsersPrivilege !== 'undefined') {
//do nothing
callback(null);
//and exit
return;
}
effectiveAccounts(context, function(err, accounts) {
if (err) { callback(err); return; }
//get all enabled privileges
var privileges = modelPrivileges.filter(function(x) {
return !x.disabled && ((x.mask & requestMask) == requestMask);
});
var cancel = false, assigned = false, entity = qry.entity(model.viewAdapter), expand = null,
perms1 = qry.entity(permissions.viewAdapter).as('p0'), expr = null;
async.eachSeries(privileges, function(item, cb) {
if (cancel) {
return cb();
}
try {
if (item.type=='global') {
//check if a privilege is assigned by the model
if (item.account==='*') {
//get permission and exit
assigned=true;
return cb(new EachSeriesCancelled());
}
else if (item.hasOwnProperty("account")) {
if (accounts.findIndex(function(x) { return x.name === item.account })>=0) {
assigned=true;
return cb(new EachSeriesCancelled());
}
}
//try to find user has global permissions assigned
permissions.where('privilege').equal(model.name).
and('parentPrivilege').equal(null).
and('target').equal('0').
and('workspace').equal(1).
and('account').in(accounts.map(function(x) { return x.id; })).
and('mask').bit(requestMask).silent().count(function(err, count) {
if (err) {
cb(err);
}
else {
if (count>=1) {
assigned=true;
return cb(new EachSeriesCancelled());
}
cb();
}
});
}
else if (item.type=='parent') {
//get field mapping
var mapping = model.inferMapping(item.property);
if (!mapping) {
return cb();
}
if (expr==null)
expr = qry.query();
expr.where(entity.select(mapping.childField)).equal(perms1.select('target')).
and(perms1.select('privilege')).equal(mapping.childModel).
and(perms1.select('parentPrivilege')).equal(mapping.parentModel).
and(perms1.select('workspace')).equal(workspace).
and(perms1.select('mask')).bit(requestMask).
and(perms1.select('account')).in(accounts.map(function(x) { return x.id; })).prepare(true);
assigned=true;
cb();
}
else if (item.type=='item') {
if (expr==null)
expr = qry.query();
expr.where(entity.select(model.primaryKey)).equal(perms1.select('target')).
and(perms1.select('privilege')).equal(model.name).
and(perms1.select('parentPrivilege')).equal(null).
and(perms1.select('workspace')).equal(workspace).
and(perms1.select('mask')).bit(requestMask).
and(perms1.select('account')).in(accounts.map(function(x) { return x.id; })).prepare(true);
assigned=true;
cb();
}
else if (item.type=='self') {
if (typeof item.filter === 'string' ) {
model.filter(item.filter, function(err, q) {
if (err) {
cb(err);
}
else {
if (q.query.$prepared) {
if (expr==null)
expr = qry.query();
expr.$where = q.query.$prepared;
if (q.query.$expand) { expand = q.query.$expand; }
expr.prepare(true);
assigned=true;
cb();
}
else
cb();
}
});
}
else {
cb();
}
}
else {
cb();
}
}
catch (e) {
cb(e);
}
}, function(err) {
if (err) {
cancel = (err instanceof EachSeriesCancelled);
if (!cancel) {
return callback(err);
}
}
if (!assigned) {
//prepare no access query
e.query.prepare();
//add no record parameter
e.query.where(e.model.fieldOf(e.model.primaryKey)).equal(null).prepare();
return callback();
}
else if (expr) {
return context.model("Permission").migrate(function(err) {
if (err) { return callback(err); }
var q = qry.query(model.viewAdapter).select([model.primaryKey]).distinct();
if (expand) {
q.join(expand[0].$entity).with(expand[0].$with);
}
q.join(perms1).with(expr);
var pqAlias = 'pq' + common.randomInt(100000,999999).toString();
e.query.join(q.as(pqAlias)).with(qry.where(entity.select(model.primaryKey)).equal(qry.entity(pqAlias).select(model.primaryKey)));
return callback();
});
}
return callback();
});
});
}
else {
callback();
}
};
var perms = {
DataPermissionEventArgs:DataPermissionEventArgs,
DataPermissionEventListener:DataPermissionEventListener,
PermissionMask:PermissionMask
};
if (typeof exports !== 'undefined') {
module.exports = perms;
}