/** * @class node_modules.cradle_security * * @author Marcello Gesmundo * * # License * * Copyright (c) 2012-2013 Yoovant by Marcello Gesmundo. All rights reserved. * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of Yoovant nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * */ module.exports = function (config) { var utils = require('object_utils'); var util = require('util'); var couchDB = require('cradle'); // namespace var my = {}; // default prefix for userId var userPrefix = "org.couchdb.user:"; /** * Configuration */ my.config = { /** * @cfg {Boolean} [debug = false] Set true if you want trace running module */ debug: false, /** * @cfg {Object} [logger = console] The logger used in debug mode. It MUST have * log, error, warn, info, debug methods */ logger: console, // for best logging please use winston: npm install winston /** * @cfg {Array} adminRoles Default CouchDB admin roles */ adminRoles: [ "admins" ], /** * @cfg {Array} memberRoles Default CouchDB user roles */ memberRoles: [ "guests" ], /** * @cfg {String} adminUsername Default username for CouchDB administrator */ adminUsername: "admin", /** * @cfg {String} adminPassword Default password for CouchDB administrator */ adminPassword: "admin" }; config = config || {}; // merge new config with default config utils.merge(my.config, config); var debug = my.config.debug; var logger = my.config.logger; // empty function var emptyFn = function () {}; // deactivate debug mode if logger is not available if (!logger) { debug = false; } if (debug) { if (logger && 'function' !== typeof logger.debug) { // console does not have debug method logger.debug = logger.log; } } else { logger = { debug: emptyFn, log: emptyFn, warn: emptyFn, error: emptyFn, info: emptyFn }; } // default CouchDB _design/_aut document var doc_auth = function (newDoc, oldDoc, userCtx, secObj) { if (newDoc._deleted === true) { // allow deletes by admins and matching users // without checking the other fields if (userCtx.roles.indexOf('_admin') !== -1) { return; } throw ({forbidden: 'Only admins may delete other user docs.'}); } var is_server_or_database_admin = function (userCtx, secObj) { // see if the user is a server admin if (userCtx.roles.indexOf('_admin') !== -1) { return true; // a server admin } // see if the user is a database admin specified by name if (secObj && secObj.admins && secObj.admins.names) { if (secObj.admins.names.indexOf(userCtx.name) !== -1) { return true; // database admin } } // see if the user is a database admin specified by role if (secObj && secObj.admins && secObj.admins.roles) { var db_roles = secObj.admins.roles; var idx; for (idx = 0; idx < userCtx.roles.length; idx++) { var user_role = userCtx.roles[idx]; if (db_roles.indexOf(user_role) !== -1) { return true; // role matches! } } } return false; // default to no admin }; if (!is_server_or_database_admin(userCtx, secObj)) { throw ({ forbidden: 'You may only read documents.' }); } }; /** * Add new user * * @param {String} username Username for new user * @param {String} password Password for new user * @param {Array} roles Roles to assign to new user * @param {Function} callback Function called when the user was created * @return {callback(err, res)} The callback to execute as result * @param {Object} callback.err Error during operation * @param {Object} callback.res Response of the operation */ couchDB.Database.prototype.addUser = function (username, password, roles, callback) { var userDb = this.connection.database('_users'); // add user if it does not exists var realName = util.format('%s%s', userPrefix, username); userDb.get(realName, function (userErr, userDoc) { if (!userDoc) { userDb.save(realName, { name: username, password: password, roles: roles, type: 'user' }, function (err, res) { if (logger) { logger.info(res); if (err) { logger.error(util.format('Error during creation of %s user', username), err); } else { logger.info(util.format('User %s created', username)); } } return callback(err, res); }); } }); }; /** * Delete user * * @param {String} username Username of the to be deleted * @param {Function} callback Function called when the user was deleted * @return {callback(err, res)} The callback to execute as result * @param {Object} callback.err Error during operation * @param {Object} callback.res Response of the operation */ couchDB.Database.prototype.delUser = function (username, callback) { var userDb = this.connection.database('_users'); var realName = util.format('%s%s', userPrefix, username); userDb.remove(realName, function (err, res) { if (logger) { logger.info(res); if (err) { logger.error(util.format('Error during deletion of %s user', username), err); } else { logger.info(util.format('User %s deleted', username)); } } return callback(err, res); }); }; /** * Add security document for authorization * * @private * @param {Function} callback Function called when the document was created * @return {callback(err, res)} The callback to execute as result * @param {Object} callback.err Error during operation * @param {Object} callback.res Response of the operation */ couchDB.Database.prototype.addAuthorization = function (callback) { // add authorization var self = this; self.save('_design/_auth', { views: {}, validate_doc_update: doc_auth }, function (err, res) { if (logger) { logger.info(res); if (err) { logger.error(util.format('Error during security document creation on %s database', self.name), err); } else { logger.info(util.format('Security document created on %s database', self.name)); } } return callback(err, res); }); }; /** * Add roles to a database for admins and readers * * @param {Array} adminRoles Roles to assign to administrator of the database * @param {Array} memberRoles Roles to assign to members of the database * @param {Function} callback Function called when the roles was created * @return {callback(err, res)} The callback to execute as result * @param {Object} callback.err Error during operation * @param {Object} callback.res Response of the operation */ couchDB.Database.prototype.addRoles = function (adminRoles, memberRoles, callback) { // add admin user for newly database var self = this; self.save('_security', { admins: { roles: adminRoles }, readers: { roles: memberRoles } }, function (err, res) { if (logger) { logger.info(res); if (err) { logger.error(util.format('Error during roles creation on %s database', self.name), err); } else { logger.info(util.format('Roles created on %s database', self.name)); } } return callback(err, res); }); }; /** * Create a new database and add new user * * @param {String} username Username for new user * @param {String} password Password for new user * @param {Array} roles Roles to assign to new user * @param {Function} callback Function called when the user was created * @return {callback(err, res)} The callback to execute as result * @param {Object} callback.err Error during operation * @param {Object} callback.res Response of the operation */ couchDB.Database.prototype.createWithUser = function (username, password, roles, callback) { // create new database var self = this; var err, res, resDbCreated, totalCallbacks = 0, maxCallbacks = 3; function callbackManager(taskErr, taskRes) { err = taskErr || err; res = taskRes || err; totalCallbacks++; if (totalCallbacks === maxCallbacks) { if (logger) { logger.info(res); if (err) { logger.error(util.format('Error during %s database creation', self.name), err); } else { logger.info(util.format('Database %s created', self.name)); } } if (err) { callback(err, res); } else { callback(err, resDbCreated); } } } self.create(function (errDb, resDb) { if (logger) { logger.info(resDb); if (errDb) { logger.error(util.format('Error during %s database creation', self.name), errDb); } } if (errDb) { callback(errDb, resDb); } else { resDbCreated = resDb; totalCallbacks = 0; // add user to couchdb users self.addUser(username, password, roles, callbackManager); // add roles self.addRoles(my.config.adminRoles, my.config.memberRoles, callbackManager); // add authorization self.addAuthorization(callbackManager); } }); }; /** * @return {Object} Database returned */ return couchDB; };