'use strict'; exports = module.exports = { add, remove, get, getByName, update, setName, getWithMembers, list, listWithMembers, getMembers, setMembers, removeMember, isMember, setLocalMembership, resetSource, // exported for testing _getMembership: 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 validateName(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 validateSource(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 = validateName(name); if (error) throw error; error = validateSource(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, source }; } 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 setLocalMembership(user, localGroupIds) { assert.strictEqual(typeof user, 'object'); // can be local or external assert(Array.isArray(localGroupIds)); // ensure groups are actually local for (const groupId of localGroupIds) { const group = await get(groupId); if (!group) throw new BoxError(BoxError.NOT_FOUND, `Group ${groupId} not found`); if (group.source) throw new BoxError(BoxError.BAD_STATE, 'Cannot set members of external group'); } const queries = []; // a remote user may already be part of some external groups. do not clear those because remote groups are non-editable queries.push({ query: 'DELETE FROM groupMembers WHERE userId = ? AND groupId IN (SELECT id FROM userGroups WHERE source = ?)', args: [ user.id, '' ] }); for (const gid of localGroupIds) { queries.push({ query: 'INSERT INTO groupMembers (groupId, userId) VALUES (? , ?)', args: [ gid, user.id ] }); } 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, 'Already member'); if (error) throw error; } async function setMembers(group, userIds, options) { assert.strictEqual(typeof group, 'object'); assert(Array.isArray(userIds)); assert.strictEqual(typeof options, 'object'); if (!options.skipSourceCheck && group.source === 'ldap') throw new BoxError(BoxError.BAD_STATE, 'Cannot set members of external group'); const queries = []; queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ group.id ] }); for (let i = 0; i < userIds.length; i++) { queries.push({ query: 'INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', args: [ group.id, 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'); data.name = data.name.toLowerCase(); const error = validateName(data.name); if (error) throw error; } if ('source' in data) { assert.strictEqual(typeof data.source, 'string'); const error = validateSource(data.source); if (error) throw error; } const args = []; const fields = []; for (const k in data) { if (k === 'name' || k === 'source') { fields.push(k + ' = ?'); args.push(data[k]); } } 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'); } async function setName(group, name) { assert.strictEqual(typeof group, 'object'); assert.strictEqual(typeof name, 'string'); if (group.source === 'ldap') throw new BoxError(BoxError.BAD_STATE, 'Cannot set name of external group'); await update(group.id, { name }); } async function resetSource() { await database.query('UPDATE userGroups SET source = ?', [ '' ]); }