From 6f774bc6a567211a8a7157458c249d89a145c3ab Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Wed, 11 Apr 2012 18:07:19 +0200 Subject: [PATCH 1/7] Separated out the code for the plugin manager --- src/static/js/admin/plugins.js | 87 ++++++++++++++++++++++++++++++ src/templates/admin/plugins.html | 90 +------------------------------- 2 files changed, 88 insertions(+), 89 deletions(-) create mode 100644 src/static/js/admin/plugins.js diff --git a/src/static/js/admin/plugins.js b/src/static/js/admin/plugins.js new file mode 100644 index 00000000..4ed8a6c6 --- /dev/null +++ b/src/static/js/admin/plugins.js @@ -0,0 +1,87 @@ +$(document).ready(function () { + var socket = io.connect().of("/pluginfw/installer"); + + var doUpdate = false; + + function updateHandlers() { + $("#progress.dialog .close").unbind('click').click(function () { + $("#progress.dialog").hide(); + }); + + $("#do-search").unbind('click').click(function () { + if ($("#search-query")[0].value != "") + socket.emit("search", $("#search-query")[0].value); + }); + + $(".do-install").unbind('click').click(function (e) { + var row = $(e.target).closest("tr"); + doUpdate = true; + socket.emit("install", row.find(".name").html()); + }); + + $(".do-uninstall").unbind('click').click(function (e) { + var row = $(e.target).closest("tr"); + doUpdate = true; + socket.emit("uninstall", row.find(".name").html()); + }); + } + + updateHandlers(); + + socket.on('progress', function (data) { + $("#progress.dialog .close").hide(); + $("#progress.dialog").show(); + var message = "Unknown status"; + if (data.message) { + message = "" + data.message.toString() + ""; + } + if (data.error) { + message = "" + data.error.toString() + ""; + } + $("#progress.dialog .message").html(message); + $("#progress.dialog .history").append("
" + message + "
"); + + if (data.progress >= 1) { + if (data.error) { + $("#progress.dialog .close").show(); + } else { + if (doUpdate) { + doUpdate = false; + socket.emit("load"); + } + $("#progress.dialog").hide(); + } + } + }); + + socket.on('search-result', function (data) { + $("#search-results *").remove(); + for (plugin_name in data.results) { + var plugin = data.results[plugin_name]; + var row = $("#search-result-template").clone(); + + for (attr in plugin) { + row.find("." + attr).html(plugin[attr]); + } + $("#search-results").append(row); + } + updateHandlers(); + }); + + socket.on('installed-results', function (data) { + $("#installed-plugins *").remove(); + for (plugin_name in data.results) { + var plugin = data.results[plugin_name]; + var row = $("#installed-plugin-template").clone(); + + for (attr in plugin.package) { + row.find("." + attr).html(plugin.package[attr]); + } + $("#installed-plugins").append(row); + } + updateHandlers(); + }); + + socket.emit("load"); + +}); diff --git a/src/templates/admin/plugins.html b/src/templates/admin/plugins.html index 6f38d048..82bad555 100644 --- a/src/templates/admin/plugins.html +++ b/src/templates/admin/plugins.html @@ -4,95 +4,7 @@ - +
From 7ab7ee9f5ee1caedbf1ca5ac10934a269ecde8f6 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Tue, 17 Apr 2012 22:18:43 +0200 Subject: [PATCH 2/7] Plugin admin fixes --- src/static/js/admin/plugins.js | 27 +++++++++++--- src/static/js/pluginfw/installer.js | 15 +++++--- src/templates/admin/plugins.html | 56 ++++++++++++++++------------- 3 files changed, 64 insertions(+), 34 deletions(-) diff --git a/src/static/js/admin/plugins.js b/src/static/js/admin/plugins.js index 4ed8a6c6..7f6c17b0 100644 --- a/src/static/js/admin/plugins.js +++ b/src/static/js/admin/plugins.js @@ -9,8 +9,10 @@ $(document).ready(function () { }); $("#do-search").unbind('click').click(function () { - if ($("#search-query")[0].value != "") - socket.emit("search", $("#search-query")[0].value); + socket.emit("search", { + pattern: $("#search-query")[0].value, + offset: $('#search-results').data('offset') || 0, + limit: 4}); }); $(".do-install").unbind('click').click(function (e) { @@ -29,8 +31,13 @@ $(document).ready(function () { updateHandlers(); socket.on('progress', function (data) { + if ($('#progress.dialog').data('progress') > data.progress) return; + $("#progress.dialog .close").hide(); $("#progress.dialog").show(); + + $('#progress.dialog').data('progress', data.progress); + var message = "Unknown status"; if (data.message) { message = "" + data.message.toString() + ""; @@ -55,16 +62,26 @@ $(document).ready(function () { }); socket.on('search-result', function (data) { - $("#search-results *").remove(); + var widget=$(".search-results"); + + widget.data('query', data.query); + widget.data('total', data.total); + + widget.find('.offset').html(data.qyery.offset); + widget.find('.limit').html(data.qyery.offset + data.qyery.offset.limit); + widget.find('.total').html(data.total); + + widget.find(".results *").remove(); for (plugin_name in data.results) { var plugin = data.results[plugin_name]; - var row = $("#search-result-template").clone(); + var row = widget.find(".template tr").clone(); for (attr in plugin) { row.find("." + attr).html(plugin[attr]); } - $("#search-results").append(row); + widget.find(".results").append(row); } + updateHandlers(); }); diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index 127a95aa..09bde920 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -55,7 +55,7 @@ exports.install = function(plugin_name, cb) { ); }; -exports.search = function(pattern, cb) { +exports.search = function(query, cb) { withNpm( function (cb) { registry.get( @@ -63,11 +63,18 @@ exports.search = function(pattern, cb) { function (er, data) { if (er) return cb(er); var res = {}; + var i = 0; for (key in data) { - if (key.indexOf(plugins.prefix) == 0 && key.indexOf(pattern) != -1) - res[key] = data[key]; + if (/* && key.indexOf(plugins.prefix) == 0 */ + key.indexOf(query.pattern) != -1) { + i++; + if (i > query.offset + && i <= query.offset + query.limit) { + res[key] = data[key]; + } + } } - cb(null, {results:res}); + cb(null, {results:res, query: query, total:i}); } ); }, diff --git a/src/templates/admin/plugins.html b/src/templates/admin/plugins.html index 82bad555..c1cc645a 100644 --- a/src/templates/admin/plugins.html +++ b/src/templates/admin/plugins.html @@ -40,31 +40,37 @@ -

Search for plugins to install

-
- - -
- - - - - - - - - - - - - - - - - -
NameDescription
- -
+
+

Search for plugins to install

+
+ + +
+ + + + + + + + + + + + + + + + + +
NameDescription
+ +
+ + .. of . + +
+

From d6f476312d7380e29d9995756c32a2b33134ab46 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Wed, 18 Apr 2012 10:16:41 +0200 Subject: [PATCH 3/7] Spelling bugfixes --- src/static/js/admin/plugins.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/static/js/admin/plugins.js b/src/static/js/admin/plugins.js index 7f6c17b0..54454c2b 100644 --- a/src/static/js/admin/plugins.js +++ b/src/static/js/admin/plugins.js @@ -67,8 +67,8 @@ $(document).ready(function () { widget.data('query', data.query); widget.data('total', data.total); - widget.find('.offset').html(data.qyery.offset); - widget.find('.limit').html(data.qyery.offset + data.qyery.offset.limit); + widget.find('.offset').html(data.query.offset); + widget.find('.limit').html(data.query.offset + data.query.limit); widget.find('.total').html(data.total); widget.find(".results *").remove(); From 4c1d94343fe1b1a8ff02d7cfaef9f9506676d072 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Wed, 18 Apr 2012 13:43:34 +0200 Subject: [PATCH 4/7] Better plugin admin interface --- src/node/hooks/express/adminplugins.js | 2 +- src/static/js/admin/plugins.js | 38 ++++++++++++++++++++++---- src/static/js/pluginfw/installer.js | 25 +++++++++++++---- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index fa7e7077..1d1320ab 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -27,7 +27,7 @@ exports.socketio = function (hook_name, args, cb) { socket.on("search", function (query) { socket.emit("progress", {progress:0, message:'Fetching results...'}); - installer.search(query, function (progress) { + installer.search(query, true, function (progress) { if (progress.results) socket.emit("search-result", progress); socket.emit("progress", progress); diff --git a/src/static/js/admin/plugins.js b/src/static/js/admin/plugins.js index 54454c2b..6ae085c2 100644 --- a/src/static/js/admin/plugins.js +++ b/src/static/js/admin/plugins.js @@ -1,18 +1,28 @@ $(document).ready(function () { var socket = io.connect().of("/pluginfw/installer"); + $('.search-results').data('query', { + pattern: '', + offset: 0, + limit: 4, + }); + var doUpdate = false; + var search = function () { + socket.emit("search", $('.search-results').data('query')); + } + function updateHandlers() { $("#progress.dialog .close").unbind('click').click(function () { $("#progress.dialog").hide(); }); $("#do-search").unbind('click').click(function () { - socket.emit("search", { - pattern: $("#search-query")[0].value, - offset: $('#search-results').data('offset') || 0, - limit: 4}); + var query = $('.search-results').data('query'); + query.pattern = $("#search-query")[0].value; + query.offset = 0; + search(); }); $(".do-install").unbind('click').click(function (e) { @@ -26,12 +36,29 @@ $(document).ready(function () { doUpdate = true; socket.emit("uninstall", row.find(".name").html()); }); + + $(".do-prev-page").unbind('click').click(function (e) { + var query = $('.search-results').data('query'); + query.offset -= query.limit; + if (query.offset < 0) { + query.offset = 0; + } + search(); + }); + $(".do-next-page").unbind('click').click(function (e) { + var query = $('.search-results').data('query'); + var total = $('.search-results').data('total'); + if (query.offset + query.limit < total) { + query.offset += query.limit; + } + search(); + }); } updateHandlers(); socket.on('progress', function (data) { - if ($('#progress.dialog').data('progress') > data.progress) return; + if (data.progress > 0 && $('#progress.dialog').data('progress') > data.progress) return; $("#progress.dialog .close").hide(); $("#progress.dialog").show(); @@ -100,5 +127,6 @@ $(document).ready(function () { }); socket.emit("load"); + search(); }); diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index 09bde920..1bb8db9e 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -55,18 +55,33 @@ exports.install = function(plugin_name, cb) { ); }; -exports.search = function(query, cb) { +exports.searchCache = null; + +exports.search = function(query, cache, cb) { withNpm( function (cb) { - registry.get( - "/-/all", null, 600, false, true, + var getData = function (cb) { + if (cache && exports.searchCache) { + cb(null, exports.searchCache); + } else { + registry.get( + "/-/all", null, 600, false, true, + function (er, data) { + if (er) return cb(er); + exports.searchCache = data; + cb(er, data); + } + ); + } + } + getData( function (er, data) { if (er) return cb(er); var res = {}; var i = 0; for (key in data) { - if (/* && key.indexOf(plugins.prefix) == 0 */ - key.indexOf(query.pattern) != -1) { + if ( key.indexOf(plugins.prefix) == 0 + && key.indexOf(query.pattern) != -1) { i++; if (i > query.offset && i <= query.offset + query.limit) { From ac36a99a7226e1092c2e7e28c9b6e32d8f82e6fb Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Thu, 19 Apr 2012 14:25:12 +0200 Subject: [PATCH 5/7] More general basic auth --- settings.json.template | 25 ++++-- src/node/hooks/express/adminplugins.js | 2 + src/node/hooks/express/socketio.js | 18 +++- src/node/hooks/express/webaccess.js | 110 +++++++++++++++++-------- src/node/utils/Settings.js | 15 ++-- src/package.json | 1 + 6 files changed, 123 insertions(+), 48 deletions(-) diff --git a/settings.json.template b/settings.json.template index a664ea8e..d8f67e76 100644 --- a/settings.json.template +++ b/settings.json.template @@ -46,12 +46,27 @@ /* This is the path to the Abiword executable. Setting it to null, disables abiword. Abiword is needed to enable the import/export of pads*/ "abiword" : null, - - /* This setting is used if you need http basic auth */ - // "httpAuth" : "user:pass", + + /* This setting is used if you require authentication of all users. + Note: /admin always requires authentication. */ + "requireAuthentication": false, - /* This setting is used for http basic auth for admin pages. If not set, the admin page won't be accessible from web*/ - // "adminHttpAuth" : "user:pass", + /* Require authorization by a module, or a user with is_admin set, + see below. Access to /admin allways requires either, regardless + of this setting. */ + "requireAuthorization": false, + + /* Users for basic authentication. is_admin = true gives access to /admin */ + "users": { + "admin": { + "password": "changeme", + "is_admin": true + }, + "user": { + "password": "changeme", + "is_admin": false + } + }, /* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */ "loglevel": "INFO" diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index 1d1320ab..6cc80cf2 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -21,6 +21,8 @@ exports.expressCreateServer = function (hook_name, args, cb) { exports.socketio = function (hook_name, args, cb) { var io = args.io.of("/pluginfw/installer"); io.on('connection', function (socket) { + if (!socket.handshake.session.user.is_admin) return; + socket.on("load", function (query) { socket.emit("installed-results", {results: plugins.plugins}); }); diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index e040f7ac..6774b653 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -7,11 +7,27 @@ var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var padMessageHandler = require("../../handler/PadMessageHandler"); var timesliderMessageHandler = require("../../handler/TimesliderMessageHandler"); - +var connect = require('connect'); + exports.expressCreateServer = function (hook_name, args, cb) { //init socket.io and redirect all requests to the MessageHandler var io = socketio.listen(args.app); + /* Require an express session cookie to be present, and load the + * session. See http://www.danielbaulig.de/socket-ioexpress for more + * info */ + io.set('authorization', function (data, accept) { + if (!data.headers.cookie) return accept('No session cookie transmitted.', false); + data.cookie = connect.utils.parseCookie(data.headers.cookie); + data.sessionID = data.cookie.express_sid; + args.app.sessionStore.get(data.sessionID, function (err, session) { + if (err || !session) return accept('Bad session / session has expired', false); + data.session = new connect.middleware.session.Session(data, session); + accept(null, true); + }); + }); + + //this is only a workaround to ensure it works with all browers behind a proxy //we should remove this when the new socket.io version is more stable io.set('transports', ['xhr-polling']); diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 48b5edae..499451d8 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -2,55 +2,99 @@ var express = require('express'); var log4js = require('log4js'); var httpLogger = log4js.getLogger("http"); var settings = require('../../utils/Settings'); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); //checks for basic http auth exports.basicAuth = function (req, res, next) { - - // When handling HTTP-Auth, an undefined password will lead to no authorization at all - var pass = settings.httpAuth || ''; - - if (req.path.indexOf('/admin') == 0) { - var pass = settings.adminHttpAuth; - + var authorize = function (cb) { + // Do not require auth for static paths...this could be a bit brittle + if (req.path.match(/^\/(static|javascripts|pluginfw)/)) return cb(true); + + if (req.path.indexOf('/admin') != 0) { + if (!settings.requireAuthentication) return cb(true); + if (!settings.requireAuthorization && req.session && req.session.user) return cb(true); + } + + if (req.session && req.session.user && req.session.user.is_admin) return cb(true); + + // hooks.aCallFirst("authorize", {resource: req.path, req: req}, cb); + cb(false); } - - // Just pass if password is an empty string - if (pass === '') { - return next(); + + var authenticate = function (cb) { + // If auth headers are present use them to authenticate... + if (req.headers.authorization && req.headers.authorization.search('Basic ') === 0) { + var userpass = new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString().split(":") + var username = userpass[0]; + var password = userpass[1]; + + if (settings.users[username] != undefined && settings.users[username].password == password) { + settings.users[username].username = username; + req.session.user = settings.users[username]; + return cb(true); + } + // return hooks.aCallFirst("authenticate", {req: req, username: username, password: password}, cb); + } + // hooks.aCallFirst("authenticate", {req: req}, cb); + cb(false); } - - - // If a password has been set and auth headers are present... - if (pass && req.headers.authorization && req.headers.authorization.search('Basic ') === 0) { - // ...check login and password - if (new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString() === pass) { - return next(); + + + var failure = function () { + /* Authentication OR authorization failed. Return Auth required + * Headers, delayed for 1 second, if authentication failed. */ + res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); + if (req.headers.authorization) { + setTimeout(function () { + res.send('Authentication required', 401); + }, 1000); + } else { + res.send('Authentication required', 401); } } - // Do not require auth for static paths...this could be a bit brittle - else if (req.path.match(/^\/(static|javascripts|pluginfw)/)) { - return next(); - } - // Otherwise return Auth required Headers, delayed for 1 second, if auth failed. - res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); - if (req.headers.authorization) { - setTimeout(function () { - res.send('Authentication required', 401); - }, 1000); - } else { - res.send('Authentication required', 401); - } + /* This is the actual authentication/authorization hoop. It is done in four steps: + + 1) Try to just access the thing + 2) If not allowed using whatever creds are in the current session already, try to authenticate + 3) If authentication using already supplied credentials succeeds, try to access the thing again + 4) If all els fails, give the user a 401 to request new credentials + + Note that the process could stop already in step 3 with a redirect to login page. + + */ + + authorize(function (ok) { + if (ok) return next(); + authenticate(function (ok) { + if (!ok) return failure(); + authorize(function (ok) { + if (ok) return next(); + failure(); + }); + }); + }); } exports.expressConfigure = function (hook_name, args, cb) { - args.app.use(exports.basicAuth); - // If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158. // Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway. if (!(settings.loglevel === "WARN" || settings.loglevel == "ERROR")) args.app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'})); args.app.use(express.cookieParser()); + + /* Do not let express create the session, so that we can retain a + * reference to it for socket.io to use. Also, set the key (cookie + * name) to a javascript identifier compatible string. Makes code + * handling it cleaner :) */ + + args.app.sessionStore = new express.session.MemoryStore(); + args.app.use(express.session({store: args.app.sessionStore, + key: 'express_sid', + secret: apikey = randomString(32)})); + + args.app.use(exports.basicAuth); } diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 12fcc55c..cb6a6403 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -80,15 +80,12 @@ exports.abiword = null; */ exports.loglevel = "INFO"; -/** - * Http basic auth, with "user:password" format - */ -exports.httpAuth = null; - -/** - * Http basic auth, with "user:password" format - */ -exports.adminHttpAuth = null; +/* This setting is used if you need authentication and/or + * authorization. Note: /admin always requires authentication, and + * either authorization by a module, or a user with is_admin set */ +exports.requireAuthentication = false; +exports.requireAuthorization = false; +exports.users = {}; //checks if abiword is avaiable exports.abiwordAvailable = function() diff --git a/src/package.json b/src/package.json index 83441da0..eda385b2 100644 --- a/src/package.json +++ b/src/package.json @@ -17,6 +17,7 @@ "ueberDB" : "0.1.7", "async" : "0.1.18", "express" : "2.5.8", + "connect" : "1.8.7", "clean-css" : "0.3.2", "uglify-js" : "1.2.5", "formidable" : "1.0.9", From 7b39da2d69e759e0f75cc19adb92e130583c68d0 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Thu, 19 Apr 2012 16:03:42 +0200 Subject: [PATCH 6/7] Bugfix for callFirst to actually call call hooks until it finds one that returns non-empty, which is actually usefull, as opposed to just calling one hook. --- src/static/js/pluginfw/hooks.js | 64 +++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index c4cd5aeb..49e46c60 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -4,27 +4,63 @@ var _; /* FIXME: Ugly hack, in the future, use same code for server & client */ if (plugins.isClient) { var async = require("ep_etherpad-lite/static/js/pluginfw/async"); - _ = require("ep_etherpad-lite/static/js/underscore"); + var _ = require("ep_etherpad-lite/static/js/underscore"); } else { var async = require("async"); - _ = require("underscore"); + var _ = require("underscore"); } exports.bubbleExceptions = true var hookCallWrapper = function (hook, hook_name, args, cb) { if (cb === undefined) cb = function (x) { return x; }; + + // Normalize output to list for both sync and async cases + var normalize = function(x) { + if (x == undefined) return []; + return x; + } + var normalizedhook = function () { + return normalize(hook.hook_fn(hook_name, args, function (x) { + return cb(normalize(x)); + })); + } + if (exports.bubbleExceptions) { - return hook.hook_fn(hook_name, args, cb); + return normalizedhook(); } else { try { - return hook.hook_fn(hook_name, args, cb); + return normalizedhook(); } catch (ex) { console.error([hook_name, hook.part.full_name, ex.stack || ex]); } } } +exports.syncMapFirst = function (lst, fn) { + var i; + var result; + for (i = 0; i < lst.length; i++) { + result = fn(lst[i]) + if (result.length) return result; + } + return undefined; +} + +exports.mapFirst = function (lst, fn, cb) { + var i = 0; + + next = function () { + if (i >= lst.length) return cb(undefined); + fn(lst[i++], function (err, result) { + if (err) return cb(err); + if (result.length) return cb(null, result); + next(); + }); + } + next(); +} + /* Don't use Array.concat as it flatterns arrays within the array */ exports.flatten = function (lst) { @@ -44,9 +80,9 @@ exports.flatten = function (lst) { exports.callAll = function (hook_name, args) { if (!args) args = {}; if (plugins.hooks[hook_name] === undefined) return []; - return exports.flatten(_.map(plugins.hooks[hook_name], function (hook) { + return _.flatten(_.map(plugins.hooks[hook_name], function (hook) { return hookCallWrapper(hook, hook_name, args); - })); + }), true); } exports.aCallAll = function (hook_name, args, cb) { @@ -59,7 +95,7 @@ exports.aCallAll = function (hook_name, args, cb) { hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); }); }, function (err, res) { - cb(null, exports.flatten(res)); + cb(null, _.flatten(res, true)); } ); } @@ -67,14 +103,22 @@ exports.aCallAll = function (hook_name, args, cb) { exports.callFirst = function (hook_name, args) { if (!args) args = {}; if (plugins.hooks[hook_name][0] === undefined) return []; - return exports.flatten(hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args)); + return exports.syncMapFirst(plugins.hooks[hook_name], function (hook) { + return hookCallWrapper(hook, hook_name, args); + }); } exports.aCallFirst = function (hook_name, args, cb) { if (!args) args = {}; if (!cb) cb = function () {}; - if (plugins.hooks[hook_name][0] === undefined) return cb(null, []); - hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args, function (res) { cb(null, exports.flatten(res)); }); + if (plugins.hooks[hook_name] === undefined) return cb(null, []); + exports.mapFirst( + plugins.hooks[hook_name], + function (hook, cb) { + hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); }); + }, + cb + ); } exports.callAllStr = function(hook_name, args, sep, pre, post) { From ecac40d062747e4668dd942443b361077fd55f74 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Thu, 19 Apr 2012 16:04:03 +0200 Subject: [PATCH 7/7] Changed the authentication mechanism to support hooks --- settings.json.template | 13 +++++---- src/node/hooks/express/adminplugins.js | 2 +- src/node/hooks/express/webaccess.js | 39 ++++++++++++++++---------- src/node/server.js | 5 ++-- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/settings.json.template b/settings.json.template index d8f67e76..f89fcd8e 100644 --- a/settings.json.template +++ b/settings.json.template @@ -51,22 +51,23 @@ Note: /admin always requires authentication. */ "requireAuthentication": false, - /* Require authorization by a module, or a user with is_admin set, - see below. Access to /admin allways requires either, regardless - of this setting. */ + /* Require authorization by a module, or a user with is_admin set, see below. */ "requireAuthorization": false, - /* Users for basic authentication. is_admin = true gives access to /admin */ + /* Users for basic authentication. is_admin = true gives access to /admin. + If you do not uncomment this, /admin will not be available! */ + /* "users": { "admin": { - "password": "changeme", + "password": "changeme1", "is_admin": true }, "user": { - "password": "changeme", + "password": "changeme1", "is_admin": false } }, + */ /* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */ "loglevel": "INFO" diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index 6cc80cf2..7b21206c 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -21,7 +21,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { exports.socketio = function (hook_name, args, cb) { var io = args.io.of("/pluginfw/installer"); io.on('connection', function (socket) { - if (!socket.handshake.session.user.is_admin) return; + if (!socket.handshake.session.user || !socket.handshake.session.user.is_admin) return; socket.on("load", function (query) { socket.emit("installed-results", {results: plugins.plugins}); diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 499451d8..028d8ab1 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -8,7 +8,13 @@ var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); //checks for basic http auth exports.basicAuth = function (req, res, next) { - var authorize = function (cb) { + var hookResultMangle = function (cb) { + return function (err, data) { + return cb(!err && data.length && data[0]); + } + } + + var authorize = function (cb) { // Do not require auth for static paths...this could be a bit brittle if (req.path.match(/^\/(static|javascripts|pluginfw)/)) return cb(true); @@ -19,8 +25,7 @@ exports.basicAuth = function (req, res, next) { if (req.session && req.session.user && req.session.user.is_admin) return cb(true); - // hooks.aCallFirst("authorize", {resource: req.path, req: req}, cb); - cb(false); + hooks.aCallFirst("authorize", {req: req, res:res, next:next, resource: req.path}, hookResultMangle(cb)); } var authenticate = function (cb) { @@ -35,24 +40,28 @@ exports.basicAuth = function (req, res, next) { req.session.user = settings.users[username]; return cb(true); } - // return hooks.aCallFirst("authenticate", {req: req, username: username, password: password}, cb); + return hooks.aCallFirst("authenticate", {req: req, res:res, next:next, username: username, password: password}, hookResultMangle(cb)); } - // hooks.aCallFirst("authenticate", {req: req}, cb); - cb(false); + hooks.aCallFirst("authenticate", {req: req, res:res, next:next}, hookResultMangle(cb)); } + /* Authentication OR authorization failed. */ var failure = function () { - /* Authentication OR authorization failed. Return Auth required - * Headers, delayed for 1 second, if authentication failed. */ - res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); - if (req.headers.authorization) { - setTimeout(function () { + return hooks.aCallFirst("authFailure", {req: req, res:res, next:next}, hookResultMangle(function (ok) { + if (ok) return; + /* No plugin handler for invalid auth. Return Auth required + * Headers, delayed for 1 second, if authentication failed + * before. */ + res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); + if (req.headers.authorization) { + setTimeout(function () { + res.send('Authentication required', 401); + }, 1000); + } else { res.send('Authentication required', 401); - }, 1000); - } else { - res.send('Authentication required', 401); - } + } + })); } diff --git a/src/node/server.js b/src/node/server.js index 6b443edb..9d2c52e4 100644 --- a/src/node/server.js +++ b/src/node/server.js @@ -30,6 +30,7 @@ var path = require('path'); var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var npm = require("npm/lib/npm.js"); +var _ = require("underscore"); //try to get the git version var version = ""; @@ -88,11 +89,11 @@ async.waterfall([ //let the server listen app.listen(settings.port, settings.ip); console.log("Server is listening at " + settings.ip + ":" + settings.port); - if(settings.adminHttpAuth){ + if(!_.isEmpty(settings.users)){ console.log("Plugin admin page listening at " + settings.ip + ":" + settings.port + "/admin/plugins"); } else{ - console.log("Admin username and password not set in settings.json. To access admin please uncomment and edit adminHttpAuth in settings.json"); + console.log("Admin username and password not set in settings.json. To access admin please uncomment and edit 'users' in settings.json"); } callback(null); }