'use strict'; exports = module.exports = { add, del, get, getByName, update, setName, getWithMembers, list, listWithMembers, getMemberIds, setMembers, isMember, setLocalMembership, resetSources, setAllowedApps, // exported for testing _getMembership: getMembership }; const apps = require('./apps.js'), assert = require('assert'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), database = require('./database.js'), eventlog = require('./eventlog.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, auditSource) { assert.strictEqual(typeof group, 'object'); assert(auditSource && typeof auditSource === '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; await eventlog.add(eventlog.ACTION_GROUP_ADD, auditSource, { id, name, source }); return { id, name, source, appIds: [] }; } async function del(group, auditSource) { assert.strictEqual(typeof group, 'object'); assert(auditSource && typeof auditSource === 'object'); const queries = [ { query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ group.id ] }, { query: 'DELETE FROM userGroups WHERE id = ?', args: [ group.id ] } ]; const result = await database.transaction(queries); if (result[1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Group not found'); await eventlog.add(eventlog.ACTION_GROUP_REMOVE, auditSource, { group }); } async function postProcess(group) { assert.strictEqual(typeof group, 'object'); const results = await database.query('SELECT id FROM apps WHERE JSON_CONTAINS(accessRestrictionJson, ?, "$.groups")', [ `"${group.id}"` ]); group.appIds = results.map(r => r.id); } 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; await postProcess(result[0]); 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; await postProcess(result[0]); 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(',') : [ ]; await postProcess(result); return result; } async function list() { const results = await database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups ORDER BY name'); for (const r of results) { await postProcess(r); } 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(',') : [ ]; }); for (const r of results) { await postProcess(r); } return results; } async function getMemberIds(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, auditSource) { assert.strictEqual(typeof group, 'object'); assert(Array.isArray(userIds)); assert.strictEqual(typeof options, 'object'); assert(auditSource && typeof auditSource === '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; await eventlog.add(eventlog.ACTION_GROUP_MEMBERSHIP, auditSource, { group, userIds }); } 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, auditSource) { assert.strictEqual(typeof group, 'object'); assert.strictEqual(typeof name, 'string'); assert(auditSource && typeof auditSource === 'object'); if (group.source === 'ldap') throw new BoxError(BoxError.BAD_STATE, 'Cannot set name of external group'); await update(group.id, { name }); await eventlog.add(eventlog.ACTION_GROUP_UPDATE, auditSource, { oldName: group.name, group }); } async function resetSources() { await database.query('UPDATE userGroups SET source = ?', [ '' ]); } async function setAllowedApps(group, appIds, auditSource) { assert.strictEqual(typeof group, 'object'); assert(Array.isArray(appIds)); assert.strictEqual(typeof auditSource, 'object'); const result = await apps.list(); for (const app of result) { const accessRestriction = app.accessRestriction || { users: [], groups: [] }; if (appIds.includes(app.id)) { // add if (accessRestriction.groups.includes(group.id)) continue; accessRestriction.groups.push(group.id); } else { // remove if (!accessRestriction.groups?.includes(group.id)) continue; accessRestriction.groups = accessRestriction.groups.filter(gid => gid !== group.id); } await apps.update(app.id, { accessRestriction }); } }