'use strict'; exports = module.exports = { add, remove, get, getByName, update, getWithMembers, list, listWithMembers, getMembers, addMember, setMembers, removeMember, isMember, setMembership, getMembership, }; const assert = require('assert'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), database = require('./database.js'), safe = require('safetydance'), uuid = require('uuid'); const GROUPS_FIELDS = [ 'id', 'name', 'source' ].join(','); // keep this in sync with validateUsername function validateGroupname(name) { assert.strictEqual(typeof name, 'string'); if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'name must be atleast 1 char'); if (name.length >= 200) return new BoxError(BoxError.BAD_FIELD, 'name too long'); if (constants.RESERVED_NAMES.indexOf(name) !== -1) return new BoxError(BoxError.BAD_FIELD, 'name is reserved'); // need to consider valid LDAP characters here (e.g '+' is reserved) if (/[^a-zA-Z0-9.-]/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'name can only contain alphanumerals, hyphen and dot'); return null; } function validateGroupSource(source) { assert.strictEqual(typeof source, 'string'); if (source !== '' && source !== 'ldap') return new BoxError(BoxError.BAD_FIELD, 'source must be "" or "ldap"'); return null; } async function add(group) { assert.strictEqual(typeof group, 'object'); let { name, source } = group; name = name.toLowerCase(); // we store names in lowercase source = source || ''; let error = validateGroupname(name); if (error) throw error; error = validateGroupSource(source); if (error) throw error; const id = `gid-${uuid.v4()}`; [error] = await safe(database.query('INSERT INTO userGroups (id, name, source) VALUES (?, ?, ?)', [ id, name, source ])); if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, error); if (error) throw error; return { id, name }; } async function remove(id) { assert.strictEqual(typeof id, 'string'); // also cleanup the groupMembers table let queries = []; queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] }); queries.push({ query: 'DELETE FROM userGroups WHERE id = ?', args: [ id ] }); const result = await database.transaction(queries); if (result[1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Group not found'); } async function get(id) { assert.strictEqual(typeof id, 'string'); const result = await database.query(`SELECT ${GROUPS_FIELDS} FROM userGroups WHERE id = ? ORDER BY name`, [ id ]); if (result.length === 0) return null; return result[0]; } async function getByName(name) { assert.strictEqual(typeof name, 'string'); const result = await database.query(`SELECT ${GROUPS_FIELDS} FROM userGroups WHERE name = ?`, [ name ]); if (result.length === 0) return null; return result[0]; } async function getWithMembers(id) { assert.strictEqual(typeof id, 'string'); const results = await database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' + ' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' + ' WHERE userGroups.id = ? ' + ' GROUP BY userGroups.id', [ id ]); if (results.length === 0) return null; const result = results[0]; result.userIds = result.userIds ? result.userIds.split(',') : [ ]; return result; } async function list() { const results = await database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups ORDER BY name'); return results; } async function listWithMembers() { const results = await database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' + ' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' + ' GROUP BY userGroups.id ORDER BY name'); results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; }); return results; } async function getMembers(groupId) { assert.strictEqual(typeof groupId, 'string'); const result = await database.query('SELECT userId FROM groupMembers WHERE groupId=?', [ groupId ]); return result.map(function (r) { return r.userId; }); } async function getMembership(userId) { assert.strictEqual(typeof userId, 'string'); const result = await database.query('SELECT groupId FROM groupMembers WHERE userId=? ORDER BY groupId', [ userId ]); return result.map(function (r) { return r.groupId; }); } async function setMembership(userId, groupIds) { assert.strictEqual(typeof userId, 'string'); assert(Array.isArray(groupIds)); let 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 ] }); }); const [error] = await safe(database.transaction(queries)); if (error && error.code === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, error.message); if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.CONFLICT, 'Already member'); if (error) throw error; } async function addMember(groupId, userId) { assert.strictEqual(typeof groupId, 'string'); assert.strictEqual(typeof userId, 'string'); const [error] = await safe(database.query('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ groupId, userId ])); if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, error); if (error && error.code === 'ER_NO_REFERENCED_ROW_2' && error.sqlMessage.includes('userId')) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); if (error && error.code === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, 'Group not found'); if (error) throw error; } async function setMembers(groupId, userIds) { assert.strictEqual(typeof groupId, 'string'); assert(Array.isArray(userIds)); let queries = []; queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ groupId ] }); for (let i = 0; i < userIds.length; i++) { queries.push({ query: 'INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', args: [ groupId, userIds[i] ] }); } const [error] = await safe(database.transaction(queries)); if (error && error.code === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, 'Group not found'); if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.CONFLICT, 'Duplicate member in list'); if (error) throw error; } async function removeMember(groupId, userId) { assert.strictEqual(typeof groupId, 'string'); assert.strictEqual(typeof userId, 'string'); const result = await database.query('DELETE FROM groupMembers WHERE groupId = ? AND userId = ?', [ groupId, userId ]); if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Group not found'); } async function isMember(groupId, userId) { assert.strictEqual(typeof groupId, 'string'); assert.strictEqual(typeof userId, 'string'); const result = await database.query('SELECT 1 FROM groupMembers WHERE groupId=? AND userId=?', [ groupId, userId ]); return result.length !== 0; } async function update(id, data) { assert.strictEqual(typeof id, 'string'); assert(data && typeof data === 'object'); if ('name' in data) { assert.strictEqual(typeof data.name, 'string'); const error = validateGroupname(data.name); if (error) throw error; } const args = []; const fields = []; for (const k in data) { if (k === 'name') { assert.strictEqual(typeof data.name, 'string'); fields.push(k + ' = ?'); args.push(data.name); } } args.push(id); const [updateError, result] = await safe(database.query('UPDATE userGroups SET ' + fields.join(', ') + ' WHERE id = ?', args)); if (updateError && updateError.code === 'ER_DUP_ENTRY' && updateError.sqlMessage.indexOf('userGroups_name') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'name already exists'); if (updateError) throw updateError; if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Group not found'); }