mostly because code is being autogenerated by all the AI stuff using this prefix. it's also used in the stack trace.
312 lines
11 KiB
JavaScript
312 lines
11 KiB
JavaScript
'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('node:assert'),
|
|
BoxError = require('./boxerror.js'),
|
|
constants = require('./constants.js'),
|
|
crypto = require('node:crypto'),
|
|
database = require('./database.js'),
|
|
eventlog = require('./eventlog.js'),
|
|
safe = require('safetydance');
|
|
|
|
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-${crypto.randomUUID()}`;
|
|
|
|
[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 });
|
|
}
|
|
}
|