Files
cloudron-box/src/groups.js

312 lines
11 KiB
JavaScript
Raw Normal View History

2016-02-07 20:25:08 -08:00
'use strict';
exports = module.exports = {
2021-06-28 15:15:28 -07:00
add,
2024-12-03 17:02:40 +01:00
del,
get,
getByName,
2024-01-19 22:28:48 +01:00
update,
2024-01-19 22:28:48 +01:00
setName,
getWithMembers,
2021-06-29 09:44:16 -07:00
list,
listWithMembers,
2016-02-08 09:41:21 -08:00
2024-12-04 09:48:25 +01:00
getMemberIds,
setMembers,
isMember,
2016-02-08 09:26:52 -08:00
setLocalMembership,
2024-12-04 09:48:25 +01:00
resetSources,
setAllowedApps,
// exported for testing
_getMembership: getMembership
2016-02-07 20:25:08 -08:00
};
const apps = require('./apps.js'),
assert = require('assert'),
2019-10-22 16:34:17 -07:00
BoxError = require('./boxerror.js'),
2016-09-20 15:07:11 -07:00
constants = require('./constants.js'),
2021-06-28 15:15:28 -07:00
database = require('./database.js'),
2024-12-04 09:48:25 +01:00
eventlog = require('./eventlog.js'),
2021-06-28 15:15:28 -07:00
safe = require('safetydance'),
uuid = require('uuid');
const GROUPS_FIELDS = [ 'id', 'name', 'source' ].join(',');
2016-02-07 20:25:08 -08:00
// keep this in sync with validateUsername
function validateName(name) {
2016-02-07 20:25:08 -08:00
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');
2016-02-07 20:25:08 -08:00
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');
2016-09-21 11:55:53 -07:00
2016-02-07 20:25:08 -08:00
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;
}
2024-12-04 09:48:25 +01:00
async function add(group, auditSource) {
2021-06-28 15:15:28 -07:00
assert.strictEqual(typeof group, 'object');
2024-12-04 09:48:25 +01:00
assert(auditSource && typeof auditSource === 'object');
2021-06-28 15:15:28 -07:00
let { name, source } = group;
2016-02-07 20:25:08 -08:00
2021-06-28 15:15:28 -07:00
name = name.toLowerCase(); // we store names in lowercase
source = source || '';
let error = validateName(name);
2021-06-28 15:15:28 -07:00
if (error) throw error;
2016-02-07 20:25:08 -08:00
error = validateSource(source);
2021-06-28 15:15:28 -07:00
if (error) throw error;
2021-06-28 15:15:28 -07:00
const id = `gid-${uuid.v4()}`;
2016-02-07 20:25:08 -08:00
2021-06-28 15:15:28 -07:00
[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;
2024-12-04 09:48:25 +01:00
await eventlog.add(eventlog.ACTION_GROUP_ADD, auditSource, { id, name, source });
2025-02-12 14:09:09 +01:00
return { id, name, source, appIds: [] };
2016-02-07 20:25:08 -08:00
}
2024-12-04 09:48:25 +01:00
async function del(group, auditSource) {
assert.strictEqual(typeof group, 'object');
assert(auditSource && typeof auditSource === 'object');
2016-02-07 20:25:08 -08:00
2024-12-04 09:48:25 +01:00
const queries = [
{ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ group.id ] },
{ query: 'DELETE FROM userGroups WHERE id = ?', args: [ group.id ] }
];
2016-02-07 20:25:08 -08:00
2021-06-28 15:15:28 -07:00
const result = await database.transaction(queries);
if (result[1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Group not found');
2024-12-04 09:48:25 +01:00
await eventlog.add(eventlog.ACTION_GROUP_REMOVE, auditSource, { group });
2016-02-07 20:25:08 -08:00
}
2025-02-12 14:09:09 +01:00
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);
}
2021-06-28 15:15:28 -07:00
async function get(id) {
2016-02-07 20:25:08 -08:00
assert.strictEqual(typeof id, 'string');
2021-06-28 15:15:28 -07:00
const result = await database.query(`SELECT ${GROUPS_FIELDS} FROM userGroups WHERE id = ? ORDER BY name`, [ id ]);
if (result.length === 0) return null;
2016-02-07 20:25:08 -08:00
2025-02-12 14:09:09 +01:00
await postProcess(result[0]);
2021-06-28 15:15:28 -07:00
return result[0];
2016-02-07 20:25:08 -08:00
}
2021-06-28 15:15:28 -07:00
async function getByName(name) {
2020-06-04 12:48:35 +02:00
assert.strictEqual(typeof name, 'string');
2021-06-28 15:15:28 -07:00
const result = await database.query(`SELECT ${GROUPS_FIELDS} FROM userGroups WHERE name = ?`, [ name ]);
if (result.length === 0) return null;
2020-06-04 12:48:35 +02:00
2025-02-12 14:09:09 +01:00
await postProcess(result[0]);
2025-02-12 14:09:09 +01:00
return result[0];
}
2021-06-28 15:15:28 -07:00
async function getWithMembers(id) {
2016-02-09 15:26:34 -08:00
assert.strictEqual(typeof id, 'string');
2021-06-28 15:15:28 -07:00
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 ]);
2016-02-09 15:26:34 -08:00
2021-06-28 15:15:28 -07:00
if (results.length === 0) return null;
2016-02-09 13:33:30 -08:00
2021-06-28 15:15:28 -07:00
const result = results[0];
result.userIds = result.userIds ? result.userIds.split(',') : [ ];
2025-02-12 14:09:09 +01:00
await postProcess(result);
2021-06-28 15:15:28 -07:00
return result;
}
2021-06-29 09:44:16 -07:00
async function list() {
2021-06-28 15:15:28 -07:00
const results = await database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups ORDER BY name');
for (const r of results) {
2025-02-12 14:09:09 +01:00
await postProcess(r);
}
2021-06-28 15:15:28 -07:00
return results;
}
2021-06-29 09:44:16 -07:00
async function listWithMembers() {
2021-06-28 15:15:28 -07:00
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');
2016-02-09 13:33:30 -08:00
2021-06-28 15:15:28 -07:00
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
2025-02-12 13:09:41 +01:00
for (const r of results) {
2025-02-12 14:09:09 +01:00
await postProcess(r);
2025-02-12 13:09:41 +01:00
}
2021-06-28 15:15:28 -07:00
return results;
2016-02-09 13:33:30 -08:00
}
2024-12-04 09:48:25 +01:00
async function getMemberIds(groupId) {
2016-02-08 09:41:21 -08:00
assert.strictEqual(typeof groupId, 'string');
2021-06-28 15:15:28 -07:00
const result = await database.query('SELECT userId FROM groupMembers WHERE groupId=?', [ groupId ]);
2016-02-08 09:41:21 -08:00
2021-06-28 15:15:28 -07:00
return result.map(function (r) { return r.userId; });
2016-02-08 09:41:21 -08:00
}
2021-06-28 15:15:28 -07:00
async function getMembership(userId) {
2016-02-08 20:38:50 -08:00
assert.strictEqual(typeof userId, 'string');
2021-06-28 15:15:28 -07:00
const result = await database.query('SELECT groupId FROM groupMembers WHERE userId=? ORDER BY groupId', [ userId ]);
2016-02-08 20:38:50 -08:00
2021-06-28 15:15:28 -07:00
return result.map(function (r) { return r.groupId; });
2016-02-08 20:38:50 -08:00
}
async function setLocalMembership(user, localGroupIds) {
assert.strictEqual(typeof user, 'object'); // can be local or external
assert(Array.isArray(localGroupIds));
2016-02-09 15:47:02 -08:00
// 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 ] });
}
2021-06-28 15:15:28 -07:00
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');
2021-06-28 15:15:28 -07:00
if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.CONFLICT, 'Already member');
if (error) throw error;
2016-02-09 15:47:02 -08:00
}
2024-12-04 09:48:25 +01:00
async function setMembers(group, userIds, options, auditSource) {
assert.strictEqual(typeof group, 'object');
assert(Array.isArray(userIds));
assert.strictEqual(typeof options, 'object');
2024-12-04 09:48:25 +01:00
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 ] });
2021-06-28 15:15:28 -07:00
for (let i = 0; i < userIds.length; i++) {
queries.push({ query: 'INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', args: [ group.id, userIds[i] ] });
2021-06-28 15:15:28 -07:00
}
2021-06-28 15:15:28 -07:00
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;
2024-12-04 09:48:25 +01:00
await eventlog.add(eventlog.ACTION_GROUP_MEMBERSHIP, auditSource, { group, userIds });
2016-02-08 09:41:21 -08:00
}
2016-02-08 10:53:01 -08:00
2021-06-28 15:15:28 -07:00
async function isMember(groupId, userId) {
2016-02-08 10:53:01 -08:00
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
2021-06-28 15:15:28 -07:00
const result = await database.query('SELECT 1 FROM groupMembers WHERE groupId=? AND userId=?', [ groupId, userId ]);
return result.length !== 0;
2016-02-08 10:53:01 -08:00
}
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;
}
2018-06-18 18:26:50 -07:00
if ('source' in data) {
assert.strictEqual(typeof data.source, 'string');
const error = validateSource(data.source);
if (error) throw error;
}
2019-07-03 13:47:12 +02:00
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));
2021-09-21 17:34:31 -07:00
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;
2021-06-28 15:15:28 -07:00
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Group not found');
2019-07-03 13:47:12 +02:00
}
2024-12-04 09:48:25 +01:00
async function setName(group, name, auditSource) {
assert.strictEqual(typeof group, 'object');
assert.strictEqual(typeof name, 'string');
2024-12-04 09:48:25 +01:00
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 });
2024-12-04 09:48:25 +01:00
await eventlog.add(eventlog.ACTION_GROUP_UPDATE, auditSource, { oldName: group.name, group });
}
2024-12-04 09:48:25 +01:00
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 });
}
}