diff --git a/controllers/accessPaths/board.js b/controllers/accessPaths/board.js deleted file mode 100644 index ffd8953..0000000 --- a/controllers/accessPaths/board.js +++ /dev/null @@ -1,24 +0,0 @@ -const _ = require('lodash'); - -/** - * @description accesspath rule definition for user's board - * - */ - -module.exports = { - ruleName: 'board', - isMatch(user, payload) { - payload = Array.isArray(payload) ? payload : [payload]; - const userBoards = _.get(user, 'framework.board'); - if (!userBoards) return false; - if (!Array.isArray(userBoards)) return false; - return _.some(payload, board => { - board = _.toLower(board); - if (_.find(userBoards, userBoard => _.toLower(userBoard) === board)) { - return true; - } - - return false; - }); - } -}; \ No newline at end of file diff --git a/controllers/accessPaths/dynamicCategoryManager.js b/controllers/accessPaths/dynamicCategoryManager.js new file mode 100644 index 0000000..088e6b4 --- /dev/null +++ b/controllers/accessPaths/dynamicCategoryManager.js @@ -0,0 +1,26 @@ +const _ = require('lodash'); +const { channelRead, frameworkRead } = require('../../helpers/learnerHelper'); +const debug = require('debug')('dynamicCategoryManager'); + +class DynamicCategoryManager { + async getCategoriesForChannel(channelId) { + try { + const channelReadResponse = await channelRead({ channelId }); + const frameworkName = _.get(channelReadResponse, 'data.result.channel.defaultFramework'); + if (!frameworkName) { + throw new Error('Default framework missing'); + } + const frameworkReadResponse = await frameworkRead({ frameworkId: frameworkName }); + const frameworkData = _.get(frameworkReadResponse, 'data.result.framework'); + const categories = _.map(frameworkData.categories, 'code'); + + return categories; + } catch (error) { + debug('Failed to fetch framework categories', error); + return []; + } + } + +} + +module.exports = new DynamicCategoryManager(); \ No newline at end of file diff --git a/controllers/accessPaths/gradeLevel.js b/controllers/accessPaths/gradeLevel.js deleted file mode 100644 index 4bc1ae7..0000000 --- a/controllers/accessPaths/gradeLevel.js +++ /dev/null @@ -1,24 +0,0 @@ -const _ = require('lodash'); - -/** - * @description accesspath rule definition for user's gradeLevel - * - */ - -module.exports = { - ruleName: 'gradeLevel', - isMatch(user, payload) { - payload = Array.isArray(payload) ? payload : [payload]; - const userGradeLevels = _.get(user, 'framework.gradeLevel'); - if (!userGradeLevels) return false; - if (!Array.isArray(userGradeLevels)) return false; - return _.some(payload, gradeLevel => { - gradeLevel = _.toLower(gradeLevel); - if (_.find(userGradeLevels, userGradeLevel => _.toLower(userGradeLevel) === gradeLevel)) { - return true; - } - - return false; - }); - } -}; \ No newline at end of file diff --git a/controllers/accessPaths/index.js b/controllers/accessPaths/index.js index 4ea6f50..1606aa7 100644 --- a/controllers/accessPaths/index.js +++ b/controllers/accessPaths/index.js @@ -1,6 +1,7 @@ const fs = require('fs'); const path = require('path'); const _ = require('lodash'); +const dynamicCategoryManager = require('./dynamicCategoryManager'); const CONSTANTS = require('../../resources/constants.json'); const { isUserAdmin } = require('../../helpers/userHelper'); @@ -34,7 +35,7 @@ const isCreatorOfReport = ({ user, report }) => _.get(report, 'createdby') === ( * @description Validates a user context against the accesspath rules set for the report * @param {*} user */ -const validateAccessPath = user => report => { +const validateAccessPath = (user, req) => async report => { let { accesspath, type } = report; if (type === CONSTANTS.REPORT_TYPE.PUBLIC) return true; @@ -49,11 +50,37 @@ const validateAccessPath = user => report => { accesspath = accessPathForPrivateReports({ user }); } + const channelId = req.get('x-channel-id') || + req.get('X-CHANNEL-ID') || + _.get(user, 'rootOrg.hashTagId') || + _.get(user, 'channel'); + if (!channelId) { + const error = new Error('Channel ID is required'); + error.statusCode = 400; + error.errorObject = { code: 'MISSING_CHANNEL_ID' }; + throw error; + } + const dynamicCategories = await dynamicCategoryManager.getCategoriesForChannel(channelId); + for (let [key, value] of Object.entries(accesspath)) { - if (!rules.has(key)) return false; - const validator = rules.get(key); - const success = validator(user, value); - if (!success) return false; + if (rules.has(key)) { + const validator = rules.get(key); + const success = validator(user, value); + + if (!success && dynamicCategories.includes(key)) { + const normalizedValue = Array.isArray(value) ? value : [value]; + const userValues = _.get(user, `framework.${key}`, []); + const normalizedUserValues = Array.isArray(userValues) ? userValues : [userValues]; + + const hasMatch = normalizedValue.some(val => + normalizedUserValues.some(uv => + String(uv).toLowerCase() === String(val).toLowerCase() + ) + ); + + if (!hasMatch) return false; + } + } } return true; diff --git a/controllers/accessPaths/medium.js b/controllers/accessPaths/medium.js deleted file mode 100644 index 003ff6b..0000000 --- a/controllers/accessPaths/medium.js +++ /dev/null @@ -1,24 +0,0 @@ -const _ = require('lodash'); - -/** - * @description accesspath rule definition of user's medium - * - */ - -module.exports = { - ruleName: 'medium', - isMatch(user, payload) { - payload = Array.isArray(payload) ? payload : [payload]; - const userMediums = _.get(user, 'framework.medium'); - if (!userMediums) return false; - if (!Array.isArray(userMediums)) return false; - return _.some(payload, medium => { - medium = _.toLower(medium); - if (_.find(userMediums, userMedium => _.toLower(userMedium) === medium)) { - return true; - } - - return false; - }); - } -}; \ No newline at end of file diff --git a/controllers/accessPaths/subject.js b/controllers/accessPaths/subject.js deleted file mode 100644 index b75599f..0000000 --- a/controllers/accessPaths/subject.js +++ /dev/null @@ -1,24 +0,0 @@ -const _ = require('lodash'); - -/** - * @description accesspath rule definition of user's subjects value - * - */ - -module.exports = { - ruleName: 'subject', - isMatch(user, payload) { - payload = Array.isArray(payload) ? payload : [payload]; - const userSubjects = _.get(user, 'framework.subject'); - if (!userSubjects) return false; - if (!Array.isArray(userSubjects)) return false; - return _.some(payload, subject => { - subject = _.toLower(subject); - if (_.find(userSubjects, userSubject => _.toLower(userSubject) === subject)) { - return true; - } - - return false; - }); - } -}; \ No newline at end of file diff --git a/controllers/parameters/$board.js b/controllers/parameters/$board.js deleted file mode 100644 index c5c741f..0000000 --- a/controllers/parameters/$board.js +++ /dev/null @@ -1,26 +0,0 @@ -const _ = require('lodash'); -var debug = require('debug')('parameters:$board'); - -const { channelRead, frameworkRead } = require('../../helpers/learnerHelper'); - -module.exports = { - name: '$board', - value: (user) => _.get(user, 'framework.board'), - cache: false, - async masterData({ user, req }) { - try { - const channelId = req.get('x-channel-id') || req.get('X-CHANNEL-ID') || _.get(user, 'rootOrg.hashTagId') || _.get(user, 'channel'); - const channelReadResponse = await channelRead({ channelId }); - const frameworkName = _.get(channelReadResponse, 'data.result.channel.defaultFramework'); - if (!frameworkName) throw new Error('default framework missing'); - const frameworkReadResponse = await frameworkRead({ frameworkId: frameworkName }); - const frameworkData = _.get(frameworkReadResponse, 'data.result.framework'); - const boardCategory = _.find(frameworkData.categories, ['code', 'board']); - if (!_.get(boardCategory, 'terms') && !Array.isArray(boardCategory.terms)) { return of([]); } - return _.map(boardCategory.terms, 'name'); - } catch (error) { - debug('$board masterData fetch failed', JSON.stringify(error)); - return []; - } - } -}; \ No newline at end of file diff --git a/controllers/parameters/index.js b/controllers/parameters/index.js index ce3a337..0ff0daa 100644 --- a/controllers/parameters/index.js +++ b/controllers/parameters/index.js @@ -8,6 +8,7 @@ const { getSharedAccessSignature } = require('../../helpers/azure-storage'); const { envVariables } = require('../../helpers/envHelpers'); const { isUserSuperAdmin } = require('../../helpers/userHelper'); const CONSTANTS = require('../../resources/constants.json'); +const { channelRead, frameworkRead } = require('../../helpers/learnerHelper'); /* @@ -204,4 +205,57 @@ const getDatasets = async ({ document, user, req }) => { return Promise.all(dataSources.map(dataSource => getDataset({ dataSource, user, req }))); }; -module.exports = { populateReportsWithParameters, getDatasets, reportParameters: parameters, isReportParameterized }; \ No newline at end of file +const setFrameworkCategoryParameters = async (req, user) => { + const channelId = + req.get('x-channel-id') || + req.get('X-CHANNEL-ID') || + _.get(user, 'rootOrg.hashTagId') || + _.get(user, 'channel'); + + if (!channelId) { + const error = new Error('Channel ID is required'); + error.statusCode = 400; + error.errorObject = { code: 'MISSING_CHANNEL_ID' }; + throw error; + } + + try { + const channelReadResponse = await channelRead({ channelId }); + const frameworkName = _.get(channelReadResponse, 'data.result.channel.defaultFramework'); + if (!frameworkName) { + const error = new Error('Default framework not found for the channel'); + error.statusCode = 404; + error.errorObject = { code: 'MISSING_DEFAULT_FRAMEWORK' }; + throw error; + } + + const frameworkReadResponse = await frameworkRead({ frameworkId: frameworkName }); + const frameworkData = _.get(frameworkReadResponse, 'data.result.framework'); + const frameworkCategories = _.map(frameworkData.categories, 'code'); + + frameworkCategories.forEach(category => { + parameters[`$${category}`] = { + name: `$${category}`, + value: (user) => _.get(user, `framework.${category}`), + cache: false, + masterData: () => { + const categoryData = _.find(frameworkData.categories, ['code', category]); + return _.map(_.get(categoryData, 'terms', []), 'name'); + } + }; + }); + + Object.values(parameters).forEach(param => { + }); + } catch (error) { + debug(`Failed to set framework category parameters for channel ${channelId}`, error); + } +}; + +module.exports = { + populateReportsWithParameters, + getDatasets, + reportParameters: parameters, + isReportParameterized, + setFrameworkCategoryParameters +}; \ No newline at end of file diff --git a/controllers/report.js b/controllers/report.js index 0728af2..c732db7 100644 --- a/controllers/report.js +++ b/controllers/report.js @@ -8,7 +8,7 @@ const { report, report_status, report_summary } = require('../models'); const CONSTANTS = require('../resources/constants.json'); const { formatApiResponse } = require('../helpers/responseFormatter'); const { validateAccessPath, matchAccessPath, accessPathForPrivateReports, isCreatorOfReport, roleBasedAccess } = require('./accessPaths'); -const { getDatasets, isReportParameterized, populateReportsWithParameters } = require('./parameters'); +const { getDatasets, isReportParameterized, populateReportsWithParameters, setFrameworkCategoryParameters } = require('./parameters'); const { fetchAndFormatExhaustDataset } = require('../helpers/dataServiceHelper'); // checks by reportid if the report exists in our database or not @@ -38,6 +38,7 @@ const search = async (req, res, next) => { }); const userDetails = req.userDetails; + await setFrameworkCategoryParameters(req, userDetails); const documents = populateReportsWithParameters(rows, userDetails); //is accesspath is provided as search filter create a closure to filter reports @@ -61,24 +62,40 @@ const search = async (req, res, next) => { 2- is user report admin or not. 3 - check access path for private and protected reports. */ - filteredReports = _.filter(documents, row => { + // Process all documents in parallel to check access + const reportAccessChecks = await Promise.all(documents.map(async row => { const isCreator = isCreatorOfReport({ user: userDetails, report: row }); - if (isCreator) return true; + if (isCreator) return { row, hasAccess: true }; - if (!roleBasedAccess({ report: row, user: userDetails })) return false; + if (!roleBasedAccess({ report: row, user: userDetails })) { + return { row, hasAccess: false }; + } if (accessPathMatchClosure) { const isMatched = accessPathMatchClosure(row); - if (!isMatched) return false; + if (!isMatched) return { row, hasAccess: false }; } const { type } = row; - if (!type) return false; - if (type === CONSTANTS.REPORT_TYPE.PUBLIC) return true; - if ((type === CONSTANTS.REPORT_TYPE.PRIVATE) || (type === CONSTANTS.REPORT_TYPE.PROTECTED)) { - return validateAccessPath(userDetails)(row); + if (!type) return { row, hasAccess: false }; + if (type === CONSTANTS.REPORT_TYPE.PUBLIC) return { row, hasAccess: true }; + + if (type === CONSTANTS.REPORT_TYPE.PRIVATE || type === CONSTANTS.REPORT_TYPE.PROTECTED) { + try { + const hasAccess = await validateAccessPath(userDetails, req)(row); + return { row, hasAccess }; + } catch (error) { + debug('Error validating access path:', error); + return { row, hasAccess: false }; + } } - }); + + return { row, hasAccess: false }; + })); + + filteredReports = reportAccessChecks + .filter(({ hasAccess }) => hasAccess) + .map(({ row }) => row); } return res.status(200).json(formatApiResponse({ id: req.id, result: { reports: filteredReports, count: filteredReports.length } })); } catch (error) { @@ -229,6 +246,7 @@ const read = async (req, res, next) => { const userDetails = req.userDetails; let document; if (!hash) { + await setFrameworkCategoryParameters(req, userDetails); [document] = populateReportsWithParameters([rawDocument], userDetails); if (!document) return next(createError(401, CONSTANTS.MESSAGES.FORBIDDEN)); } else { @@ -246,7 +264,7 @@ const read = async (req, res, next) => { } if ((type === CONSTANTS.REPORT_TYPE.PROTECTED) || (type === CONSTANTS.REPORT_TYPE.PRIVATE)) { - const isAuthorized = validateAccessPath(userDetails)(document); + const isAuthorized = await validateAccessPath(userDetails, req)(document); if (!isAuthorized) { return next(createError(401, CONSTANTS.MESSAGES.FORBIDDEN)); } @@ -528,6 +546,7 @@ const readWithDatasets = async (req, res, next) => { const user = req.userDetails; let document; if (!hash) { + await setFrameworkCategoryParameters(req, userDetails); [document] = populateReportsWithParameters([rawDocument], user); if (!document) return next(createError(401, CONSTANTS.MESSAGES.FORBIDDEN)); } else { @@ -545,7 +564,7 @@ const readWithDatasets = async (req, res, next) => { } if ((document.type === CONSTANTS.REPORT_TYPE.PRIVATE) || (document.type === CONSTANTS.REPORT_TYPE.PROTECTED)) { - const isAuthorized = validateAccessPath(user)(document); + const isAuthorized = await validateAccessPath(user, req)(document); if (!isAuthorized) { return next(createError(401, CONSTANTS.MESSAGES.FORBIDDEN)); } diff --git a/helpers/dataServiceHelper.js b/helpers/dataServiceHelper.js index 66e1f21..995aa0e 100644 --- a/helpers/dataServiceHelper.js +++ b/helpers/dataServiceHelper.js @@ -3,7 +3,7 @@ var memoryCache = require('memory-cache'); var debug = require('debug')('helpers:dataServiceHelper'); -const { reportParameters } = require('../controllers/parameters'); +const { reportParameters, setFrameworkCategoryParameters} = require('../controllers/parameters'); const { envVariables } = require('./envHelpers'); const { dataServiceProxyUpstream } = require('./upstream_axios'); const { isUserSuperAdmin } = require('./userHelper'); @@ -46,6 +46,7 @@ const fetchAndFormatExhaustDataset = async ({ req, document, user }) => { [parameter] = parameters; } + await setFrameworkCategoryParameters(req, user); const isParameterized = parameter && (parameter in reportParameters); if (isParameterized) { const { masterData, cache = false, value } = reportParameters[parameter]; diff --git a/helpers/envHelpers.js b/helpers/envHelpers.js index 80b00dd..12d0657 100644 --- a/helpers/envHelpers.js +++ b/helpers/envHelpers.js @@ -2,7 +2,7 @@ const { get } = require('lodash'); const env = get(process, 'env'); const fs = require('fs'); -var debug = require('debug')('parameters:$board'); +var debug = require('debug'); const packageObj = JSON.parse(fs.readFileSync('package.json', 'utf8'));