Files
cloudron-box/src/groups.js
Girish Ramakrishnan 964c1a5f5a remove field from errors
we have standardized on indexOf in error.message by now
2022-02-07 13:44:29 -08:00

239 lines
8.4 KiB
JavaScript

'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');
}