/**
* Module for validation functions that manage access rights to application data. Divided in parts access for Global Admins, Admins and Users.
* @module api/utils/rights
*/
var common = require("./common.js"),
plugins = require('../../plugins/pluginManager.js'),
Promise = require("bluebird"),
crypto = require('crypto'),
log = require('./log.js')('core:rights');
var authorize = require('./authorizer.js'); //for token validations
var collectionMap = {};//map to know when data about som collections/events was refreshed
var cachedSchema = {};
//check token and return owner id if token valid
//owner d used later to set all member variables.
/**Validate if token exists and is not expired(uzing authorize.js)
* @param {object} params params
* @param {string} params.qstring.auth_token authentication token
* @param {string}params.req.headers.countly-token {string} authentication token
* @param {string} params.fullPath current full path
* @returns {Promise} promise
*/
function validate_token_if_exists(params) {
return new Promise(function(resolve) {
var token = params.qstring.auth_token || params.req.headers["countly-token"] || "";
if (token && token !== "") {
authorize.verify_return({
db: common.db,
qstring: params.qstring,
token: token,
req_path: params.fullPath,
callback: function(valid) {
//false or owner.id
if (valid) {
resolve(valid);
}
else {
resolve('token-invalid');
}
}
});
}
else {
resolve("token-not-given");
}
});
}
/**
* Validate user for read access by api_key for provided app_id (both required parameters for the request).
* User must exist, must not be locked, must pass plugin validation (if any) and have at least user access to the provided app (which also must exist).
* If user does not pass validation, it outputs error to request. In case validation passes, provided callback is called.
* Additionally populates params with member information and app information.
* @param {params} params - {@link params} object
* @param {function} callback - function to call only if validation passes
* @param {any=} callbackParam - parameter to pass to callback function (params is automatically passed to callback function, no need to include that)
* @returns {Promise} promise
*/
exports.validateUserForRead = function(params, callback, callbackParam) {
return wrapCallback(params, callback, callbackParam, function(resolve, reject) {
validate_token_if_exists(params).then(function(result) {
var query = "";
// then result is owner id
if (result !== 'token-not-given' && result !== 'token-invalid') {
query = {'_id': common.db.ObjectID(result)};
}
else {
if (!params.qstring.api_key) {
if (result === 'token-invalid') {
common.returnMessage(params, 400, 'Token not valid');
return false;
}
else {
common.returnMessage(params, 400, 'Missing parameter "api_key" or "auth_token"');
return false;
}
}
params.qstring.api_key = params.qstring.api_key + "";
query = {'api_key': params.qstring.api_key};
}
common.db.collection('members').findOne(query, function(err, member) {
if (!member || err) {
common.returnMessage(params, 401, 'User does not exist');
reject('User does not exist');
return false;
}
if (typeof params.qstring.app_id === "undefined") {
common.returnMessage(params, 401, 'No app_id provided');
reject('No app_id provided');
return false;
}
const userApps = module.exports.getUserApps(member);
if (!((userApps.indexOf(params.qstring.app_id) !== -1) || member.global_admin)) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
return false;
}
if (member && member.locked) {
common.returnMessage(params, 401, 'User is locked');
reject('User is locked');
return false;
}
common.db.collection('apps').findOne({'_id': common.db.ObjectID(params.qstring.app_id + "")}, function(err1, app) {
if (!app) {
common.returnMessage(params, 401, 'App does not exist');
reject('App does not exist');
return false;
}
params.member = member;
params.app_id = app._id;
params.app_cc = app.country;
params.appTimezone = app.timezone;
params.app = app;
params.time = common.initTimeObj(params.appTimezone, params.qstring.timestamp);
if (plugins.dispatch("/validation/user", {params: params})) {
if (!params.res.finished) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
}
return false;
}
plugins.dispatch("/o/validate", {
params: params,
app: app
});
resolve(callbackParam);
});
});
},
function() {
common.returnMessage(params, 401, 'Token is invalid');
reject('Token is invalid');
return false;
});
});
};
/**
* Validate user for write access by api_key for provided app_id (both required parameters for the request).
* User must exist, must not be locked, must pass plugin validation (if any) and have at least admin access to the provided app (which also must exist).
* If user does not pass validation, it outputs error to request. In case validation passes, provided callback is called.
* Additionally populates params with member information and app information.
* @param {params} params - {@link params} object
* @param {function} callback - function to call only if validation passes
* @param {any=} callbackParam - parameter to pass to callback function (params is automatically passed to callback function, no need to include that)
* @returns {Promise} promise
*/
exports.validateUserForWrite = function(params, callback, callbackParam) {
return wrapCallback(params, callback, callbackParam, function(resolve, reject) {
validate_token_if_exists(params).then(function(result) {
var query = "";
// then result is owner id
if (result !== 'token-not-given' && result !== 'token-invalid') {
query = {'_id': common.db.ObjectID(result)};
}
else {
if (!params.qstring.api_key) {
if (result === 'token-invalid') {
common.returnMessage(params, 400, 'Token not valid');
return false;
}
else {
common.returnMessage(params, 400, 'Missing parameter "api_key" or "auth_token"');
return false;
}
}
params.qstring.api_key = params.qstring.api_key + "";
query = {'api_key': params.qstring.api_key};
}
common.db.collection('members').findOne(query, function(err, member) {
if (!member || err) {
common.returnMessage(params, 401, 'User does not exist');
reject('User does not exist');
return false;
}
if (!(module.exports.hasAdminAccess(member, params.qstring.app_id))) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
return false;
}
if (member && member.locked) {
common.returnMessage(params, 401, 'User is locked');
reject('User is locked');
return false;
}
common.db.collection('apps').findOne({'_id': common.db.ObjectID(params.qstring.app_id + "")}, function(err1, app) {
if (!app) {
common.returnMessage(params, 401, 'App does not exist');
reject('App does not exist');
return false;
}
else if ((params.populator || params.qstring.populator) && app.locked) {
common.returnMessage(params, 403, 'App is locked');
reject('App is locked');
return false;
}
params.app_id = app._id;
params.appTimezone = app.timezone;
params.app = app;
params.time = common.initTimeObj(params.appTimezone, params.qstring.timestamp);
params.member = member;
if (plugins.dispatch("/validation/user", {params: params})) {
if (!params.res.finished) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
}
return false;
}
resolve(callbackParam);
});
});
},
function() {
common.returnMessage(params, 401, 'Token is invalid');
reject('Token is invalid');
return false;
});
});
};
/**
* Validate user for global admin access by api_key (required parameter for the request).
* User must exist, must not be locked, must pass plugin validation (if any) and have global admin access.
* If user does not pass validation, it outputs error to request. In case validation passes, provided callback is called.
* Additionally populates params with member information.
* @param {params} params - {@link params} object
* @param {function} callback - function to call only if validation passes
* @param {any=} callbackParam - parameter to pass to callback function (params is automatically passed to callback function, no need to include that)
* @returns {Promise} promise
*/
exports.validateGlobalAdmin = function(params, callback, callbackParam) {
return wrapCallback(params, callback, callbackParam, function(resolve, reject) {
validate_token_if_exists(params).then(function(result) {
var query = "";
// then result is owner id
if (result !== 'token-not-given' && result !== 'token-invalid') {
query = {'_id': common.db.ObjectID(result)};
}
else {
if (!params.qstring.api_key) {
if (result === 'token-invalid') {
common.returnMessage(params, 400, 'Token not valid');
return false;
}
else {
common.returnMessage(params, 400, 'Missing parameter "api_key" or "auth_token"');
return false;
}
}
params.qstring.api_key = params.qstring.api_key + "";
query = {'api_key': params.qstring.api_key};
}
common.db.collection('members').findOne(query, function(err, member) {
if (!member || err) {
common.returnMessage(params, 401, 'User does not exist');
reject('User does not exist');
return false;
}
if (!member.global_admin) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
return false;
}
if (member && member.locked) {
common.returnMessage(params, 401, 'User is locked');
reject('User is locked');
return false;
}
params.member = member;
params.member.auth_token = params.qstring.auth_token || params.req.headers["countly-token"] || "";
if (plugins.dispatch("/validation/user", {params: params})) {
if (!params.res.finished) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
}
return false;
}
resolve(callbackParam);
});
},
function() {
common.returnMessage(params, 401, 'Token is invalid');
reject('Token is invalid');
return false;
});
});
};
/**
* Validate user for admin access for specific app by api_key (required parameter for the request).
* User must exist, must not be locked, must pass plugin validation (if any).
* If user does not pass validation, it outputs error to request. In case validation passes, provided callback is called.
* Additionally populates params with member information.
* @param {params} params - {@link params} object
* @param {function} callback - function to call only if validation passes
* @param {any=} callbackParam - parameter to pass to callback function (params is automatically passed to callback function, no need to include that)
* @returns {Promise} promise
*/
exports.validateAppAdmin = function(params, callback, callbackParam) {
return wrapCallback(params, callback, callbackParam, function(resolve, reject) {
validate_token_if_exists(params).then(function(result) {
var query = "";
// then result is owner id
if (result !== 'token-not-given' && result !== 'token-invalid') {
query = {'_id': common.db.ObjectID(result)};
}
else {
if (!params.qstring.api_key) {
if (result === 'token-invalid') {
common.returnMessage(params, 400, 'Token not valid');
return false;
}
else {
common.returnMessage(params, 400, 'Missing parameter "api_key" or "auth_token"');
return false;
}
}
params.qstring.api_key = params.qstring.api_key + "";
query = {'api_key': params.qstring.api_key};
}
common.db.collection('members').findOne(query, function(err, member) {
if (!member || err) {
common.returnMessage(params, 401, 'User does not exist');
reject('User does not exist');
return false;
}
if (!params.qstring.app_id) {
common.returnMessage(params, 400, 'No app id provided');
return false;
}
if (!member.global_admin) {
if (!member.permission || member.permission._.a.indexOf(params.qstring.app_id) === -1) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
return false;
}
}
if (member && member.locked) {
common.returnMessage(params, 401, 'User is locked');
reject('User is locked');
return false;
}
params.member = member;
params.member.auth_token = params.qstring.auth_token || params.req.headers["countly-token"] || "";
if (plugins.dispatch("/validation/user", {params: params})) {
if (!params.res.finished) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
}
return false;
}
resolve(callbackParam);
});
},
function() {
common.returnMessage(params, 401, 'Token is invalid');
reject('Token is invalid');
return false;
});
});
};
/**
* Basic user validation by api_key (required parameter for the request), mostly used for custom validation afterwards (like multi app access).
* User must exist, must not be locked and must pass plugin validation (if any).
* If user does not pass validation, it outputs error to request. In case validation passes, provided callback is called.
* Additionally populates params with member information.
* @param {params} params - {@link params} object
* @param {function} callback - function to call only if validation passes
* @param {any=} callbackParam - parameter to pass to callback function (params is automatically passed to callback function, no need to include that)
* @returns {Promise} promise
*/
exports.validateUser = function(params, callback, callbackParam) {
//old backwards compatability call check
if (typeof params === "function") {
var temp = params;
params = callback;
callback = temp;
}
return wrapCallback(params, callback, callbackParam, function(resolve, reject) {
validate_token_if_exists(params).then(function(result) {
var query = "";
// then result is owner id
if (result !== 'token-not-given' && result !== 'token-invalid') {
query = {'_id': common.db.ObjectID(result)};
}
else {
if (!params.qstring.api_key) {
if (result === 'token-invalid') {
common.returnMessage(params, 400, 'Token not valid');
return false;
}
else {
common.returnMessage(params, 400, 'Missing parameter "api_key" or "auth_token"');
return false;
}
}
params.qstring.api_key = params.qstring.api_key + "";
query = {'api_key': params.qstring.api_key};
}
common.db.collection('members').findOne(query, function(err, member) {
if (!member || err) {
common.returnMessage(params, 401, 'User does not exist');
reject('User does not exist');
return false;
}
if (member && member.locked) {
common.returnMessage(params, 401, 'User is locked');
reject('User is locked');
return false;
}
params.member = member;
if (plugins.dispatch("/validation/user", {params: params})) {
if (!params.res.finished) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
}
return false;
}
resolve(callbackParam);
});
},
function() {
common.returnMessage(params, 401, 'Token is invalid');
reject('Token is invalid');
return false;
});
});
};
/**
* Wrap callback using promise
* @param {params} params - {@link params} object
* @param {function} callback - function to call only if validation passes
* @param {any=} callbackParam - parameter to pass to callback function
* @param {function} func - promise function
* @returns {Promise} promise
*/
function wrapCallback(params, callback, callbackParam, func) {
var promise = new Promise(func);
if (typeof callback === "function") {
promise.asCallback(function(err) {
if (!err) {
let ret;
if (callbackParam) {
ret = callback(callbackParam, params);
}
else {
ret = callback(params);
}
if (ret && typeof ret.then === 'function') {
ret.catch(e => {
log.e('Error in CRUD callback', e);
common.returnMessage(params, 500, 'Server error');
});
}
}
});
}
else if (callback) {
console.log("Incorrect callback function", callback);
}
return promise;
}
/**
* Function to load and cache data
* @param {object} apps - apps
* @param {function} callback - callback function
*/
function loadAndCacheEventsData(apps, callback) {
const appIds = [];
const appNamesById = {};
var anyNameMissing = false;
apps.forEach((app) => {
cachedSchema[app._id + ''] = cachedSchema[app._id + ''] || {};
cachedSchema[app._id + ''].loading = true;
appIds.push(common.db.ObjectID(app._id + ''));
appNamesById[app._id + ''] = app.name;
if (!appNamesById[app._id + '']) {
anyNameMissing = true;
}
});
/**
* Get events collections with replaced app names
* A helper function for db access check
* @param {object} appColl - application ids and names
* @param {function} cb - callback method
**/
function getEvents(appColl, cb) {
common.db.collection('events').find({'_id': { $in: appColl.appIds }}).toArray(function(err, events) {
if (!err && events) {
for (let h = 0; h < events.length; h++) {
if (events[h].list) {
for (let i = 0; i < events[h].list.length; i++) {
collectionMap[crypto.createHash('sha1').update(events[h].list[i] + events[h]._id + "").digest('hex')] = {"n": true, "a": events[h]._id + "", "e": events[h].list[i], "name": "(" + appNamesById[events[h]._id + ''] + ": " + events[h].list[i] + ")"};
}
}
}
}
appColl.appIds.forEach((appId) => {
if (plugins.internalDrillEvents) {
for (let i = 0; i < plugins.internalDrillEvents.length; i++) {
collectionMap[crypto.createHash('sha1').update(plugins.internalDrillEvents[i] + appId + "").digest('hex')] = {"n": true, "a": appId + "", "e": plugins.internalDrillEvents[i], "name": "(" + appColl.appNamesById[appId + ''] + ": " + plugins.internalDrillEvents[i] + ")"};
}
}
if (plugins.internalEvents) {
for (let i = 0; i < plugins.internalEvents.length; i++) {
collectionMap[crypto.createHash('sha1').update(plugins.internalEvents[i] + appId + "").digest('hex')] = {"n": true, "a": appId + "", "e": plugins.internalEvents[i], "name": "(" + appColl.appNamesById[appId + ''] + ": " + plugins.internalEvents[i] + ")"};
}
}
});
cb(null, true);
});
}
/**
* Get views collections with replaced app names
* A helper function for db access check
* @param {object} appColl - application ids and names
* @param {function} cb - callback method
**/
function getViews(appColl, cb) {
common.db.collection('views').find({'_id': { $in: appColl.appIds }}).toArray(function(err, viewDocs) {
if (!err && viewDocs) {
for (let idx = 0; idx < viewDocs.length; idx++) {
if (viewDocs[idx].segments) {
for (var segkey in viewDocs[idx].segments) {
collectionMap["app_viewdata" + crypto.createHash('sha1').update(segkey + viewDocs[idx]._id + '').digest('hex')] = {"n": true, "a": viewDocs[idx]._id + '', "vs": segkey, "name": "(" + appColl.appNamesById[viewDocs[idx]._id + ''] + ": " + segkey + ")"};
}
}
}
}
appColl.appIds.forEach((appId) => {
collectionMap["app_viewdata" + crypto.createHash('sha1').update("" + appId).digest('hex')] = {"n": true, "a": "" + appId, "vs": "", "name": "(" + appColl.appNamesById[appId + ''] + ": no-segment)"};
});
cb(null, true);
});
}
if (anyNameMissing) { //We do not have name for APPs, so we need to fetch them
common.db.collection('apps').find({'_id': { $in: appIds }}, {'name': 1}).toArray(function(err, newapps) {
if (err) {
log.e(err);
callback(err);
}
else {
for (var i = 0; i < newapps.length; i++) {
newapps[i].name = newapps[i].name || "Unknown";
}
loadAndCacheEventsData(newapps, callback);
}
});
}
else {
getEvents({ appIds, appNamesById }, function(err) {
if (err) {
log.e(err);
}
getViews({ appIds, appNamesById }, function(err1) {
if (err1) {
log.e(err1);
}
for (var item in collectionMap) {
if (appNamesById[collectionMap[item].a]) {
if (!collectionMap[item].n) {
delete collectionMap[item];
}
else {
delete collectionMap[item].n;
}
}
}
apps.forEach((app) => {
cachedSchema[app._id + ''].ts = Date.now();
cachedSchema[app._id + ''].loading = false;
});
common.cachedSchema = cachedSchema;
common.collectionMap = collectionMap;
callback(err || err1);
});
});
}
}
/**
* Get events data
* A helper function for db access check
* @param {object} params - {@link params} object
* @param {array} apps - array with each element being app document
* @param {function} callback - callback method
**/
function dbLoadEventsData(params, apps, callback) {
var events = {};
var views = {};
var callCalculate = [];
var appMap = {};
for (var a in apps) {
if (!cachedSchema[apps[a]._id + ''] || (cachedSchema[apps[a]._id + ''] && !cachedSchema[apps[a]._id + ''].loading && (Date.now() - cachedSchema[apps[a]._id + ''].ts) > 10 * 60 * 1000)) {
callCalculate.push(apps[a]);
}
appMap[apps[a]._id + ''] = true;
}
if (params.member.eventList) {
callback(null, params.member.eventList, params.member.viewList);
if (callCalculate.length > 0) {
loadAndCacheEventsData(callCalculate, function(err) {
if (err) {
log.e(err);
}
});
}
}
else if (callCalculate.length > 0) {
loadAndCacheEventsData(callCalculate, function(err) {
if (err) {
log.e(err);
}
for (var key in collectionMap) {
if (appMap[collectionMap[key].a]) {
if (collectionMap[key].e) {
events[key] = collectionMap[key].name;
}
else if (collectionMap[key].vs) {
views[key] = collectionMap[key].name;
}
}
}
params.member.eventList = events;
params.member.viewList = views;
callback(null, events, views);
});
}
else {
for (var key in collectionMap) {
if (appMap[collectionMap[key].a]) {
if (collectionMap[key].e) {
events[key] = collectionMap[key].name;
}
else if (collectionMap[key].vs) {
views[key] = collectionMap[key].name;
}
}
}
params.member.eventList = events;
params.member.viewList = views;
callback(null, events, views);
}
}
exports.dbLoadEventsData = dbLoadEventsData;
exports.getCollectionName = function(hashValue) {
if (collectionMap[hashValue]) {
return collectionMap[hashValue].name;
}
else {
return hashValue;
}
};
/**
* Check user has access to collection
* @param {object} params - {@link params} object
* @param {string} collection - collection will be checked for access
* @param {string} app_id - app_id to which to restrict access
* @param {function} callback - callback method includes boolean variable as argument
* @returns {function} returns callback
**/
exports.dbUserHasAccessToCollection = function(params, collection, app_id, callback) {
if (typeof app_id === "function") {
callback = app_id;
app_id = null;
}
if (params.member.global_admin && !app_id) {
//global admin without app_id restriction just has access to everything
return callback(true);
}
var apps = [];
var userApps = module.exports.getUserApps(params.member);
var hashValue = "";
//use whatever user has permission for
apps = userApps || [];
// also check for app based restrictions
if (params.member.app_restrict) {
for (var appid in params.member.app_restrict) {
if (params.member.app_restrict[appid].indexOf("#/manage/db") !== -1 && apps.indexOf(appid) !== -1) {
apps.splice(apps.indexOf(appid), 1);
}
}
}
if (app_id) {
if (params.member.global_admin) {
apps = [app_id];
}
else {
apps = apps.filter(id => id + "" === app_id + "");
}
}
var appList = [];
if (collection.indexOf("events") === 0 || collection.indexOf("drill_events") === 0) {
for (let i = 0; i < apps.length; i++) {
if (apps[i].length) {
appList.push({_id: apps[i]});
}
}
hashValue = collection.replace("drill_events", "").replace("events", "");
dbLoadEventsData(params, appList, function(err) {
if (err) {
log.e("[rights.js].dbUserHasAccessToCollection() failed at dbLoadEventsData (events) callback.", err);
return callback(false);
}
else {
if (collectionMap[hashValue] && apps.length > 0 && apps.indexOf(collectionMap[hashValue].a) !== -1) {
return callback(true);
}
else {
return callback(false);
}
}
});
}
else if (collection.indexOf("app_viewdata") === 0) {
for (let i = 0; i < apps.length; i++) {
if (apps[i].length) {
appList.push({_id: apps[i]});
}
}
hashValue = collection;//we keep app_viewdata
dbLoadEventsData(params, appList, function(err) {
if (err) {
log.e("[rights.js].dbUserHasAccessToCollection() failed at dbLoadEventsData (app_viewdata) callback.", err);
return callback(false);
}
else {
if (collectionMap[hashValue] && apps.length > 0 && apps.indexOf(collectionMap[hashValue].a) !== -1) {
return callback(true);
}
else {
return callback(false);
}
}
});
}
else {
for (let i = 0; i < apps.length; i++) {
if (apps[i].length > 0 && collection.indexOf(apps[i], collection.length - apps[i].length) !== -1) {
return callback(true);
}
}
return callback(false);
}
};
/**
* Validate user for read access by api_key for provided app_id (both required parameters for the request).
* User must exist, must not be locked, must pass plugin validation (if any) and have at least read access to the provided app (which also must exist).
* If user does not pass validation, it outputs error to request. In case validation passes, provided callback is called.
* Additionally populates params with member information and app information.
* @param {params} params - {@link params} object
* @param {string} feature - feature that trying to access
* @param {function} callback - function to call only if validation passes
* @param {any=} callbackParam - parameter to pass to callback function (params is automatically passed to callback function, no need to include that)
* @returns {Promise} promise
*/
exports.validateRead = function(params, feature, callback, callbackParam) {
return wrapCallback(params, callback, callbackParam, function(resolve, reject) {
validate_token_if_exists(params).then(function(result) {
var query = "";
// then result is owner id
if (result !== 'token-not-given' && result !== 'token-invalid') {
query = {'_id': common.db.ObjectID(result)};
}
else {
if (!params.qstring.api_key) {
if (result === 'token-invalid') {
common.returnMessage(params, 400, 'Token not valid');
return false;
}
else {
common.returnMessage(params, 400, 'Missing parameter "api_key" or "auth_token"');
return false;
}
}
params.qstring.api_key = params.qstring.api_key + "";
query = {'api_key': params.qstring.api_key};
}
common.db.collection('members').findOne(query, function(err, member) {
if (!member || err) {
common.returnMessage(params, 401, 'User does not exist');
reject('User does not exist');
return false;
}
if (!member.global_admin && typeof params.qstring.app_id === "undefined") {
common.returnMessage(params, 401, 'No app_id provided');
reject('No app_id provided');
return false;
}
// is member.permission exist?
// is member.permission an object?
// is params.qstring.app_id property of member.permission object?
// is member.permission.r[app_id].all is true?
// or member.global_admin?
if (!member.global_admin) {
if (typeof member.permission !== 'undefined') {
var isPermissionObjectExistForRead = (typeof member.permission.r === "object" && typeof member.permission.r[params.qstring.app_id] === "object");
var isFeatureAllowedInReadPermissionObject = false;
if (typeof feature === "string") {
isFeatureAllowedInReadPermissionObject = isPermissionObjectExistForRead && (member.permission.r[params.qstring.app_id].all || (member.permission.r[params.qstring.app_id].allowed && member.permission.r[params.qstring.app_id].allowed[feature]));
}
else {
isFeatureAllowedInReadPermissionObject = false;
if (feature) {
for (var i = 0; i < feature.length; i++) {
if (isPermissionObjectExistForRead && (member.permission.r[params.qstring.app_id].all || (member.permission.r[params.qstring.app_id].allowed && member.permission.r[params.qstring.app_id].allowed[feature[i]]))) {
isFeatureAllowedInReadPermissionObject = true;
break;
}
}
}
}
var hasAdminAccess = (typeof member.permission === "object" && typeof member.permission._ === "object" && typeof member.permission._.a === "object") && member.permission._.a.indexOf(params.qstring.app_id) > -1;
// don't allow if user has not permission for feature and has no admin access for current app
if (!(isFeatureAllowedInReadPermissionObject) && !(hasAdminAccess)) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
return false;
}
}
else {
// check for legacy auth
if (!((member.user_of && Array.isArray(member.user_of) && member.user_of.indexOf(params.qstring.app_id) !== -1) || member.global_admin)) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
return false;
}
}
}
if (member && member.locked) {
common.returnMessage(params, 401, 'User is locked');
reject('User is locked');
return false;
}
if (params.qstring.app_id) {
common.db.collection('apps').findOne({'_id': common.db.ObjectID(params.qstring.app_id + "")}, function(err1, app) {
if (!app) {
common.returnMessage(params, 401, 'App does not exist');
reject('App does not exist');
return false;
}
else if (app) {
params.app_id = app._id;
params.app_cc = app.country;
params.appTimezone = app.timezone;
params.app = app;
params.time = common.initTimeObj(params.appTimezone, params.qstring.timestamp);
}
params.member = member;
if (plugins.dispatch("/validation/user", {params: params})) {
if (!params.res.finished) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
}
return false;
}
if (app) {
plugins.dispatch("/o/validate", {
params: params,
app: app
});
}
resolve(callbackParam);
});
}
else {
params.member = member;
if (plugins.dispatch("/validation/user", {params: params})) {
if (!params.res.finished) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
}
return false;
}
resolve(callbackParam);
}
});
},
function() {
common.returnMessage(params, 401, 'Token is invalid');
reject('Token is invalid');
return false;
});
});
};
/**
* Validate user for write access by api_key for provided app_id (both required parameters for the request).
* User must exist, must not be locked, must pass plugin validation (if any) and have accessType that passed as accessType parameter to the provided app (which also must exist).
* If user does not pass validation, it outputs error to request. In case validation passes, provided callback is called.
* Additionally populates params with member information and app information.
* @param {params} params - {@link params} object
* @param {string} feature - feature that trying to access
* @param {string} accessType - required access type for related request (c: create, u: update and d: delete)
* @param {function} callback - function to call only if validation passes
* @param {any=} callbackParam - parameter to pass to callback function (params is automatically passed to callback function, no need to include that)
* @returns {Promise} promise
*/
function validateWrite(params, feature, accessType, callback, callbackParam) {
return wrapCallback(params, callback, callbackParam, function(resolve, reject) {
validate_token_if_exists(params).then(function(result) {
var query = "";
//var appIdExceptions = ['global_users', 'global_applications', 'global_jobs', 'global_plugins', 'global_configurations', 'global_upload'];
// then result is owner id
if (result !== 'token-not-given' && result !== 'token-invalid') {
query = {'_id': common.db.ObjectID(result)};
}
else {
if (!params.qstring.api_key) {
if (result === 'token-invalid') {
common.returnMessage(params, 400, 'Token not valid');
return false;
}
else {
common.returnMessage(params, 400, 'Missing parameter "api_key" or "auth_token"');
return false;
}
}
params.qstring.api_key = params.qstring.api_key + "";
query = {'api_key': params.qstring.api_key};
}
common.db.collection('members').findOne(query, function(err, member) {
if (!member || err) {
common.returnMessage(params, 401, 'User does not exist');
reject('User does not exist');
return false;
}
if (!member.global_admin && /*appIdExceptions.indexOf(feature) === -1 && */ typeof params.qstring.app_id === "undefined") {
common.returnMessage(params, 401, 'No app_id provided');
reject('No app_id provided');
return false;
}
if (!member.global_admin) {
if (typeof member.permission !== 'undefined') {
var isPermissionObjectExistForAccessType = (typeof member.permission[accessType] === "object" && typeof member.permission[accessType][params.qstring.app_id] === "object");
var isFeatureAllowedInRelatedPermissionObject = false;
// if feature name passed as single string
if (typeof feature === "string") {
isFeatureAllowedInRelatedPermissionObject = isPermissionObjectExistForAccessType && (member.permission[accessType][params.qstring.app_id].all || (member.permission[accessType][params.qstring.app_id].allowed && member.permission[accessType][params.qstring.app_id].allowed[feature]));
}
// or feature name passed as string array
else {
isFeatureAllowedInRelatedPermissionObject = false;
for (var i = 0; i < feature.length; i++) {
if (isPermissionObjectExistForAccessType && (member.permission[accessType][params.qstring.app_id].all || (member.permission[accessType][params.qstring.app_id].allowed && member.permission[accessType][params.qstring.app_id].allowed[feature[i]]))) {
isFeatureAllowedInRelatedPermissionObject = true;
break;
}
}
}
var hasAdminAccess = (typeof member.permission === "object" && typeof member.permission._ === "object" && typeof member.permission._.a === "object") && member.permission._.a.indexOf(params.qstring.app_id) > -1;
// don't allow if user has not permission for feature and has no admin access for current app
if (!(isFeatureAllowedInRelatedPermissionObject) && !(hasAdminAccess)) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
return false;
}
}
else {
if (!module.exports.hasAdminAccess(member, params.qstring.app_id)) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
return false;
}
}
}
if (member && member.locked) {
common.returnMessage(params, 401, 'User is locked');
reject('User is locked');
return false;
}
if (params.qstring.app_id) {
common.db.collection('apps').findOne({'_id': common.db.ObjectID(params.qstring.app_id + "")}, function(err1, app) {
if (!app) {
common.returnMessage(params, 401, 'App does not exist');
reject('App does not exist');
return false;
}
else if ((params.populator || params.qstring.populator) && app.locked) {
common.returnMessage(params, 403, 'App is locked');
reject('App is locked');
return false;
}
else if (app) {
params.app_id = app._id;
params.app = app;
params.appTimezone = app.timezone;
params.time = common.initTimeObj(params.appTimezone, params.qstring.timestamp);
}
params.member = member;
if (plugins.dispatch("/validation/user", {params: params})) {
if (!params.res.finished) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
}
return false;
}
resolve(callbackParam);
});
}
else {
params.member = member;
if (plugins.dispatch("/validation/user", {params: params})) {
if (!params.res.finished) {
common.returnMessage(params, 401, 'User does not have right');
reject('User does not have right');
}
return false;
}
resolve(callbackParam);
}
});
},
function() {
common.returnMessage(params, 401, 'Token is invalid');
reject('Token is invalid');
return false;
});
});
}
/**
* Creates filter object to filter by member allowed collections
* @param {object} member - members object from params
* @param {string} dbName - database name as string
* @param {string} collectionName - collection Name
* @returns {object} filter object
*/
exports.getBaseAppFilter = function(member, dbName, collectionName) {
var base_filter = {};
var apps = exports.getUserApps(member);
if (dbName === "countly_drill" && collectionName === "drill_events") {
if (Array.isArray(apps) && apps.length > 0) {
base_filter.a = {"$in": apps};
}
}
else if (dbName === "countly" && collectionName === "events_data") {
var in_array = [];
if (Array.isArray(apps) && apps.length > 0) {
for (var i = 0; i < apps.length; i++) {
in_array.push(new RegExp("^" + apps[i] + "_.*"));
}
base_filter = {"_id": {"$in": in_array}};
}
}
return base_filter;
};
/**
* Validate user for create access by api_key for provided app_id (both required parameters for the request).
* @param {params} params - {@link params} object
* @param {string} feature - feature that trying to access
* @param {function} callback - function to call only if validation passes
* @param {any=} callbackParam - parameter to pass to callback function (params is automatically passed to callback function, no need to include that)
*/
exports.validateCreate = function(params, feature, callback, callbackParam) {
validateWrite(params, feature, 'c', callback, callbackParam);
};
/**
* Validate user for update access by api_key for provided app_id (both required parameters for the request).
* @param {params} params - {@link params} object
* @param {string} feature - feature that trying to access
* @param {function} callback - function to call only if validation passes
* @param {any=} callbackParam - parameter to pass to callback function (params is automatically passed to callback function, no need to include that)
*/
exports.validateUpdate = function(params, feature, callback, callbackParam) {
validateWrite(params, feature, 'u', callback, callbackParam);
};
/**
* Validate user for delete access by api_key for provided app_id (both required parameters for the request).
* @param {params} params - {@link params} object
* @param {string} feature - feature that trying to access
* @param {function} callback - function to call only if validation passes
* @param {any=} callbackParam - parameter to pass to callback function (params is automatically passed to callback function, no need to include that)
*/
exports.validateDelete = function(params, feature, callback, callbackParam) {
validateWrite(params, feature, 'd', callback, callbackParam);
};
/**
* Is user has admin access on selected app?
* @param {object} member - member object from params
* @param {string} app_id - id value of related app
* @param {string} type - type of access (c, r, u, d)
* @returns {boolean} isAdmin - is that user has admin access on that app?
*/
exports.hasAdminAccess = function(member, app_id, type) {
var hasPermissionObject = typeof member.permission !== "undefined";
if (hasPermissionObject && member.permission._ && member.permission._.a && member.permission._.a.includes(app_id)) {
return true;
}
var isAdmin = true;
// check users who has permission property
if (hasPermissionObject) {
var types = type ? [type] : ["c", "r", "u", "d"];
for (var i = 0; i < types.length; i++) {
if (member.permission[types[i]] && member.permission[types[i]][app_id] && !member.permission[types[i]][app_id].all) {
isAdmin = false;
}
}
}
// check legacy users who has admin_of property
// users should have at least one app in admin_of array
else {
isAdmin = typeof member.admin_of !== "undefined" && member.admin_of.indexOf(app_id) > -1;
}
return isAdmin || member.global_admin;
};
exports.hasCreateRight = function(feature, app_id, member) {
var hasAppSpecificRight = (member.permission && member.permission.c && member.permission.c[app_id] && member.permission.c[app_id].allowed && member.permission.c[app_id].allowed[feature]);
var hasGlobalAdminRight = member.global_admin;
var hasAppAdminRight = exports.hasAdminAccess(member, app_id, "c");
return hasAppSpecificRight || hasGlobalAdminRight || hasAppAdminRight;
};
exports.hasReadRight = function(feature, app_id, member) {
var hasAppSpecificRight = (member.permission && member.permission.r && member.permission.r[app_id] && member.permission.r[app_id].allowed && member.permission.r[app_id].allowed[feature]);
var hasGlobalAdminRight = member.global_admin;
var hasAppAdminRight = exports.hasAdminAccess(member, app_id, "r");
return hasAppSpecificRight || hasGlobalAdminRight || hasAppAdminRight;
};
exports.hasUpdateRight = function(feature, app_id, member) {
var hasAppSpecificRight = (member.permission && member.permission.u && member.permission.u[app_id] && member.permission.u[app_id].allowed && member.permission.u[app_id].allowed[feature]);
var hasGlobalAdminRight = member.global_admin;
var hasAppAdminRight = exports.hasAdminAccess(member, app_id, "u");
return hasAppSpecificRight || hasGlobalAdminRight || hasAppAdminRight;
};
exports.hasDeleteRight = function(feature, app_id, member) {
var hasAppSpecificRight = (member.permission && member.permission.d && member.permission.d[app_id] && member.permission.d[app_id].allowed && member.permission.d[app_id].allowed[feature]);
var hasGlobalAdminRight = member.global_admin;
var hasAppAdminRight = exports.hasAdminAccess(member, app_id, "d");
return hasAppSpecificRight || hasGlobalAdminRight || hasAppAdminRight;
};
exports.getUserApps = function(member) {
let userApps = [];
if (member.global_admin) {
return userApps;
}
else {
if (typeof member.permission !== "undefined") {
for (var i = 0; i < member.permission._.u.length; i++) {
userApps = userApps.concat(member.permission._.u[i]);
}
return userApps.concat(member.permission._.a);
}
else {
return member.user_of;
}
}
};
exports.getUserAppsForFeaturePermission = function(member, feature, permissionType) {
let userApps = [];
if (member.global_admin) {
return userApps;
}
if (typeof member.permission !== "undefined") {
const permissionList = member.permission[permissionType];
for (var appId in permissionList) {
const targetPermissionForApp = permissionList[appId];
if (targetPermissionForApp.all === true || targetPermissionForApp.allowed[feature] === true) {
userApps.push(appId);
}
}
}
return userApps;
};
exports.getAdminApps = function(member) {
if (member.global_admin) {
return [];
}
else {
if (typeof member.permission !== "undefined") {
return member.permission._.a;
}
else {
return member.admin_of;
}
}
};