Skip to content

Commit 8d3118a

Browse files
authored
Merge pull request #5986 from Countly/ar2rsawseen/24.10
DBViewever improvements
2 parents 6e08c92 + 25adad5 commit 8d3118a

File tree

6 files changed

+166
-12
lines changed

6 files changed

+166
-12
lines changed

api/utils/requestProcessor.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const Promise = require('bluebird');
77
const url = require('url');
88
const common = require('./common.js');
99
const countlyCommon = require('../lib/countly.common.js');
10-
const { validateAppAdmin, validateUser, validateRead, validateUserForRead, validateUserForWrite, validateGlobalAdmin, dbUserHasAccessToCollection, validateUpdate, validateDelete, validateCreate } = require('./rights.js');
10+
const { validateAppAdmin, validateUser, validateRead, validateUserForRead, validateUserForWrite, validateGlobalAdmin, dbUserHasAccessToCollection, validateUpdate, validateDelete, validateCreate, getBaseAppFilter } = require('./rights.js');
1111
const authorize = require('./authorizer.js');
1212
const taskmanager = require('./taskmanager.js');
1313
const plugins = require('../../plugins/pluginManager.js');
@@ -2128,7 +2128,7 @@ const processRequest = (params) => {
21282128
}
21292129

21302130
dbUserHasAccessToCollection(params, params.qstring.collection, (hasAccess) => {
2131-
if (hasAccess) {
2131+
if (hasAccess || (params.qstring.db === "countly_drill" && params.qstring.collection === "drill_events") || (params.qstring.db === "countly" && params.qstring.collection === "events_data")) {
21322132
var dbs = { countly: common.db, countly_drill: common.drillDb, countly_out: common.outDb, countly_fs: countlyFs.gridfs.getHandler() };
21332133
var db = "";
21342134
if (params.qstring.db && dbs[params.qstring.db]) {
@@ -2137,6 +2137,23 @@ const processRequest = (params) => {
21372137
else {
21382138
db = common.db;
21392139
}
2140+
if (!params.member.global_admin && params.qstring.collection === "drill_events" || params.qstring.collection === "events_data") {
2141+
var base_filter = getBaseAppFilter(params.member, params.qstring.db, params.qstring.collection);
2142+
if (base_filter && Object.keys(base_filter).length > 0) {
2143+
params.qstring.query = params.qstring.query || {};
2144+
for (var key in base_filter) {
2145+
if (params.qstring.query[key]) {
2146+
params.qstring.query.$and = params.qstring.query.$and || [];
2147+
params.qstring.query.$and.push({[key]: base_filter[key]});
2148+
params.qstring.query.$and.push({[key]: params.qstring.query[key]});
2149+
delete params.qstring.query[key];
2150+
}
2151+
else {
2152+
params.qstring.query[key] = base_filter[key];
2153+
}
2154+
}
2155+
}
2156+
}
21402157
countlyApi.data.exports.fromDatabase({
21412158
db: db,
21422159
params: params,

api/utils/rights.js

+26-1
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,32 @@ function validateWrite(params, feature, accessType, callback, callbackParam) {
10831083
});
10841084
});
10851085
}
1086-
1086+
/**
1087+
* Creates filter object to filter by member allowed collections
1088+
* @param {object} member - members object from params
1089+
* @param {string} dbName - database name as string
1090+
* @param {string} collectionName - collection Name
1091+
* @returns {object} filter object
1092+
*/
1093+
exports.getBaseAppFilter = function(member, dbName, collectionName) {
1094+
var base_filter = {};
1095+
var apps = exports.getUserApps(member);
1096+
if (dbName === "countly_drill" && collectionName === "drill_events") {
1097+
if (Array.isArray(apps) && apps.length > 0) {
1098+
base_filter.a = {"$in": apps};
1099+
}
1100+
}
1101+
else if (dbName === "countly" && collectionName === "events_data") {
1102+
var in_array = [];
1103+
if (Array.isArray(apps) && apps.length > 0) {
1104+
for (var i = 0; i < apps.length; i++) {
1105+
in_array.push(new RegExp("^" + apps[i] + "_.*"));
1106+
}
1107+
base_filter = {"_id": {"$in": in_array}};
1108+
}
1109+
}
1110+
return base_filter;
1111+
};
10871112
/**
10881113
* Validate user for create access by api_key for provided app_id (both required parameters for the request).
10891114
* @param {params} params - {@link params} object

plugins/dbviewer/api/api.js

+111-8
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,88 @@ var common = require('../../../api/utils/common.js'),
55
countlyFs = require('../../../api/utils/countlyFs.js'),
66
_ = require('underscore'),
77
taskManager = require('../../../api/utils/taskmanager.js'),
8-
{ getCollectionName, dbUserHasAccessToCollection, dbLoadEventsData, validateUser, getUserApps, validateGlobalAdmin, hasReadRight } = require('../../../api/utils/rights.js'),
8+
{ getCollectionName, dbUserHasAccessToCollection, dbLoadEventsData, validateUser, getUserApps, validateGlobalAdmin, hasReadRight, getBaseAppFilter } = require('../../../api/utils/rights.js'),
99
exported = {};
1010
const { MongoInvalidArgumentError } = require('mongodb');
1111

1212
const { EJSON } = require('bson');
1313

1414
const FEATURE_NAME = 'dbviewer';
15+
const whiteListedAggregationStages = {
16+
"$addFields": true,
17+
"$bucket": true,
18+
"$bucketAuto": true,
19+
//"$changeStream": false,
20+
//"$changeStreamSplitLargeEvents": false,
21+
//"$collStats": false,
22+
"$count": true,
23+
//"$currentOp": false,
24+
"$densify": true,
25+
//"$documents": false
26+
"$facet": true,
27+
"$fill": true,
28+
"$geoNear": true,
29+
"$graphLookup": true,
30+
"$group": true,
31+
//"$indexStats": false,
32+
"$limit": true,
33+
//"$listLocalSessions": false
34+
//"$listSampledQueries": false
35+
//"$listSearchIndexes": false
36+
//"$listSessions": false
37+
//"$lookup": false
38+
"$match": true,
39+
//"$merge": false
40+
//"$mergeCursors": false
41+
//"$out": false
42+
//"$planCacheStats": false,
43+
"$project": true,
44+
"$querySettings": true,
45+
"$redact": true,
46+
"$replaceRoot": true,
47+
"$replaceWith": true,
48+
"$sample": true,
49+
"$search": true,
50+
"$searchMeta": true,
51+
"$set": true,
52+
"$setWindowFields": true,
53+
//"$sharedDataDistribution": false,
54+
"$skip": true,
55+
"$sort": true,
56+
"$sortByCount": true,
57+
//"$unionWith": false,
58+
"$unset": true,
59+
"$unwind": true,
60+
"$vectorSearch": true //atlas specific
61+
};
1562
var spawn = require('child_process').spawn,
1663
child;
64+
1765
(function() {
1866
plugins.register("/permissions/features", function(ob) {
1967
ob.features.push(FEATURE_NAME);
2068
});
69+
/**
70+
* Function removes not allowed aggregation stages from the pipeline
71+
* @param {array} aggregation - current aggregation pipeline
72+
* @returns {object} changes - object with information which operations were removed
73+
*/
74+
function escapeNotAllowedAggregationStages(aggregation) {
75+
var changes = {};
76+
for (var z = 0; z < aggregation.length; z++) {
77+
for (var key in aggregation[z]) {
78+
if (!whiteListedAggregationStages[key]) {
79+
changes[key] = true;
80+
delete aggregation[z][key];
81+
}
82+
}
83+
if (Object.keys(aggregation[z]).length === 0) {
84+
aggregation.splice(z, 1);
85+
z--;
86+
}
87+
}
88+
return changes;
89+
}
2190

2291
/**
2392
* @api {get} /o/db Access database
@@ -179,6 +248,25 @@ var spawn = require('child_process').spawn,
179248
filter = {};
180249
}
181250

251+
var base_filter = {};
252+
if (!params.member.global_admin) {
253+
base_filter = getBaseAppFilter(params.member, dbNameOnParam, params.qstring.collection);
254+
}
255+
256+
if (base_filter && Object.keys(base_filter).length > 0) {
257+
for (var key in base_filter) {
258+
if (filter[key]) {
259+
filter.$and = filter.$and || [];
260+
filter.$and.push({[key]: base_filter[key]});
261+
filter.$and.push({[key]: filter[key]});
262+
delete filter[key];
263+
}
264+
else {
265+
filter[key] = base_filter[key];
266+
}
267+
}
268+
}
269+
182270
if (dbs[dbNameOnParam]) {
183271
try {
184272
var cursor = dbs[dbNameOnParam].collection(params.qstring.collection).find(filter, { projection });
@@ -191,6 +279,7 @@ var spawn = require('child_process').spawn,
191279
common.returnMessage(params, 400, "Invalid collection name: Collection names can not contain '$' or other invalid characters");
192280
}
193281
else {
282+
log.e(error);
194283
common.returnMessage(params, 500, "An unexpected error occurred.");
195284
}
196285
return false;
@@ -291,7 +380,7 @@ var spawn = require('child_process').spawn,
291380
async.each(results, function(col, done) {
292381
if (col.collectionName.indexOf("system.indexes") === -1 && col.collectionName.indexOf("sessions_") === -1) {
293382
userHasAccess(params, col.collectionName, params.qstring.app_id, function(hasAccess) {
294-
if (hasAccess) {
383+
if (hasAccess || col.collectionName === "events_data" || col.collectionName === "drill_events") {
295384
ob = parseCollectionName(col.collectionName, lookup);
296385
db.collections[ob.pretty] = ob.name;
297386
}
@@ -318,8 +407,9 @@ var spawn = require('child_process').spawn,
318407
* Get aggregated result by the parameter on the url
319408
* @param {string} collection - collection will be applied related query
320409
* @param {object} aggregation - aggregation object
410+
* @param {object} changes - object referencing removed stages from pipeline
321411
* */
322-
function aggregate(collection, aggregation) {
412+
function aggregate(collection, aggregation, changes) {
323413
if (params.qstring.iDisplayLength) {
324414
aggregation.push({ "$limit": parseInt(params.qstring.iDisplayLength) });
325415
}
@@ -339,6 +429,10 @@ var spawn = require('child_process').spawn,
339429
else if (collection === 'auth_tokens') {
340430
aggregation.splice(addProjectionAt, 0, {"$addFields": {"_id": "***redacted***"}});
341431
}
432+
else if ((collection === "events_data" || collection === "drill_events") && !params.member.global_admin) {
433+
var base_filter = getBaseAppFilter(params.member, dbNameOnParam, params.qstring.collection);
434+
aggregation.splice(0, 0, {"$match": base_filter});
435+
}
342436
// check task is already running?
343437
taskManager.checkIfRunning({
344438
db: dbs[dbNameOnParam],
@@ -375,7 +469,7 @@ var spawn = require('child_process').spawn,
375469
},
376470
outputData: function(aggregationErr, result) {
377471
if (!aggregationErr) {
378-
common.returnOutput(params, { sEcho: params.qstring.sEcho, iTotalRecords: 0, iTotalDisplayRecords: 0, "aaData": result });
472+
common.returnOutput(params, { sEcho: params.qstring.sEcho, iTotalRecords: 0, iTotalDisplayRecords: 0, "aaData": result, "removed": (changes || {}) });
379473
}
380474
else {
381475
common.returnMessage(params, 500, aggregationErr);
@@ -409,7 +503,12 @@ var spawn = require('child_process').spawn,
409503

410504
if (appId) {
411505
if (hasReadRight(FEATURE_NAME, appId, parameters.member)) {
412-
return dbUserHasAccessToCollection(parameters, collection, appId, callback);
506+
if (collection === "events_data" || collection === "drill_events") {
507+
return callback(true);
508+
}
509+
else {
510+
return dbUserHasAccessToCollection(parameters, collection, appId, callback);
511+
}
413512
}
414513
}
415514
else {
@@ -485,10 +584,14 @@ var spawn = require('child_process').spawn,
485584
}
486585
else {
487586
userHasAccess(params, params.qstring.collection, function(hasAccess) {
488-
if (hasAccess) {
587+
if (hasAccess || params.qstring.collection === "events_data" || params.qstring.collection === "drill_events") {
489588
try {
490589
let aggregation = EJSON.parse(params.qstring.aggregation);
491-
aggregate(params.qstring.collection, aggregation);
590+
var changes = escapeNotAllowedAggregationStages(aggregation);
591+
if (changes && Object.keys(changes).length > 0) {
592+
log.d("Removed stages from pipeline: ", JSON.stringify(changes));
593+
}
594+
aggregate(params.qstring.collection, aggregation, changes);
492595
}
493596
catch (e) {
494597
common.returnMessage(params, 500, 'Aggregation object is not valid.');
@@ -508,7 +611,7 @@ var spawn = require('child_process').spawn,
508611
}
509612
else {
510613
userHasAccess(params, params.qstring.collection, function(hasAccess) {
511-
if (hasAccess) {
614+
if (hasAccess || params.qstring.collection === "events_data" || params.qstring.collection === "drill_events") {
512615
dbGetCollection();
513616
}
514617
else {

plugins/dbviewer/frontend/public/javascripts/countly.views.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,13 @@
534534
if (res.aaData.length) {
535535
self.fields = Object.keys(map);
536536
}
537+
if (res.removed && typeof res.removed === 'object' && Object.keys(res.removed).length > 0) {
538+
self.removed = CV.i18n('dbviewer.removed-warning') + Object.keys(res.removed).join(", ");
539+
540+
}
541+
else {
542+
self.removed = "";
543+
}
537544
}
538545
if (err) {
539546
var message = CV.i18n('dbviewer.server-error');
@@ -559,7 +566,7 @@
559566
}
560567
},
561568
updatePath: function(query) {
562-
window.location.hash = "#/manage/db/aggregate/" + this.db + "/" + this.collection + "/" + query;
569+
app.navigate("#/manage/db/aggregate/" + this.db + "/" + this.collection + "/" + query);
563570
},
564571
getCollectionName: function() {
565572
var self = this;

plugins/dbviewer/frontend/public/localization/dbviewer.properties

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ dbviewer.generate-aggregate-report= Generate aggregate report
3636
dbviewer.back-to-dbviewer = Back to DB Viewer
3737
dbviewer.invalid-pipeline = Invalid pipeline object, please check pipeline input.
3838
dbviewer.server-error = There was a server error. There might be more information in logs.
39+
dbviewer.removed-warning = Some stages are removed from aggregation pipleine. Following stages are allowed only with global admin rights:
3940
dbviewer.not-found-data = Couldn't find any results
4041
dbviewer.execute-aggregation = Execute Aggregation on {0}
4142
dbviewer.prepare-new-aggregation = Prepare New Aggregation

plugins/dbviewer/frontend/public/templates/aggregate.html

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
</el-button>
2626
</div>
2727
</cly-section>
28+
<cly-notification v-if="removed" class="bu-mb-5 cly-vue-events-all__alerts" :text="removed" ></cly-notification>
2829
<cly-section>
2930
<cly-datatable-n :force-loading="queryLoading" :rows="aggregationResult" :prevent-default-sort="true">
3031
<template v-slot="scope">

0 commit comments

Comments
 (0)