Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions controllers/accessPaths/board.js

This file was deleted.

26 changes: 26 additions & 0 deletions controllers/accessPaths/dynamicCategoryManager.js
Original file line number Diff line number Diff line change
@@ -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();
24 changes: 0 additions & 24 deletions controllers/accessPaths/gradeLevel.js

This file was deleted.

37 changes: 32 additions & 5 deletions controllers/accessPaths/index.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
24 changes: 0 additions & 24 deletions controllers/accessPaths/medium.js

This file was deleted.

24 changes: 0 additions & 24 deletions controllers/accessPaths/subject.js

This file was deleted.

26 changes: 0 additions & 26 deletions controllers/parameters/$board.js

This file was deleted.

56 changes: 55 additions & 1 deletion controllers/parameters/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
const { envVariables } = require('../../helpers/envHelpers');
const { isUserSuperAdmin } = require('../../helpers/userHelper');
const CONSTANTS = require('../../resources/constants.json');
const { channelRead, frameworkRead } = require('../../helpers/learnerHelper');

/*

Expand Down Expand Up @@ -125,7 +126,7 @@
const resolvedPath = path.replace(parameter, value);
const promise = getSharedAccessSignature({ filePath: resolvedPath })
.then(({ sasUrl, expiresAt }) => ({ key: value, sasUrl, expiresAt }))
.catch(_ => ({ key: value, sasUrl: null, expiresAt: null }))

Check warning on line 129 in controllers/parameters/index.js

View workflow job for this annotation

GitHub Actions / Run Code Quality Checks

'_' is defined but never used
.then(data => {
const { key, sasUrl, expiresAt } = data;
dataset.data.push({
Expand Down Expand Up @@ -181,7 +182,7 @@
}

} else {
const { sasUrl, expiresAt } = await getSharedAccessSignature({ filePath: path }).catch(error => null);

Check warning on line 185 in controllers/parameters/index.js

View workflow job for this annotation

GitHub Actions / Run Code Quality Checks

'error' is defined but never used
dataset.data = [{
id: 'default',
type: null,
Expand All @@ -204,4 +205,57 @@
return Promise.all(dataSources.map(dataSource => getDataset({ dataSource, user, req })));
};

module.exports = { populateReportsWithParameters, getDatasets, reportParameters: parameters, isReportParameterized };
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 => {

Check warning on line 248 in controllers/parameters/index.js

View workflow job for this annotation

GitHub Actions / Run Code Quality Checks

'param' is defined but never used
});
} catch (error) {
debug(`Failed to set framework category parameters for channel ${channelId}`, error);
}
};

module.exports = {
populateReportsWithParameters,
getDatasets,
reportParameters: parameters,
isReportParameterized,
setFrameworkCategoryParameters
};
43 changes: 31 additions & 12 deletions controllers/report.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { Op } = require('sequelize');

Check warning on line 1 in controllers/report.js

View workflow job for this annotation

GitHub Actions / Run Code Quality Checks

'Op' is assigned a value but never used
const createError = require('http-errors');
var debug = require('debug')('controllers:report');
const _ = require('lodash');
Expand All @@ -8,7 +8,7 @@
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
Expand Down Expand Up @@ -38,6 +38,7 @@
});

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
Expand All @@ -61,24 +62,40 @@
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) {
Expand Down Expand Up @@ -229,6 +246,7 @@
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 {
Expand All @@ -246,7 +264,7 @@
}

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));
}
Expand Down Expand Up @@ -528,6 +546,7 @@
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 {
Expand All @@ -545,7 +564,7 @@
}

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));
}
Expand Down
Loading
Loading