From 35ad3be9690d9c4548134e494edb70a980375da8 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 Dec 2013 20:34:21 +0000 Subject: [PATCH] Adding support for restricting category read access based on user groups --- public/src/forum/admin/categories.js | 37 +++++++++++++++++- public/templates/admin/categories.tpl | 8 +++- src/categoryTools.js | 54 ++++++++++++++++++++++++++- src/groups.js | 36 ++++++++++++++++++ src/routes/api.js | 30 +++++++++++---- src/websockets.js | 31 +++++++++++++++ 6 files changed, 186 insertions(+), 10 deletions(-) diff --git a/public/src/forum/admin/categories.js b/public/src/forum/admin/categories.js index 0874ffa878..73eafde5df 100644 --- a/public/src/forum/admin/categories.js +++ b/public/src/forum/admin/categories.js @@ -211,6 +211,7 @@ define(['uploader'], function(uploader) { var modal = $('#category-permissions-modal'), searchEl = modal.find('#permission-search'), resultsEl = modal.find('.search-results'), + groupsResultsEl = modal.find('.groups-results'), searchDelay; searchEl.off().on('keyup', function() { @@ -263,6 +264,40 @@ define(['uploader'], function(uploader) { searchEl.keyup(); }); + // User Groups and privileges + socket.emit('api:admin.categories.groupsearch', cid, function(err, results) { + var groupsFrag = document.createDocumentFragment(), + liEl = document.createElement('li'); + var numResults = results.length, + resultObj; + + for(var x=0;x' + + '' + + '' + + '' + + '' + + ' '+resultObj.name; + + groupsFrag.appendChild(liEl.cloneNode(true)); + } + + groupsResultsEl.html(groupsFrag); + }); + + groupsResultsEl.off().on('click', '[data-gpriv]', function(e) { + var btnEl = $(this), + gid = btnEl.parents('li[data-gid]').attr('data-gid'), + privilege = this.getAttribute('data-gpriv'); + e.preventDefault(); + socket.emit('api:admin.categories.setGroupPrivilege', cid, gid, privilege, !btnEl.hasClass('active'), function(err, privileges) { + btnEl.toggleClass('active', privileges[privilege]); + }); + }) + modal.modal(); }; @@ -312,4 +347,4 @@ define(['uploader'], function(uploader) { }; return Categories; -}); \ No newline at end of file +}); diff --git a/public/templates/admin/categories.tpl b/public/templates/admin/categories.tpl index 25b85daf68..9651b387a9 100644 --- a/public/templates/admin/categories.tpl +++ b/public/templates/admin/categories.tpl @@ -154,6 +154,12 @@ +
+
+ +
+
+ @@ -162,4 +168,4 @@ - \ No newline at end of file + diff --git a/src/categoryTools.js b/src/categoryTools.js index ccbce0f4fd..e1f79e8c07 100644 --- a/src/categoryTools.js +++ b/src/categoryTools.js @@ -65,4 +65,56 @@ CategoryTools.privileges = function(cid, uid, callback) { }); }; -module.exports = CategoryTools; \ No newline at end of file +CategoryTools.groupPrivileges = function(cid, gid, callback) { + async.parallel({ + "+gr": function(next) { + var key = 'cid:' + cid + ':privileges:+gr'; + Groups.exists(key, function(err, exists) { + if (exists) { + async.parallel({ + isMember: function(next) { + Groups.isMemberByGroupName(gid, key, next); + }, + isEmpty: function(next) { + Groups.isEmptyByGroupName(key, next); + } + }, next); + } else { + next(null, { + isMember: false, + isEmpty: true + }); + } + }); + }, + "+gw": function(next) { + var key = 'cid:' + cid + ':privileges:+gw'; + Groups.exists(key, function(err, exists) { + if (exists) { + async.parallel({ + isMember: function(next) { + Groups.isMemberByGroupName(gid, key, next); + }, + isEmpty: function(next) { + Groups.isEmptyByGroupName(key, next); + } + }, next); + } else { + next(null, { + isMember: false, + isEmpty: true + }); + } + }); + } + }, function(err, privileges) { + callback(err, !privileges ? null : { + "+gr": privileges['+gr'].isMember, + "+gw": privileges['+gw'].isMember, + read: (privileges['+gr'].isMember || privileges['+gr'].isEmpty), + write: (privileges['+gw'].isMember || privileges['+gw'].isEmpty), + }); + }); +}; + +module.exports = CategoryTools; diff --git a/src/groups.js b/src/groups.js index 3d56520257..782cca7ff3 100644 --- a/src/groups.js +++ b/src/groups.js @@ -263,4 +263,40 @@ }); }; + Groups.getCategoryAccess = function(cid, uid, callback){ + var access = false; + // check user group read access level + async.series([function(callback){ + // get groups with read permission + db.getObjectField('group:gid', 'cid:' + cid + ':privileges:+gr', function(err, gid){ + // get the user groups that belong to this read group + db.getSetMembers('gid:' + gid + ':members', function (err, gids) { + // check if user belong to any of these user groups + var groups_check = new Array(); + gids.forEach(function(cgid){ + groups_check.push(function(callback){ + Groups.isMember(uid, cgid, function(err, isMember){ + if (isMember){ + access = true; + } + callback(null, gids); + }) + }); + }); + // do a series check. We want to make sure we check all the groups before determining if the user + // has access or not. + async.series(groups_check, function(err, results){ + callback(null, results); + }); + }); + }); + + }], + function(err, results){ + // if the read group is empty we will asume that read access has been granted to ALL + if (results[0].length == 0){ access = true; } + callback(false, access); + }); + }; + }(module.exports)); diff --git a/src/routes/api.js b/src/routes/api.js index 45e9514ce8..cb28e0bffb 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -4,6 +4,7 @@ var path = require('path'), db = require('../database'), user = require('../user'), + groups = require('../groups'), auth = require('./authentication'), topics = require('../topics'), posts = require('../posts'), @@ -121,7 +122,15 @@ var path = require('path'), if (parseInt(data.deleted, 10) === 1 && parseInt(data.expose_tools, 10) === 0) { return res.json(404, {}); } - res.json(data); + // get the category this post belongs to and check category access + var cid = data.category_slug.split("/")[0]; + groups.getCategoryAccess(cid, uid, function(err, access){ + if (access){ + res.json(data); + } else { + res.send(403); + } + }) } else next(); }); }); @@ -132,13 +141,20 @@ var path = require('path'), // Category Whitelisting categoryTools.privileges(req.params.id, uid, function(err, privileges) { if (!err && privileges.read) { - categories.getCategoryById(req.params.id, uid, function (err, data) { - if (!err && data && parseInt(data.disabled, 10) === 0) { - res.json(data); + groups.getCategoryAccess(req.params.id, uid, function(err, access){ + if (access){ + categories.getCategoryById(req.params.id, uid, function (err, data) { + if (!err && data && parseInt(data.disabled, 10) === 0) { + res.json(data); + } else { + next(); + } + }, req.params.id, uid); } else { - next(); + res.send(403); } - }, req.params.id, uid); + + }); } else { res.send(403); } @@ -303,4 +319,4 @@ var path = require('path'), }); }); } -}(exports)); \ No newline at end of file +}(exports)); diff --git a/src/websockets.js b/src/websockets.js index 81a3bbe221..2000b34617 100644 --- a/src/websockets.js +++ b/src/websockets.js @@ -1084,6 +1084,37 @@ websockets.init = function(io) { }); }); + socket.on('api:admin.categories.setGroupPrivilege', function(cid, gid, privilege, set, callback) { + var cb = function(err) { + CategoryTools.groupPrivileges(cid, gid, callback); + }; + + if (set) { + groups.joinByGroupName('cid:' + cid + ':privileges:' + privilege, gid, cb); + } else { + groups.leaveByGroupName('cid:' + cid + ':privileges:' + privilege, gid, cb); + } + }); + + socket.on('api:admin.categories.groupsearch', function(cid, callback) { + groups.list({expand:false}, function(err, data){ + async.map(data, function(groupObj, next) { + CategoryTools.groupPrivileges(cid, groupObj.gid, function(err, privileges) { + if (!err) { + groupObj.privileges = privileges; + } else { + winston.error('[socket api:admin.categories.groupsearch] Could not retrieve permissions'); + } + + next(null, groupObj); + }); + }, function(err, data) { + if (!callback) socket.emit('api:admin.categories.groupsearch', data); + else callback(null, data); + }); + }); + }); + socket.on('api:admin.themes.getInstalled', function(callback) { meta.themes.get(function(err, themeArr) { callback(themeArr);