From 2d12e8f22f830cd19fd786cf6893811c5eaf5ced Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 17:36:19 +0300 Subject: [PATCH] [fix][views] apply the token owner's read right on the heatmap endpoint The countly-token branch of /o/actions resolves the target app from the caller supplied app_key and then served the data once the token itself verified. It never resolved the token to the member who created it, so that member's own rights were never consulted. Everywhere else a token acts as its owner: verify_return hands back the owner, and the usual validation loads that member and applies their rights, while the token's app and endpoint fields only narrow things further. This branch skipped that step, which left the optional app restriction as the only thing bounding which app could be read. A token saved without an app restriction is not narrowed at all, which is correct in itself, so nothing remained to bound the read. Load the owner and require a views read right on the app resolved from app_key, the way validateRead does for the api_key branch below it. The heatmap feature has been out of the product for over two years and this endpoint stays only so that a long-standing integration does not break, so this keeps the existing flow working: the dashboard mints its token scoped to the active app, and its owner holds the read right for that app. --- plugins/views/api/api.js | 63 ++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/plugins/views/api/api.js b/plugins/views/api/api.js index 0e3b7863343..cc6099553ae 100644 --- a/plugins/views/api/api.js +++ b/plugins/views/api/api.js @@ -8,7 +8,7 @@ var pluginOb = {}, plugins = require('../../pluginManager.js'), fetch = require('../../../api/parts/data/fetch.js'), log = common.log('views:api'), - { validateRead, validateUpdate, validateDelete } = require('../../../api/utils/rights.js'); + { validateRead, validateUpdate, validateDelete, hasReadRight } = require('../../../api/utils/rights.js'); const viewsUtils = require("./parts/viewsUtils.js"); const FEATURE_NAME = 'views'; @@ -1546,31 +1546,44 @@ const escapedViewSegments = { "name": true, "segment": true, "height": true, "wi callback: function(owner, expires_after) { if (owner) { var token = params.req.headers["countly-token"]; - if (expires_after < 600 && expires_after > -1) { - authorize.extend_token({ - extendTill: Date.now() + 600000, //10 minutes - token: params.req.headers["countly-token"], - callback: function(/*err,res*/) { - params.token_headers = {"countly-token": token, "content-language": token, "Access-Control-Expose-Headers": "countly-token"}; - 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); - getHeatmap(params); - } - }); + //A token acts as its owner: everywhere else it is resolved to the + //member who created it and that member's own rights decide what the + //request may read, while the token's app and endpoint fields only narrow + //it further. This branch used to skip that step, so the app resolved from + //the caller's app_key was served without consulting the owner at all. + //Apply the owner's read right here, the way validateRead does for the + //api_key branch below. + common.db.collection('members').findOne({_id: common.db.ObjectID(owner + "")}, function(memberErr, member) { + if (memberErr || !member || !hasReadRight(FEATURE_NAME, app._id + "", member)) { + common.returnMessage(params, 401, 'User does not have view right for this application'); + return false; + } + if (expires_after < 600 && expires_after > -1) { + authorize.extend_token({ + extendTill: Date.now() + 600000, //10 minutes + token: params.req.headers["countly-token"], + callback: function(/*err,res*/) { + params.token_headers = {"countly-token": token, "content-language": token, "Access-Control-Expose-Headers": "countly-token"}; + 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); + getHeatmap(params); + } + }); - } - else { - params.token_headers = {"countly-token": token, "content-language": token, "Access-Control-Expose-Headers": "countly-token"}; - 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); - getHeatmap(params); - } + } + else { + params.token_headers = {"countly-token": token, "content-language": token, "Access-Control-Expose-Headers": "countly-token"}; + 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); + getHeatmap(params); + } + }); } else { common.returnMessage(params, 401, 'User does not have view right for this application');