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 ,
2020-12-22 10:34:19 -08:00
get ,
getByName ,
2024-01-19 22:28:48 +01:00
2024-01-19 22:48:29 +01:00
update ,
2024-01-19 22:28:48 +01:00
setName ,
2020-12-22 10:34:19 -08:00
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 ,
2020-12-22 10:34:19 -08:00
setMembers ,
isMember ,
2016-02-08 09:26:52 -08:00
2024-02-28 15:55:54 +01:00
setLocalMembership ,
2024-12-04 09:48:25 +01:00
resetSources ,
2024-01-13 22:35:23 +01:00
2025-02-12 12:36:50 +01:00
setAllowedApps ,
2024-02-28 15:55:54 +01:00
// exported for testing
_getMembership : getMembership
2016-02-07 20:25:08 -08:00
} ;
2025-02-12 12:36:50 +01:00
const apps = require ( './apps.js' ) ,
2025-08-14 11:17:38 +05:30
assert = require ( 'node: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' ) ,
2025-08-14 11:17:38 +05:30
crypto = require ( 'node:crypto' ) ,
2021-06-28 15:15:28 -07:00
database = require ( './database.js' ) ,
2024-12-04 09:48:25 +01:00
eventlog = require ( './eventlog.js' ) ,
2025-07-28 12:53:27 +02:00
safe = require ( 'safetydance' ) ;
2021-06-28 15:15:28 -07:00
const GROUPS _FIELDS = [ 'id' , 'name' , 'source' ] . join ( ',' ) ;
2016-02-07 20:25:08 -08:00
2016-09-30 12:25:42 -07:00
// keep this in sync with validateUsername
2024-01-19 22:48:29 +01:00
function validateName ( name ) {
2016-02-07 20:25:08 -08:00
assert . strictEqual ( typeof name , 'string' ) ;
2016-09-30 12:25:42 -07:00
2022-02-07 13:19:59 -08:00
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
2022-02-07 13:19:59 -08:00
if ( constants . RESERVED _NAMES . indexOf ( name ) !== - 1 ) return new BoxError ( BoxError . BAD _FIELD , 'name is reserved' ) ;
2016-02-09 12:16:30 -08:00
2018-06-14 22:20:09 -07:00
// need to consider valid LDAP characters here (e.g '+' is reserved)
2022-02-07 13:19:59 -08:00
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 ;
}
2024-01-19 22:48:29 +01:00
function validateSource ( source ) {
2020-06-04 14:17:56 +02:00
assert . strictEqual ( typeof source , 'string' ) ;
2022-02-07 13:19:59 -08:00
if ( source !== '' && source !== 'ldap' ) return new BoxError ( BoxError . BAD _FIELD , 'source must be "" or "ldap"' ) ;
2020-06-04 14:17:56 +02:00
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 || '' ;
2016-09-30 12:33:18 -07:00
2024-01-19 22:48:29 +01:00
let error = validateName ( name ) ;
2021-06-28 15:15:28 -07:00
if ( error ) throw error ;
2016-02-07 20:25:08 -08:00
2024-01-19 22:48:29 +01:00
error = validateSource ( source ) ;
2021-06-28 15:15:28 -07:00
if ( error ) throw error ;
2020-06-04 14:17:56 +02:00
2025-07-28 12:53:27 +02:00
const id = ` gid- ${ crypto . randomUUID ( ) } ` ;
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 ] ) ) ;
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , error ) ;
2021-06-28 15:15:28 -07:00
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
2025-10-20 16:37:14 +02:00
const arSearch = ` JSON_SEARCH(accessRestrictionJson, 'one', ?, NULL, ' $ .groups') ` ;
const opSearch = ` JSON_SEARCH(operatorsJson, 'one', ?, NULL, ' $ .groups') ` ;
2024-12-04 09:48:25 +01:00
const queries = [
2025-10-20 16:37:14 +02:00
{ query : ` UPDATE apps SET accessRestrictionJson=JSON_REMOVE(accessRestrictionJson, REPLACE( ${ arSearch } , '"', '')) WHERE ${ arSearch } IS NOT NULL ` , args : [ group . id , group . id ] } ,
{ query : ` UPDATE apps SET operatorsJson=JSON_REMOVE(operatorsJson, REPLACE( ${ opSearch } , '"', '')) WHERE ${ opSearch } IS NOT NULL ` , args : [ group . id , group . id ] } ,
2024-12-04 09:48:25 +01:00
{ query : 'DELETE FROM groupMembers WHERE groupId = ?' , args : [ group . id ] } ,
2025-10-20 16:37:14 +02:00
{ query : 'DELETE FROM userGroups WHERE id = ?' , args : [ group . id ] } , // keep this the last query as we check affectedRows below
2024-12-04 09:48:25 +01:00
] ;
2016-02-07 20:25:08 -08:00
2021-06-28 15:15:28 -07:00
const result = await database . transaction ( queries ) ;
2025-10-20 16:37:14 +02:00
if ( result [ queries . length - 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 12:36:50 +01:00
2025-02-12 14:09:09 +01:00
return result [ 0 ] ;
2025-02-12 12:36:50 +01:00
}
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 ) ;
2016-06-02 21:07:33 -07:00
2021-06-28 15:15:28 -07:00
return result ;
2016-06-02 21:07:33 -07:00
}
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' ) ;
2025-02-12 12:36:50 +01:00
for ( const r of results ) {
2025-02-12 14:09:09 +01:00
await postProcess ( r ) ;
2025-02-12 12:36:50 +01:00
}
2021-06-28 15:15:28 -07:00
return results ;
}
2016-06-02 21:07:33 -07:00
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
2025-11-04 09:12:25 +01: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
}
2024-02-28 15:55:54 +01: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
2024-02-28 15:55:54 +01:00
// ensure groups are actually local
for ( const groupId of localGroupIds ) {
2024-02-28 14:38:42 +01:00
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' ) ;
}
2024-02-28 15:55:54 +01:00
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 ) {
2024-01-19 22:48:29 +01:00
queries . push ( { query : 'INSERT INTO groupMembers (groupId, userId) VALUES (? , ?)' , args : [ gid , user . id ] } ) ;
2024-02-28 15:55:54 +01:00
}
2021-06-28 15:15:28 -07:00
const [ error ] = await safe ( database . transaction ( queries ) ) ;
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_NO_REFERENCED_ROW_2' ) throw new BoxError ( BoxError . NOT _FOUND , 'Group not found' ) ;
if ( error && error . sqlCode === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . CONFLICT , 'Already member' ) ;
2021-06-28 15:15:28 -07:00
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 ) {
2024-01-19 22:48:29 +01:00
assert . strictEqual ( typeof group , 'object' ) ;
2016-09-29 14:44:12 -07:00
assert ( Array . isArray ( userIds ) ) ;
2024-01-19 22:48:29 +01:00
assert . strictEqual ( typeof options , 'object' ) ;
2024-12-04 09:48:25 +01:00
assert ( auditSource && typeof auditSource === 'object' ) ;
2016-09-29 14:44:12 -07:00
2024-01-19 22:48:29 +01:00
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 ++ ) {
2024-01-19 22:48:29 +01:00
queries . push ( { query : 'INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)' , args : [ group . id , userIds [ i ] ] } ) ;
2021-06-28 15:15:28 -07:00
}
2016-09-29 14:44:12 -07:00
2021-06-28 15:15:28 -07:00
const [ error ] = await safe ( database . transaction ( queries ) ) ;
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_NO_REFERENCED_ROW_2' ) throw new BoxError ( BoxError . NOT _FOUND , 'Group not found' ) ;
if ( error && error . sqlCode === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . CONFLICT , 'Duplicate member in list' ) ;
2021-06-28 15:15:28 -07:00
if ( error ) throw error ;
2016-09-29 14:44:12 -07:00
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
}
2018-06-14 21:12:52 -07:00
2024-01-19 22:48:29 +01:00
async function update ( id , data ) {
assert . strictEqual ( typeof id , 'string' ) ;
assert ( data && typeof data === 'object' ) ;
2018-06-14 22:42:40 -07:00
2024-01-19 22:48:29 +01:00
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
2024-01-19 22:48:29 +01: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
2024-01-19 22:48:29 +01: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-01-13 22:35:23 +01:00
2024-12-04 09:48:25 +01:00
async function setName ( group , name , auditSource ) {
2024-01-19 22:48:29 +01:00
assert . strictEqual ( typeof group , 'object' ) ;
assert . strictEqual ( typeof name , 'string' ) ;
2024-12-04 09:48:25 +01:00
assert ( auditSource && typeof auditSource === 'object' ) ;
2024-01-19 22:48:29 +01:00
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-01-19 22:48:29 +01:00
}
2024-12-04 09:48:25 +01:00
async function resetSources ( ) {
2024-01-13 22:35:23 +01:00
await database . query ( 'UPDATE userGroups SET source = ?' , [ '' ] ) ;
}
2025-02-12 12:36:50 +01:00
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 } ) ;
}
}