diff --git a/api/utils/requestProcessor.js b/api/utils/requestProcessor.js index e7d2417986a..cd19a368b15 100644 --- a/api/utils/requestProcessor.js +++ b/api/utils/requestProcessor.js @@ -7,7 +7,7 @@ const Promise = require('bluebird'); const url = require('url'); const common = require('./common.js'); const countlyCommon = require('../lib/countly.common.js'); -const { validateAppAdmin, validateUser, validateRead, validateUserForRead, validateUserForWrite, validateGlobalAdmin, dbUserHasAccessToCollection, validateUpdate, validateDelete, validateCreate } = require('./rights.js'); +const { validateAppAdmin, validateUser, validateRead, validateUserForRead, validateUserForWrite, validateGlobalAdmin, dbUserHasAccessToCollection, validateUpdate, validateDelete, validateCreate, getBaseAppFilter } = require('./rights.js'); const authorize = require('./authorizer.js'); const taskmanager = require('./taskmanager.js'); const plugins = require('../../plugins/pluginManager.js'); @@ -2128,7 +2128,7 @@ const processRequest = (params) => { } dbUserHasAccessToCollection(params, params.qstring.collection, (hasAccess) => { - if (hasAccess) { + if (hasAccess || (params.qstring.db === "countly_drill" && params.qstring.collection === "drill_events") || (params.qstring.db === "countly" && params.qstring.collection === "events_data")) { var dbs = { countly: common.db, countly_drill: common.drillDb, countly_out: common.outDb, countly_fs: countlyFs.gridfs.getHandler() }; var db = ""; if (params.qstring.db && dbs[params.qstring.db]) { @@ -2137,6 +2137,23 @@ const processRequest = (params) => { else { db = common.db; } + if (!params.member.global_admin && params.qstring.collection === "drill_events" || params.qstring.collection === "events_data") { + var base_filter = getBaseAppFilter(params.member, params.qstring.db, params.qstring.collection); + if (base_filter && Object.keys(base_filter).length > 0) { + params.qstring.query = params.qstring.query || {}; + for (var key in base_filter) { + if (params.qstring.query[key]) { + params.qstring.query.$and = params.qstring.query.$and || []; + params.qstring.query.$and.push({[key]: base_filter[key]}); + params.qstring.query.$and.push({[key]: params.qstring.query[key]}); + delete params.qstring.query[key]; + } + else { + params.qstring.query[key] = base_filter[key]; + } + } + } + } countlyApi.data.exports.fromDatabase({ db: db, params: params, diff --git a/api/utils/rights.js b/api/utils/rights.js index 6cb3024ff6e..5d431752865 100644 --- a/api/utils/rights.js +++ b/api/utils/rights.js @@ -1083,7 +1083,32 @@ function validateWrite(params, feature, accessType, callback, callbackParam) { }); }); } - +/** + * 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 diff --git a/plugins/dbviewer/api/api.js b/plugins/dbviewer/api/api.js index 5ea7f771fef..46c45a19425 100644 --- a/plugins/dbviewer/api/api.js +++ b/plugins/dbviewer/api/api.js @@ -5,19 +5,88 @@ var common = require('../../../api/utils/common.js'), countlyFs = require('../../../api/utils/countlyFs.js'), _ = require('underscore'), taskManager = require('../../../api/utils/taskmanager.js'), - { getCollectionName, dbUserHasAccessToCollection, dbLoadEventsData, validateUser, getUserApps, validateGlobalAdmin, hasReadRight } = require('../../../api/utils/rights.js'), + { getCollectionName, dbUserHasAccessToCollection, dbLoadEventsData, validateUser, getUserApps, validateGlobalAdmin, hasReadRight, getBaseAppFilter } = require('../../../api/utils/rights.js'), exported = {}; const { MongoInvalidArgumentError } = require('mongodb'); const { EJSON } = require('bson'); const FEATURE_NAME = 'dbviewer'; +const whiteListedAggregationStages = { + "$addFields": true, + "$bucket": true, + "$bucketAuto": true, + //"$changeStream": false, + //"$changeStreamSplitLargeEvents": false, + //"$collStats": false, + "$count": true, + //"$currentOp": false, + "$densify": true, + //"$documents": false + "$facet": true, + "$fill": true, + "$geoNear": true, + "$graphLookup": true, + "$group": true, + //"$indexStats": false, + "$limit": true, + //"$listLocalSessions": false + //"$listSampledQueries": false + //"$listSearchIndexes": false + //"$listSessions": false + //"$lookup": false + "$match": true, + //"$merge": false + //"$mergeCursors": false + //"$out": false + //"$planCacheStats": false, + "$project": true, + "$querySettings": true, + "$redact": true, + "$replaceRoot": true, + "$replaceWith": true, + "$sample": true, + "$search": true, + "$searchMeta": true, + "$set": true, + "$setWindowFields": true, + //"$sharedDataDistribution": false, + "$skip": true, + "$sort": true, + "$sortByCount": true, + //"$unionWith": false, + "$unset": true, + "$unwind": true, + "$vectorSearch": true //atlas specific +}; var spawn = require('child_process').spawn, child; + (function() { plugins.register("/permissions/features", function(ob) { ob.features.push(FEATURE_NAME); }); + /** + * Function removes not allowed aggregation stages from the pipeline + * @param {array} aggregation - current aggregation pipeline + * @returns {object} changes - object with information which operations were removed + */ + function escapeNotAllowedAggregationStages(aggregation) { + var changes = {}; + for (var z = 0; z < aggregation.length; z++) { + for (var key in aggregation[z]) { + if (!whiteListedAggregationStages[key]) { + changes[key] = true; + delete aggregation[z][key]; + } + } + if (Object.keys(aggregation[z]).length === 0) { + aggregation.splice(z, 1); + z--; + } + } + return changes; + } /** * @api {get} /o/db Access database @@ -179,6 +248,25 @@ var spawn = require('child_process').spawn, filter = {}; } + var base_filter = {}; + if (!params.member.global_admin) { + base_filter = getBaseAppFilter(params.member, dbNameOnParam, params.qstring.collection); + } + + if (base_filter && Object.keys(base_filter).length > 0) { + for (var key in base_filter) { + if (filter[key]) { + filter.$and = filter.$and || []; + filter.$and.push({[key]: base_filter[key]}); + filter.$and.push({[key]: filter[key]}); + delete filter[key]; + } + else { + filter[key] = base_filter[key]; + } + } + } + if (dbs[dbNameOnParam]) { try { var cursor = dbs[dbNameOnParam].collection(params.qstring.collection).find(filter, { projection }); @@ -191,6 +279,7 @@ var spawn = require('child_process').spawn, common.returnMessage(params, 400, "Invalid collection name: Collection names can not contain '$' or other invalid characters"); } else { + log.e(error); common.returnMessage(params, 500, "An unexpected error occurred."); } return false; @@ -291,7 +380,7 @@ var spawn = require('child_process').spawn, async.each(results, function(col, done) { if (col.collectionName.indexOf("system.indexes") === -1 && col.collectionName.indexOf("sessions_") === -1) { userHasAccess(params, col.collectionName, params.qstring.app_id, function(hasAccess) { - if (hasAccess) { + if (hasAccess || col.collectionName === "events_data" || col.collectionName === "drill_events") { ob = parseCollectionName(col.collectionName, lookup); db.collections[ob.pretty] = ob.name; } @@ -318,8 +407,9 @@ var spawn = require('child_process').spawn, * Get aggregated result by the parameter on the url * @param {string} collection - collection will be applied related query * @param {object} aggregation - aggregation object + * @param {object} changes - object referencing removed stages from pipeline * */ - function aggregate(collection, aggregation) { + function aggregate(collection, aggregation, changes) { if (params.qstring.iDisplayLength) { aggregation.push({ "$limit": parseInt(params.qstring.iDisplayLength) }); } @@ -339,6 +429,10 @@ var spawn = require('child_process').spawn, else if (collection === 'auth_tokens') { aggregation.splice(addProjectionAt, 0, {"$addFields": {"_id": "***redacted***"}}); } + else if ((collection === "events_data" || collection === "drill_events") && !params.member.global_admin) { + var base_filter = getBaseAppFilter(params.member, dbNameOnParam, params.qstring.collection); + aggregation.splice(0, 0, {"$match": base_filter}); + } // check task is already running? taskManager.checkIfRunning({ db: dbs[dbNameOnParam], @@ -375,7 +469,7 @@ var spawn = require('child_process').spawn, }, outputData: function(aggregationErr, result) { if (!aggregationErr) { - common.returnOutput(params, { sEcho: params.qstring.sEcho, iTotalRecords: 0, iTotalDisplayRecords: 0, "aaData": result }); + common.returnOutput(params, { sEcho: params.qstring.sEcho, iTotalRecords: 0, iTotalDisplayRecords: 0, "aaData": result, "removed": (changes || {}) }); } else { common.returnMessage(params, 500, aggregationErr); @@ -409,7 +503,12 @@ var spawn = require('child_process').spawn, if (appId) { if (hasReadRight(FEATURE_NAME, appId, parameters.member)) { - return dbUserHasAccessToCollection(parameters, collection, appId, callback); + if (collection === "events_data" || collection === "drill_events") { + return callback(true); + } + else { + return dbUserHasAccessToCollection(parameters, collection, appId, callback); + } } } else { @@ -485,10 +584,14 @@ var spawn = require('child_process').spawn, } else { userHasAccess(params, params.qstring.collection, function(hasAccess) { - if (hasAccess) { + if (hasAccess || params.qstring.collection === "events_data" || params.qstring.collection === "drill_events") { try { let aggregation = EJSON.parse(params.qstring.aggregation); - aggregate(params.qstring.collection, aggregation); + var changes = escapeNotAllowedAggregationStages(aggregation); + if (changes && Object.keys(changes).length > 0) { + log.d("Removed stages from pipeline: ", JSON.stringify(changes)); + } + aggregate(params.qstring.collection, aggregation, changes); } catch (e) { common.returnMessage(params, 500, 'Aggregation object is not valid.'); @@ -508,7 +611,7 @@ var spawn = require('child_process').spawn, } else { userHasAccess(params, params.qstring.collection, function(hasAccess) { - if (hasAccess) { + if (hasAccess || params.qstring.collection === "events_data" || params.qstring.collection === "drill_events") { dbGetCollection(); } else { diff --git a/plugins/dbviewer/frontend/public/javascripts/countly.views.js b/plugins/dbviewer/frontend/public/javascripts/countly.views.js index 2436521175b..4f2a661466e 100644 --- a/plugins/dbviewer/frontend/public/javascripts/countly.views.js +++ b/plugins/dbviewer/frontend/public/javascripts/countly.views.js @@ -534,6 +534,13 @@ if (res.aaData.length) { self.fields = Object.keys(map); } + if (res.removed && typeof res.removed === 'object' && Object.keys(res.removed).length > 0) { + self.removed = CV.i18n('dbviewer.removed-warning') + Object.keys(res.removed).join(", "); + + } + else { + self.removed = ""; + } } if (err) { var message = CV.i18n('dbviewer.server-error'); @@ -559,7 +566,7 @@ } }, updatePath: function(query) { - window.location.hash = "#/manage/db/aggregate/" + this.db + "/" + this.collection + "/" + query; + app.navigate("#/manage/db/aggregate/" + this.db + "/" + this.collection + "/" + query); }, getCollectionName: function() { var self = this; diff --git a/plugins/dbviewer/frontend/public/localization/dbviewer.properties b/plugins/dbviewer/frontend/public/localization/dbviewer.properties index 018c8d96649..eb102853b63 100644 --- a/plugins/dbviewer/frontend/public/localization/dbviewer.properties +++ b/plugins/dbviewer/frontend/public/localization/dbviewer.properties @@ -36,6 +36,7 @@ dbviewer.generate-aggregate-report= Generate aggregate report dbviewer.back-to-dbviewer = Back to DB Viewer dbviewer.invalid-pipeline = Invalid pipeline object, please check pipeline input. dbviewer.server-error = There was a server error. There might be more information in logs. +dbviewer.removed-warning = Some stages are removed from aggregation pipleine. Following stages are allowed only with global admin rights: dbviewer.not-found-data = Couldn't find any results dbviewer.execute-aggregation = Execute Aggregation on {0} dbviewer.prepare-new-aggregation = Prepare New Aggregation diff --git a/plugins/dbviewer/frontend/public/templates/aggregate.html b/plugins/dbviewer/frontend/public/templates/aggregate.html index 63fa391b593..56c711e00ab 100755 --- a/plugins/dbviewer/frontend/public/templates/aggregate.html +++ b/plugins/dbviewer/frontend/public/templates/aggregate.html @@ -25,6 +25,7 @@ +