diff --git a/src/groupdb.js b/src/groupdb.js index c5f8a1d7e..22fecffa5 100644 --- a/src/groupdb.js +++ b/src/groupdb.js @@ -14,6 +14,7 @@ exports = module.exports = { isMember: isMember, getGroups: getGroups, + setGroups: setGroups, _clear: clear }; @@ -115,6 +116,9 @@ function clear(callback) { } function getMembers(groupId, callback) { + assert.strictEqual(typeof groupId, 'string'); + assert.strictEqual(typeof callback, 'function'); + database.query('SELECT userId FROM groupMembers WHERE groupId=?', [ groupId ], function (error, result) { if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); // if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); // need to differentiate group with no members and invalid groupId @@ -124,7 +128,10 @@ function getMembers(groupId, callback) { } function getGroups(userId, callback) { - database.query('SELECT userId FROM groupMembers WHERE userId=?', [ userId ], function (error, result) { + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + database.query('SELECT groupId FROM groupMembers WHERE userId=? ORDER BY groupId', [ userId ], function (error, result) { if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); // if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); // need to differentiate group with no members and invalid groupId @@ -132,6 +139,25 @@ function getGroups(userId, callback) { }); } +function setGroups(userId, groupIds, callback) { + assert.strictEqual(typeof userId, 'string'); + assert(Array.isArray(groupIds)); + assert.strictEqual(typeof callback, 'function'); + + var queries = [ ]; + queries.push({ query: 'DELETE from groupMembers WHERE userId = ?', args: [ userId ] }); + groupIds.forEach(function (gid) { + queries.push({ query: 'INSERT INTO groupMembers (groupId, userId) VALUES (? , ?)', args: [ gid, userId ] }); + }); + + database.transaction(queries, function (error) { + if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, error.message)); + if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); + + callback(null); + }); +} + function addMember(groupId, userId, callback) { assert.strictEqual(typeof groupId, 'string'); assert.strictEqual(typeof userId, 'string'); diff --git a/src/groups.js b/src/groups.js index ac606ddb0..ff98185ea 100644 --- a/src/groups.js +++ b/src/groups.js @@ -17,6 +17,7 @@ exports = module.exports = { isMember: isMember, getGroups: getGroups, + setGroups: setGroups, ADMIN_GROUP_ID: 'admin' // see db migration code and groupdb._clear }; @@ -153,6 +154,19 @@ function getGroups(userId, callback) { }); } +function setGroups(userId, groupIds, callback) { + assert.strictEqual(typeof userId, 'string'); + assert(Array.isArray(groupIds)); + assert.strictEqual(typeof callback, 'function'); + + groupdb.setGroups(userId, groupIds, function (error, result) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND)); + if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error)); + + return callback(null, result); + }); +} + function addMember(groupId, userId, callback) { assert.strictEqual(typeof groupId, 'string'); assert.strictEqual(typeof userId, 'string'); diff --git a/src/routes/test/groups-test.js b/src/routes/test/groups-test.js index 416a97c4e..52bc80708 100644 --- a/src/routes/test/groups-test.js +++ b/src/routes/test/groups-test.js @@ -11,6 +11,7 @@ var appdb = require('../../appdb.js'), config = require('../../config.js'), database = require('../../database.js'), expect = require('expect.js'), + groups = require('../../groups.js'), superagent = require('superagent'), server = require('../../server.js'), settings = require('../../settings.js'), @@ -168,4 +169,43 @@ describe('Groups API', function () { }); }); }); + + describe('Set groups', function () { + before(function (done) { + async.series([ + groups.create.bind(null, 'group0'), + groups.create.bind(null, 'group1') + ], done); + }); + + it('cannot add user to invalid group', function (done) { + superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME + '/set_groups') + .query({ access_token: token }) + .send({ groupIds: [ 'admin', 'something' ]}) + .end(function (error, result) { + expect(result.statusCode).to.equal(404); + done(); + }); + }); + + it('can add user to valid group', function (done) { + superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME + '/set_groups') + .query({ access_token: token }) + .send({ groupIds: [ 'admin', 'group0', 'group1' ]}) + .end(function (error, result) { + expect(result.statusCode).to.equal(204); + done(); + }); + }); + + it('can remove last user from admin', function (done) { + superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME + '/set_groups') + .query({ access_token: token }) + .send({ groupIds: [ 'group0', 'group1' ]}) + .end(function (error, result) { + expect(result.statusCode).to.equal(403); // not allowed + done(); + }); + }); + }); }); diff --git a/src/routes/user.js b/src/routes/user.js index b05853ed4..be75e63bb 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -13,7 +13,8 @@ exports = module.exports = { remove: removeUser, verifyPassword: verifyPassword, requireAdmin: requireAdmin, - sendInvite: sendInvite + sendInvite: sendInvite, + setGroups: setGroups }; var assert = require('assert'), @@ -226,3 +227,18 @@ function sendInvite(req, res, next) { next(new HttpSuccess(200, {})); }); } + +function setGroups(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + assert.strictEqual(typeof req.params.userId, 'string'); + + if (!Array.isArray(req.body.groupIds)) return next(new HttpError(400, 'API call requires a groups array.')); + + user.setGroups(req.params.userId, req.body.groupIds, function (error) { + if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'One or more groups not found')); + if (error && error.reason === UserError.NOT_ALLOWED) return next(new HttpError(403, 'Last admin')); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(204)); + }); +} diff --git a/src/server.js b/src/server.js index 9ef9a5500..72f471cdb 100644 --- a/src/server.js +++ b/src/server.js @@ -107,6 +107,7 @@ function initializeExpressSync() { router.del ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.user.remove); router.post('/api/v1/users/:userId/password', usersScope, routes.user.changePassword); // changePassword verifies password router.post('/api/v1/users/:userId/admin', usersScope, routes.user.requireAdmin, routes.user.changeAdmin); + router.put('/api/v1/users/:userId/set_groups', usersScope, routes.user.requireAdmin, routes.user.setGroups); router.post('/api/v1/users/:userId/invite', usersScope, routes.user.requireAdmin, routes.user.sendInvite); // Group management diff --git a/src/test/groups-test.js b/src/test/groups-test.js index d1fab30d0..e67e4b03d 100644 --- a/src/test/groups-test.js +++ b/src/test/groups-test.js @@ -17,6 +17,22 @@ var async = require('async'), var GROUP0_NAME = 'administrators', GROUP0_ID = GROUP0_NAME; +var GROUP1_NAME = 'externs', + GROUP1_ID = GROUP1_NAME; + +var USER_0 = { + id: 'uuid213', + username: 'uuid213', + password: 'secret', + email: 'safe@me.com', + admin: false, + salt: 'morton', + createdAt: 'sometime back', + modifiedAt: 'now', + resetToken: hat(256), + displayName: '' +}; + function setup(done) { // ensure data/config/mount paths database.initialize(function (error) { @@ -55,6 +71,13 @@ describe('Groups', function () { }); }); + it('cannot create group - reserved', function (done) { + groups.create('users', function (error) { + expect(error.reason).to.be(GroupError.BAD_NAME); + done(); + }); + }); + it('can create valid group', function (done) { groups.create(GROUP0_NAME, function (error) { expect(error).to.be(null); @@ -100,19 +123,6 @@ describe('Groups', function () { }); describe('Group membership', function () { - var USER_0 = { - id: 'uuid213', - username: 'uuid213', - password: 'secret', - email: 'safe@me.com', - admin: false, - salt: 'morton', - createdAt: 'sometime back', - modifiedAt: 'now', - resetToken: hat(256), - displayName: '' - }; - before(function (done) { async.series([ setup, @@ -218,3 +228,42 @@ describe('Group membership', function () { }); }); }); + +describe('Set user groups', function () { + before(function (done) { + async.series([ + setup, + groups.create.bind(null, GROUP0_NAME), + groups.create.bind(null, GROUP1_NAME), + userdb.add.bind(null, USER_0.id, USER_0) + ], done); + }); + after(cleanup); + + it('can set user to single group', function (done) { + groups.setGroups(USER_0.id, [ GROUP0_ID ], function (error) { + expect(error).to.be(null); + + groups.getGroups(USER_0.id, function (error, groupIds) { + expect(error).to.be(null); + expect(groupIds.length).to.be(1); + expect(groupIds[0]).to.be(GROUP0_ID); + done(); + }); + }); + }); + + it('can set user to multiple groups', function (done) { + groups.setGroups(USER_0.id, [ GROUP0_ID, GROUP1_ID ], function (error) { + expect(error).to.be(null); + + groups.getGroups(USER_0.id, function (error, groupIds) { + expect(error).to.be(null); + expect(groupIds.length).to.be(2); + expect(groupIds[0]).to.be(GROUP0_ID); + expect(groupIds[1]).to.be(GROUP1_ID); + done(); + }); + }); + }); +}); diff --git a/src/user.js b/src/user.js index 86a633c7d..e7459a5a3 100644 --- a/src/user.js +++ b/src/user.js @@ -20,7 +20,8 @@ exports = module.exports = { update: updateUser, createOwner: createOwner, getOwner: getOwner, - sendInvite: sendInvite + sendInvite: sendInvite, + setGroups: setGroups }; var assert = require('assert'), @@ -28,6 +29,7 @@ var assert = require('assert'), crypto = require('crypto'), DatabaseError = require('./databaseerror.js'), groups = require('./groups.js'), + GroupError = groups.GroupError, hat = require('hat'), mailer = require('./mailer.js'), tokendb = require('./tokendb.js'), @@ -314,6 +316,28 @@ function changeAdmin(username, admin, callback) { }); } +function setGroups(userId, groupIds, callback) { + assert.strictEqual(typeof userId, 'string'); + assert(Array.isArray(groupIds)); + assert.strictEqual(typeof callback, 'function'); + + userdb.getAllAdmins(function (error, result) { + if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + + // protect from a system where there is no admin left + if (result.length <= 1 && result[0].id === userId && groupIds.indexOf(groups.ADMIN_GROUP_ID) === -1) { + return callback(new UserError(UserError.NOT_ALLOWED, 'Only admin')); + } + + groups.setGroups(userId, groupIds, function (error) { + if (error && error.reason === GroupError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, 'One or more groups not found')); + if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + + callback(); + }); + }); +} + function getAllAdmins(callback) { assert.strictEqual(typeof callback, 'function');