2019-08-28 18:22:07 +02:00
'use strict' ;
2025-10-08 20:11:55 +02:00
exports = module . exports = {
getConfig ,
setConfig ,
verifyPassword ,
maybeCreateUser ,
supports2FA ,
startSyncer ,
removePrivateFields ,
sync
} ;
2025-08-14 11:17:38 +05:30
const assert = require ( 'node:assert' ) ,
2021-09-30 09:50:30 -07:00
AuditSource = require ( './auditsource.js' ) ,
2019-10-24 13:41:41 -07:00
BoxError = require ( './boxerror.js' ) ,
2019-10-25 15:58:11 -07:00
constants = require ( './constants.js' ) ,
2024-01-13 15:53:14 +01:00
cron = require ( './cron.js' ) ,
2019-08-30 19:11:27 +02:00
debug = require ( 'debug' ) ( 'box:externalldap' ) ,
2024-01-13 13:02:43 +01:00
eventlog = require ( './eventlog.js' ) ,
2020-06-04 13:26:13 +02:00
groups = require ( './groups.js' ) ,
2019-08-28 18:22:07 +02:00
ldap = require ( 'ldapjs' ) ,
2021-06-28 15:15:28 -07:00
safe = require ( 'safetydance' ) ,
2019-08-28 18:22:07 +02:00
settings = require ( './settings.js' ) ,
2019-08-29 17:19:51 +02:00
tasks = require ( './tasks.js' ) ,
2021-06-28 15:15:28 -07:00
users = require ( './users.js' ) ,
2025-08-14 11:17:38 +05:30
util = require ( 'node:util' ) ;
2019-08-28 18:22:07 +02:00
2019-10-25 15:58:11 -07:00
function removePrivateFields ( ldapConfig ) {
assert . strictEqual ( typeof ldapConfig , 'object' ) ;
2025-10-08 18:40:27 +02:00
delete ldapConfig . bindPassword ;
2019-10-25 15:58:11 -07:00
return ldapConfig ;
}
2025-10-08 18:40:27 +02:00
function injectPrivateFields ( newConfig , currentConfig ) {
if ( ! Object . hasOwn ( newConfig , 'bindPassword' ) ) newConfig . bindPassword = currentConfig . bindPassword ;
}
2019-11-19 09:53:00 +01:00
function translateUser ( ldapConfig , ldapUser ) {
assert . strictEqual ( typeof ldapConfig , 'object' ) ;
2022-02-18 17:31:02 +01:00
// RFC: https://datatracker.ietf.org/doc/html/rfc2798
2024-10-22 14:49:14 +02:00
const user = {
2024-10-30 17:41:54 +01:00
username : typeof ldapUser [ ldapConfig . usernameField ] === 'string' ? ldapUser [ ldapConfig . usernameField ] . toLowerCase ( ) : '' ,
2020-07-01 14:34:28 +02:00
email : ldapUser . mail || ldapUser . mailPrimaryAddress ,
2022-08-02 14:02:35 +02:00
twoFactorAuthenticationEnabled : ! ! ldapUser . twoFactorAuthenticationEnabled ,
2022-02-18 17:31:02 +01:00
displayName : ldapUser . displayName || ldapUser . cn // user.giveName + ' ' + user.sn
2019-11-19 09:53:00 +01:00
} ;
if ( ! user . username || ! user . email || ! user . displayName ) {
2020-07-01 14:34:28 +02:00
debug ( ` [Invalid LDAP user] username= ${ user . username } email= ${ user . email } displayName= ${ user . displayName } ` ) ;
2024-10-22 14:49:14 +02:00
return null ;
2019-11-19 09:53:00 +01:00
}
2024-10-22 14:49:14 +02:00
return user ;
2019-11-19 09:53:00 +01:00
}
2024-01-20 11:35:27 +01:00
function supports2FA ( config ) {
return config . provider === 'cloudron' ;
}
2019-08-28 18:22:07 +02:00
// performs service bind if required
2023-08-03 02:06:07 +05:30
async function getClient ( config , options ) {
assert . strictEqual ( typeof config , 'object' ) ;
2021-09-01 13:09:49 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2020-06-26 14:08:16 +02:00
2019-08-29 22:51:27 +02:00
// basic validation to not crash
2024-10-30 16:21:21 +01:00
try { ldap . parseDN ( config . baseDn ) ; } catch ( e ) { throw new BoxError ( BoxError . BAD _FIELD , ` invalid baseDn ${ config . baseDn } : ${ e . message } ` ) ; }
try { ldap . parseFilter ( config . filter ) ; } catch ( e ) { throw new BoxError ( BoxError . BAD _FIELD , ` invalid filter ${ config . filter } : ${ e . mssage } ` ) ; }
2020-06-25 17:36:23 +02:00
2021-09-01 13:09:49 -07:00
let client ;
2019-08-29 17:19:51 +02:00
try {
2023-08-03 02:06:07 +05:30
const ldapConfig = {
url : config . url ,
tlsOptions : {
rejectUnauthorized : config . acceptSelfSignedCerts ? false : true
2024-01-23 23:27:06 +01:00
} ,
// https://github.com/ldapjs/node-ldapjs/issues/486
timeout : 60000 ,
connectTimeout : 10000
2023-08-03 02:06:07 +05:30
} ;
client = ldap . createClient ( ldapConfig ) ;
2019-08-29 17:19:51 +02:00
} catch ( e ) {
2021-09-01 13:09:49 -07:00
if ( e instanceof ldap . ProtocolError ) throw new BoxError ( BoxError . BAD _FIELD , 'url protocol is invalid' ) ;
2025-02-27 16:55:12 +01:00
throw new BoxError ( BoxError . INTERNAL _ERROR , ` Client creation error: ${ e . message } ` ) ;
2019-08-29 17:19:51 +02:00
}
2019-08-28 18:22:07 +02:00
2021-09-01 13:09:49 -07:00
return await new Promise ( ( resolve , reject ) => {
// ensure we don't just crash
2024-01-23 23:27:06 +01:00
client . on ( 'error' , function ( error ) { // don't reject, we must have gotten a bind error
2023-04-30 21:49:28 +02:00
debug ( 'getClient: ExternalLdap client error:' , error ) ;
2021-09-01 13:09:49 -07:00
} ) ;
2019-08-28 18:22:07 +02:00
2023-01-12 14:39:58 +01:00
// skip bind auth if none exist or if not wanted
2023-08-03 02:06:07 +05:30
if ( ! config . bindDn || ! options . bind ) return resolve ( client ) ;
2023-01-12 14:39:58 +01:00
2023-08-03 02:06:07 +05:30
client . bind ( config . bindDn , config . bindPassword , function ( error ) {
2024-10-30 16:21:21 +01:00
if ( error instanceof ldap . InvalidCredentialsError ) return reject ( new BoxError ( BoxError . INVALID _CREDENTIALS , 'Incorrect bind password' ) ) ;
2025-02-27 16:55:12 +01:00
if ( error ) return reject ( new BoxError ( BoxError . EXTERNAL _ERROR , ` Bind error: ${ error . message } ` ) ) ;
2021-09-01 13:09:49 -07:00
resolve ( client ) ;
} ) ;
2019-08-28 18:22:07 +02:00
} ) ;
}
2021-09-01 13:09:49 -07:00
async function clientSearch ( client , dn , searchOptions ) {
assert . strictEqual ( typeof client , 'object' ) ;
2020-06-05 10:29:36 +02:00
assert . strictEqual ( typeof dn , 'string' ) ;
2021-09-01 13:09:49 -07:00
assert . strictEqual ( typeof searchOptions , 'object' ) ;
2020-06-05 10:29:36 +02:00
2021-09-01 13:09:49 -07:00
// basic validation to not crash
2024-10-30 16:21:21 +01:00
try { ldap . parseDN ( dn ) ; } catch ( e ) { throw new BoxError ( BoxError . BAD _FIELD , ` invalid dn ${ dn } : ${ e . message } ` ) ; }
2020-07-30 15:08:01 +02:00
2021-09-01 13:09:49 -07:00
return await new Promise ( ( resolve , reject ) => {
2020-06-05 10:29:36 +02:00
client . search ( dn , searchOptions , function ( error , result ) {
2024-10-30 16:21:21 +01:00
if ( error instanceof ldap . NoSuchObjectError ) return reject ( new BoxError ( BoxError . NOT _FOUND , ` dn not found ${ dn } ` ) ) ;
2025-02-27 16:55:12 +01:00
if ( error ) return reject ( new BoxError ( BoxError . EXTERNAL _ERROR , ` search error: ${ error . message } ` ) ) ;
2020-06-05 10:29:36 +02:00
2024-10-22 14:40:53 +02:00
const ldapObjects = [ ] ;
2020-06-05 10:29:36 +02:00
result . on ( 'searchEntry' , entry => ldapObjects . push ( entry . object ) ) ;
2025-02-27 16:55:12 +01:00
result . on ( 'error' , error => reject ( new BoxError ( BoxError . EXTERNAL _ERROR , ` search error: ${ error . message } ` ) ) ) ;
2020-06-05 10:29:36 +02:00
result . on ( 'end' , function ( result ) {
2021-09-01 13:09:49 -07:00
if ( result . status !== 0 ) return reject ( new BoxError ( BoxError . EXTERNAL _ERROR , 'Server returned status ' + result . status ) ) ;
2020-06-05 10:29:36 +02:00
2021-09-01 13:09:49 -07:00
resolve ( ldapObjects ) ;
2020-06-05 10:29:36 +02:00
} ) ;
} ) ;
} ) ;
}
2019-11-19 09:53:00 +01:00
2025-02-26 12:18:09 +01:00
async function supportsPagination ( client ) {
assert . strictEqual ( typeof client , 'object' ) ;
2019-10-30 14:37:48 -07:00
2021-09-01 13:09:49 -07:00
const searchOptions = {
2025-02-26 12:18:09 +01:00
scope : 'base' ,
filter : '(objectClass=*)' ,
attributes : [ 'supportedControl' , 'supportedExtension' , 'supportedFeature' ]
2021-09-01 13:09:49 -07:00
} ;
2019-10-30 14:37:48 -07:00
2025-02-26 12:18:09 +01:00
const result = await clientSearch ( client , '' , searchOptions ) ;
const controls = result . supportedControl ;
if ( ! controls || ! Array . isArray ( controls ) ) {
debug ( 'supportsPagination: no supportedControl attribute returned' ) ;
return false ;
}
if ( ! controls . includes ( ldap . PagedResultsControl . OID ) ) {
debug ( 'supportsPagination: server does not support pagination. Available controls:' , controls ) ;
return false ;
}
debug ( 'supportsPagination: server supports pagination' ) ;
return true ;
}
async function ldapGetByDN ( config , dn ) {
assert . strictEqual ( typeof config , 'object' ) ;
assert . strictEqual ( typeof dn , 'string' ) ;
2021-09-01 13:09:49 -07:00
debug ( ` ldapGetByDN: Get object at ${ dn } ` ) ;
2019-10-30 14:37:48 -07:00
2023-08-03 02:06:07 +05:30
const client = await getClient ( config , { bind : true } ) ;
2025-02-26 12:18:09 +01:00
const paged = await supportsPagination ( client ) ;
const searchOptions = {
paged ,
scope : 'sub'
} ;
2021-09-01 13:09:49 -07:00
const result = await clientSearch ( client , dn , searchOptions ) ;
client . unbind ( ) ;
2024-10-30 16:21:21 +01:00
if ( result . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , ` dn ${ dn } not found ` ) ;
2021-09-01 13:09:49 -07:00
return result [ 0 ] ;
2019-10-30 14:37:48 -07:00
}
2023-08-03 02:06:07 +05:30
async function ldapUserSearch ( config , options ) {
assert . strictEqual ( typeof config , 'object' ) ;
2020-06-03 21:23:53 +02:00
assert . strictEqual ( typeof options , 'object' ) ;
2025-02-26 12:18:09 +01:00
const client = await getClient ( config , { bind : true } ) ;
const paged = await supportsPagination ( client ) ;
2025-02-26 14:14:09 +01:00
2021-09-01 13:09:49 -07:00
const searchOptions = {
2025-02-26 12:18:09 +01:00
paged ,
2023-08-03 02:06:07 +05:30
filter : ldap . parseFilter ( config . filter ) ,
2025-02-26 12:18:09 +01:00
scope : 'sub'
2021-09-01 13:09:49 -07:00
} ;
2020-06-03 21:23:53 +02:00
2021-09-01 13:09:49 -07:00
if ( options . filter ) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
const extraFilter = ldap . parseFilter ( options . filter ) ;
searchOptions . filter = new ldap . AndFilter ( { filters : [ extraFilter , searchOptions . filter ] } ) ;
}
2020-06-03 21:23:53 +02:00
2023-08-03 02:06:07 +05:30
const result = await clientSearch ( client , config . baseDn , searchOptions ) ;
2021-09-01 13:09:49 -07:00
client . unbind ( ) ;
return result ;
}
2020-06-03 21:23:53 +02:00
2023-08-03 02:06:07 +05:30
async function ldapGroupSearch ( config , options ) {
assert . strictEqual ( typeof config , 'object' ) ;
2021-09-01 13:09:49 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2020-06-03 21:23:53 +02:00
2021-09-01 13:09:49 -07:00
const searchOptions = {
paged : true ,
scope : 'sub' // We may have to make this configurable
} ;
2020-06-03 21:23:53 +02:00
2023-08-03 02:06:07 +05:30
if ( config . groupFilter ) searchOptions . filter = ldap . parseFilter ( config . groupFilter ) ;
2020-06-03 21:23:53 +02:00
2021-09-01 13:09:49 -07:00
if ( options . filter ) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
const extraFilter = ldap . parseFilter ( options . filter ) ;
searchOptions . filter = new ldap . AndFilter ( { filters : [ extraFilter , searchOptions . filter ] } ) ;
}
2020-06-03 21:23:53 +02:00
2023-08-03 02:06:07 +05:30
const client = await getClient ( config , { bind : true } ) ;
const result = await clientSearch ( client , config . groupBaseDn , searchOptions ) ;
2021-09-01 13:09:49 -07:00
client . unbind ( ) ;
return result ;
2020-06-03 21:23:53 +02:00
}
2021-09-01 13:09:49 -07:00
async function testConfig ( config ) {
2019-08-28 18:22:07 +02:00
assert . strictEqual ( typeof config , 'object' ) ;
2021-09-01 13:09:49 -07:00
if ( config . provider === 'noop' ) return null ;
2019-08-29 12:28:41 +02:00
2021-09-01 13:09:49 -07:00
if ( ! config . url ) return new BoxError ( BoxError . BAD _FIELD , 'url must not be empty' ) ;
if ( ! config . url . startsWith ( 'ldap://' ) && ! config . url . startsWith ( 'ldaps://' ) ) return new BoxError ( BoxError . BAD _FIELD , 'url is missing ldap:// or ldaps:// prefix' ) ;
2019-10-25 15:47:55 -07:00
if ( ! config . usernameField ) config . usernameField = 'uid' ;
2019-10-31 11:46:00 -07:00
2019-10-30 09:35:30 -07:00
// bindDn may not be a dn!
2021-09-01 13:09:49 -07:00
if ( ! config . baseDn ) return new BoxError ( BoxError . BAD _FIELD , 'basedn must not be empty' ) ;
2024-10-30 16:21:21 +01:00
try { ldap . parseDN ( config . baseDn ) ; } catch ( e ) { throw new BoxError ( BoxError . BAD _FIELD , ` invalid base ${ config . baseDn } : ${ e . message } ` ) ; }
2019-10-31 11:46:00 -07:00
2021-09-01 13:09:49 -07:00
if ( ! config . filter ) return new BoxError ( BoxError . BAD _FIELD , 'filter must not be empty' ) ;
2024-10-30 16:21:21 +01:00
try { ldap . parseFilter ( config . filter ) ; } catch ( e ) { return new BoxError ( BoxError . BAD _FIELD , ` invalid filter ${ config . filter } : ${ e . message } ` ) ; }
2019-08-28 18:22:07 +02:00
2021-09-01 13:09:49 -07:00
if ( 'syncGroups' in config && typeof config . syncGroups !== 'boolean' ) return new BoxError ( BoxError . BAD _FIELD , 'syncGroups must be a boolean' ) ;
if ( 'acceptSelfSignedCerts' in config && typeof config . acceptSelfSignedCerts !== 'boolean' ) return new BoxError ( BoxError . BAD _FIELD , 'acceptSelfSignedCerts must be a boolean' ) ;
2020-06-07 13:49:01 +02:00
if ( config . syncGroups ) {
2021-09-01 13:09:49 -07:00
if ( ! config . groupBaseDn ) return new BoxError ( BoxError . BAD _FIELD , 'groupBaseDn must not be empty' ) ;
2024-10-30 16:21:21 +01:00
try { ldap . parseDN ( config . groupBaseDn ) ; } catch ( e ) { return new BoxError ( BoxError . BAD _FIELD , ` invalid groupBaseDn ${ config . groupBaseDn } : ${ e . message } ` ) ; }
2020-06-07 13:49:01 +02:00
2021-09-01 13:09:49 -07:00
if ( ! config . groupFilter ) return new BoxError ( BoxError . BAD _FIELD , 'groupFilter must not be empty' ) ;
2024-10-30 16:21:21 +01:00
try { ldap . parseFilter ( config . groupFilter ) ; } catch ( e ) { return new BoxError ( BoxError . BAD _FIELD , ` invalid groupFilter ${ config . groupFilter } : ${ e . message } ` ) ; }
2020-06-07 13:49:01 +02:00
2021-09-01 13:09:49 -07:00
if ( ! config . groupnameField || typeof config . groupnameField !== 'string' ) return new BoxError ( BoxError . BAD _FIELD , 'groupFilter must not be empty' ) ;
2020-06-07 13:49:01 +02:00
}
2021-09-01 13:09:49 -07:00
const [ error , client ] = await safe ( getClient ( config , { bind : true } ) ) ;
if ( error ) return error ;
2019-08-28 18:22:07 +02:00
2021-09-01 13:09:49 -07:00
const opts = {
filter : config . filter ,
scope : 'sub'
} ;
2019-08-28 18:22:07 +02:00
2021-09-01 13:09:49 -07:00
const [ searchError , ] = await safe ( clientSearch ( client , config . baseDn , opts ) ) ;
client . unbind ( ) ;
if ( searchError ) return searchError ;
2019-08-28 18:22:07 +02:00
2021-09-01 13:09:49 -07:00
return null ;
2019-08-28 18:22:07 +02:00
}
2025-10-08 18:40:27 +02:00
async function getConfig ( ) {
const value = await settings . get ( settings . EXTERNAL _LDAP _KEY ) ;
if ( value === null ) return { provider : 'noop' , autoCreate : false } ;
const config = JSON . parse ( value ) ;
if ( ! config . autoCreate ) config . autoCreate = false ; // ensure new keys
return config ;
}
async function setConfig ( newConfig , auditSource ) {
assert . strictEqual ( typeof newConfig , 'object' ) ;
assert ( auditSource && typeof auditSource === 'object' ) ;
if ( constants . DEMO ) throw new BoxError ( BoxError . BAD _STATE , 'Not allowed in demo mode' ) ;
const currentConfig = await getConfig ( ) ;
injectPrivateFields ( newConfig , currentConfig ) ;
const error = await testConfig ( newConfig ) ;
if ( error ) throw error ;
await settings . setJson ( settings . EXTERNAL _LDAP _KEY , newConfig ) ;
if ( newConfig . provider === 'noop' ) {
await users . resetSources ( ) ; // otherwise, the owner could be 'ldap' source and lock themselves out
await groups . resetSources ( ) ;
}
await eventlog . add ( eventlog . ACTION _EXTERNAL _LDAP _CONFIGURE , auditSource , { oldConfig : removePrivateFields ( currentConfig ) , config : removePrivateFields ( newConfig ) } ) ;
await cron . handleExternalLdapChanged ( newConfig ) ;
}
2021-10-26 18:04:25 +02:00
async function maybeCreateUser ( identifier ) {
2019-11-19 09:53:00 +01:00
assert . strictEqual ( typeof identifier , 'string' ) ;
2023-08-03 02:06:07 +05:30
const config = await getConfig ( ) ;
if ( config . provider === 'noop' ) throw new BoxError ( BoxError . BAD _STATE , 'not enabled' ) ;
if ( ! config . autoCreate ) throw new BoxError ( BoxError . BAD _STATE , 'auto create not enabled' ) ;
2019-11-19 09:53:00 +01:00
2023-08-03 02:06:07 +05:30
const ldapUsers = await ldapUserSearch ( config , { filter : ` ${ config . usernameField } = ${ identifier } ` } ) ;
2024-10-30 16:21:21 +01:00
if ( ldapUsers . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , ` no users found for filter ${ config . usernameField } = ${ identifier } ` ) ;
if ( ldapUsers . length > 1 ) throw new BoxError ( BoxError . CONFLICT , ` more than 1 user matches filter ${ config . usernameField } = ${ identifier } ` ) ;
2019-11-19 09:53:00 +01:00
2023-08-03 02:06:07 +05:30
const user = translateUser ( config , ldapUsers [ 0 ] ) ;
2024-10-30 16:21:21 +01:00
if ( ! user ) throw new BoxError ( BoxError . BAD _FIELD , 'Failed to translate user' ) ;
2019-11-19 09:53:00 +01:00
2023-08-26 08:18:58 +05:30
return await users . add ( user . email , { username : user . username , password : null , displayName : user . displayName , source : 'ldap' } , AuditSource . EXTERNAL _LDAP ) ;
2022-08-02 14:02:35 +02:00
}
2024-01-08 11:55:35 +01:00
async function verifyPassword ( username , password , options ) {
2024-01-07 22:01:57 +01:00
assert . strictEqual ( typeof username , 'string' ) ;
2022-08-02 14:02:35 +02:00
assert . strictEqual ( typeof password , 'string' ) ;
2024-01-08 11:55:35 +01:00
assert . strictEqual ( typeof options , 'object' ) ;
2022-08-02 14:02:35 +02:00
2023-08-03 02:06:07 +05:30
const config = await getConfig ( ) ;
if ( config . provider === 'noop' ) throw new BoxError ( BoxError . BAD _STATE , 'not enabled' ) ;
2022-08-02 14:02:35 +02:00
2024-01-07 22:01:57 +01:00
const ldapUsers = await ldapUserSearch ( config , { filter : ` ${ config . usernameField } = ${ username } ` } ) ;
2024-10-30 16:21:21 +01:00
if ( ldapUsers . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'no such user' ) ;
if ( ldapUsers . length > 1 ) throw new BoxError ( BoxError . CONFLICT , 'multiple users found' ) ;
2022-08-02 14:02:35 +02:00
2023-08-03 02:06:07 +05:30
const client = await getClient ( config , { bind : false } ) ;
2022-08-02 14:02:35 +02:00
2023-03-12 15:09:20 +01:00
let userAuthDn ;
2024-01-20 13:21:01 +01:00
if ( ! options . skipTotpCheck && supports2FA ( config ) ) {
2024-01-07 22:01:57 +01:00
// inject totptoken into first attribute. in ldap, '+' is the attribute separator in a RDNS
2023-03-12 15:09:20 +01:00
const rdns = ldapUsers [ 0 ] . dn . split ( ',' ) ;
2024-01-08 11:55:35 +01:00
userAuthDn = ` ${ rdns [ 0 ] } +totptoken= ${ options . totpToken } , ` + rdns . slice ( 1 ) . join ( ',' ) ;
2023-03-12 15:09:20 +01:00
} else {
userAuthDn = ldapUsers [ 0 ] . dn ;
}
2022-08-02 14:02:35 +02:00
2023-03-12 15:09:20 +01:00
const [ error ] = await safe ( util . promisify ( client . bind . bind ( client ) ) ( userAuthDn , password ) ) ;
2022-08-02 14:02:35 +02:00
client . unbind ( ) ;
2024-01-03 15:19:03 +01:00
if ( error instanceof ldap . InvalidCredentialsError ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , error . lde _message ) ;
2025-02-27 16:55:12 +01:00
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bind error: ${ error . message } ` ) ;
2020-07-01 14:59:26 +02:00
2024-10-22 14:49:14 +02:00
const user = translateUser ( config , ldapUsers [ 0 ] ) ;
2024-10-30 16:21:21 +01:00
if ( ! user ) throw new BoxError ( BoxError . BAD _FIELD , 'could not translate user' ) ;
2024-10-22 14:49:14 +02:00
return user ;
2019-08-29 22:43:06 +02:00
}
2021-09-01 13:09:49 -07:00
async function startSyncer ( ) {
2023-08-03 02:06:07 +05:30
const config = await getConfig ( ) ;
if ( config . provider === 'noop' ) throw new BoxError ( BoxError . BAD _STATE , 'not enabled' ) ;
2019-08-28 18:22:07 +02:00
2021-09-01 13:09:49 -07:00
const taskId = await tasks . add ( tasks . TASK _SYNC _EXTERNAL _LDAP , [ ] ) ;
2025-06-17 18:54:12 +02:00
safe ( tasks . startTask ( taskId , { } ) , { debug } ) ; // background
2021-09-01 13:09:49 -07:00
return taskId ;
2019-08-29 17:19:51 +02:00
}
2023-08-03 02:06:07 +05:30
async function syncUsers ( config , progressCallback ) {
assert . strictEqual ( typeof config , 'object' ) ;
2019-08-29 17:19:51 +02:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2023-08-03 02:06:07 +05:30
const ldapUsers = await ldapUserSearch ( config , { } ) ;
2019-08-29 17:19:51 +02:00
2021-09-01 13:09:49 -07:00
debug ( ` syncUsers: Found ${ ldapUsers . length } users ` ) ;
2019-08-29 17:19:51 +02:00
2021-09-01 13:09:49 -07:00
let percent = 10 ;
2024-10-22 14:40:53 +02:00
const step = 30 / ( ldapUsers . length + 1 ) ; // ensure no divide by 0
2019-08-29 22:56:48 +02:00
2021-09-01 13:09:49 -07:00
// we ignore all errors here and just log them for now
for ( let i = 0 ; i < ldapUsers . length ; i ++ ) {
2024-10-22 14:40:53 +02:00
const ldapUser = translateUser ( config , ldapUsers [ i ] ) ;
2024-10-22 14:49:14 +02:00
if ( ! ldapUser ) continue ;
2019-10-25 16:58:15 -07:00
2021-09-01 13:09:49 -07:00
percent += step ;
progressCallback ( { percent , message : ` Syncing... ${ ldapUser . username } ` } ) ;
2021-09-01 14:37:09 +02:00
2021-09-01 13:09:49 -07:00
const user = await users . getByUsername ( ldapUser . username ) ;
2021-09-01 14:37:09 +02:00
2021-09-01 13:09:49 -07:00
if ( ! user ) {
debug ( ` syncUsers: [adding user] username= ${ ldapUser . username } email= ${ ldapUser . email } displayName= ${ ldapUser . displayName } ` ) ;
2023-08-26 08:18:58 +05:30
const [ userAddError ] = await safe ( users . add ( ldapUser . email , { username : ldapUser . username , password : null , displayName : ldapUser . displayName , source : 'ldap' } , AuditSource . EXTERNAL _LDAP ) ) ;
2023-04-16 10:49:59 +02:00
if ( userAddError ) debug ( 'syncUsers: Failed to create user. %j %o' , ldapUser , userAddError ) ;
2021-09-01 13:09:49 -07:00
} else if ( user . source !== 'ldap' ) {
2021-09-13 21:17:27 +02:00
debug ( ` syncUsers: [mapping user] username= ${ ldapUser . username } email= ${ ldapUser . email } displayName= ${ ldapUser . displayName } ` ) ;
2023-08-26 08:18:58 +05:30
const [ userMappingError ] = await safe ( users . update ( user , { email : ldapUser . email , fallbackEmail : ldapUser . email , displayName : ldapUser . displayName , source : 'ldap' } , AuditSource . EXTERNAL _LDAP ) ) ;
2023-04-16 10:49:59 +02:00
if ( userMappingError ) debug ( 'Failed to map user. %j %o' , ldapUser , userMappingError ) ;
2021-09-01 13:09:49 -07:00
} else if ( user . email !== ldapUser . email || user . displayName !== ldapUser . displayName ) {
debug ( ` syncUsers: [updating user] username= ${ ldapUser . username } email= ${ ldapUser . email } displayName= ${ ldapUser . displayName } ` ) ;
2023-08-26 08:18:58 +05:30
const [ userUpdateError ] = await safe ( users . update ( user , { email : ldapUser . email , fallbackEmail : ldapUser . email , displayName : ldapUser . displayName } , AuditSource . EXTERNAL _LDAP ) ) ;
2023-04-16 10:49:59 +02:00
if ( userUpdateError ) debug ( 'Failed to update user. %j %o' , ldapUser , userUpdateError ) ;
2021-09-01 13:09:49 -07:00
} else {
// user known and up-to-date
debug ( ` syncUsers: [up-to-date user] username= ${ ldapUser . username } email= ${ ldapUser . email } displayName= ${ ldapUser . displayName } ` ) ;
2021-09-01 14:37:09 +02:00
}
2021-09-01 13:09:49 -07:00
}
2019-10-30 14:37:48 -07:00
2021-09-01 13:09:49 -07:00
debug ( 'syncUsers: done' ) ;
2020-06-05 09:26:52 +02:00
}
2020-06-03 21:23:53 +02:00
2023-08-03 02:06:07 +05:30
async function syncGroups ( config , progressCallback ) {
assert . strictEqual ( typeof config , 'object' ) ;
2020-06-05 09:26:52 +02:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2020-06-03 21:23:53 +02:00
2023-08-03 02:06:07 +05:30
if ( ! config . syncGroups ) {
2021-09-01 13:09:49 -07:00
debug ( 'syncGroups: Group sync is disabled' ) ;
2020-06-07 13:49:01 +02:00
progressCallback ( { percent : 70 , message : 'Skipping group sync...' } ) ;
2021-09-01 13:09:49 -07:00
return [ ] ;
2020-06-05 09:26:52 +02:00
}
2023-08-03 02:06:07 +05:30
const ldapGroups = await ldapGroupSearch ( config , { } ) ;
2020-06-04 13:26:13 +02:00
2021-09-01 13:09:49 -07:00
debug ( ` syncGroups: Found ${ ldapGroups . length } groups ` ) ;
2020-06-04 13:26:13 +02:00
2021-09-01 13:09:49 -07:00
let percent = 40 ;
2024-10-22 14:40:53 +02:00
const step = 30 / ( ldapGroups . length + 1 ) ; // ensure no divide by 0
2020-06-04 13:26:13 +02:00
2021-09-01 13:09:49 -07:00
for ( const ldapGroup of ldapGroups ) {
2023-08-03 02:06:07 +05:30
let groupName = ldapGroup [ config . groupnameField ] ;
2021-09-01 13:09:49 -07:00
if ( ! groupName ) return ;
2024-01-19 22:48:29 +01:00
if ( typeof groupName !== 'string' ) return ; // some servers return empty array for unknown properties :-/
2020-06-05 10:13:19 +02:00
2021-09-01 13:09:49 -07:00
groupName = groupName . toLowerCase ( ) ;
2020-06-05 09:03:26 +02:00
2021-09-01 13:09:49 -07:00
percent += step ;
progressCallback ( { percent , message : ` Syncing... ${ groupName } ` } ) ;
2020-06-04 12:28:31 +02:00
2021-09-01 13:09:49 -07:00
const result = await groups . getByName ( groupName ) ;
2020-06-05 09:26:52 +02:00
2021-09-01 13:09:49 -07:00
if ( ! result ) {
debug ( ` syncGroups: [adding group] groupname= ${ groupName } ` ) ;
2024-12-04 09:48:25 +01:00
const [ error ] = await safe ( groups . add ( { name : groupName , source : 'ldap' } , AuditSource . EXTERNAL _LDAP ) ) ;
2021-09-01 13:09:49 -07:00
if ( error ) debug ( 'syncGroups: Failed to create group' , groupName , error ) ;
} else {
2024-01-19 22:48:29 +01:00
// convert local group to ldap group. 2 reasons:
2025-11-23 15:35:18 +01:00
// 1. we reset source flag when externalldap is disabled. if we renable, it automatically converts
2024-01-19 22:48:29 +01:00
// 2. externalldap connector usually implies user wants to user external users/groups.
groups . update ( result . id , { source : 'ldap' } ) ;
2021-09-01 13:09:49 -07:00
debug ( ` syncGroups: [up-to-date group] groupname= ${ groupName } ` ) ;
}
}
2020-06-05 09:26:52 +02:00
2021-09-01 13:09:49 -07:00
debug ( 'syncGroups: sync done' ) ;
2020-06-05 09:26:52 +02:00
}
2024-01-19 17:24:35 +01:00
async function syncGroupMembers ( config , progressCallback ) {
2023-08-03 02:06:07 +05:30
assert . strictEqual ( typeof config , 'object' ) ;
2020-06-05 09:26:52 +02:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2023-08-03 02:06:07 +05:30
if ( ! config . syncGroups ) {
2024-01-19 17:24:35 +01:00
debug ( 'syncGroupMembers: Group users sync is disabled' ) ;
2020-06-05 09:26:52 +02:00
progressCallback ( { percent : 99 , message : 'Skipping group users sync...' } ) ;
2021-09-01 13:09:49 -07:00
return [ ] ;
2020-06-05 09:26:52 +02:00
}
2025-11-04 09:12:25 +01:00
const allGroups = await groups . listWithMembers ( ) ;
2021-09-01 13:09:49 -07:00
const ldapGroups = allGroups . filter ( function ( g ) { return g . source === 'ldap' ; } ) ;
2024-01-19 17:24:35 +01:00
debug ( ` syncGroupMembers: Found ${ ldapGroups . length } groups to sync users ` ) ;
2021-09-01 13:09:49 -07:00
2025-11-04 09:12:25 +01:00
for ( const ldapGroup of ldapGroups ) {
debug ( ` syncGroupMembers: Sync users for group ${ ldapGroup . name } ` ) ;
2021-09-01 13:09:49 -07:00
2023-08-03 02:06:07 +05:30
const result = await ldapGroupSearch ( config , { } ) ;
2021-09-01 13:09:49 -07:00
if ( ! result || result . length === 0 ) {
2025-11-04 09:12:25 +01:00
debug ( ` syncGroupMembers: Unable to find group ${ ldapGroup . name } ignoring for now. ` ) ;
2021-09-01 13:09:49 -07:00
continue ;
}
// since our group names are lowercase we cannot use potentially case matching ldap filters
2024-10-22 14:40:53 +02:00
const found = result . find ( function ( r ) {
2023-08-03 02:06:07 +05:30
if ( ! r [ config . groupnameField ] ) return false ;
2025-11-04 09:12:25 +01:00
return r [ config . groupnameField ] . toLowerCase ( ) === ldapGroup . name ;
2021-09-01 13:09:49 -07:00
} ) ;
if ( ! found ) {
2025-11-04 09:12:25 +01:00
debug ( ` syncGroupMembers: Unable to find group ${ ldapGroup . name } ignoring for now. ` ) ;
2021-09-01 13:09:49 -07:00
continue ;
}
2023-07-31 13:12:39 +02:00
let ldapGroupMembers = found . member || found . uniquemember || [ ] ;
2021-09-01 13:09:49 -07:00
// if only one entry is in the group ldap returns a string, not an array!
if ( typeof ldapGroupMembers === 'string' ) ldapGroupMembers = [ ldapGroupMembers ] ;
2025-11-04 09:12:25 +01:00
debug ( ` syncGroupMembers: Group ${ ldapGroup . name } has ${ ldapGroupMembers . length } members. ` ) ;
2021-09-01 13:09:49 -07:00
2024-01-19 17:24:35 +01:00
const userIds = [ ] ;
2021-09-01 13:09:49 -07:00
for ( const memberDn of ldapGroupMembers ) {
2023-08-03 02:06:07 +05:30
const [ ldapError , result ] = await safe ( ldapGetByDN ( config , memberDn ) ) ;
2021-09-01 13:09:49 -07:00
if ( ldapError ) {
2025-11-04 09:12:25 +01:00
debug ( ` syncGroupMembers: Group ${ ldapGroup . name } failed to get ${ memberDn } : %o ` , ldapError ) ;
2021-09-01 13:09:49 -07:00
continue ;
}
2025-11-04 09:12:25 +01:00
debug ( ` syncGroupMembers: Group ${ ldapGroup . name } has member object ${ memberDn } ` ) ;
2021-09-01 13:09:49 -07:00
2024-01-19 17:24:35 +01:00
const username = result [ config . usernameField ] ? . toLowerCase ( ) ;
2021-09-01 13:09:49 -07:00
if ( ! username ) continue ;
const [ getError , userObject ] = await safe ( users . getByUsername ( username ) ) ;
if ( getError || ! userObject ) {
2024-01-19 17:24:35 +01:00
debug ( ` syncGroupMembers: Failed to get user by username ${ username } . %o ` , getError ? getError : 'User not found' ) ;
2021-09-01 13:09:49 -07:00
continue ;
}
2024-01-19 17:24:35 +01:00
userIds . push ( userObject . id ) ;
2021-09-01 13:09:49 -07:00
}
2025-11-04 09:20:12 +01:00
const membersChanged = ldapGroup . userIds . length !== userIds . length || ldapGroup . userIds . some ( id => ! userIds . includes ( id ) ) ;
2025-11-04 09:12:25 +01:00
if ( membersChanged ) {
debug ( ` syncGroupMembers: Group ${ ldapGroup . name } changed. ` ) ;
const [ setError ] = await safe ( groups . setMembers ( ldapGroup , userIds , { skipSourceCheck : true } , AuditSource . EXTERNAL _LDAP ) ) ;
if ( setError ) debug ( ` syncGroupMembers: Failed to set members of group ${ ldapGroup . name } . %o ` , setError ) ;
} else {
debug ( ` syncGroupMembers: Group ${ ldapGroup . name } is unchanged. ` ) ;
}
2021-09-01 13:09:49 -07:00
}
2024-01-19 17:24:35 +01:00
debug ( 'syncGroupMembers: done' ) ;
2020-06-05 09:26:52 +02:00
}
2021-09-01 13:09:49 -07:00
async function sync ( progressCallback ) {
2020-06-05 09:26:52 +02:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
progressCallback ( { percent : 10 , message : 'Starting ldap user sync' } ) ;
2023-08-03 02:06:07 +05:30
const config = await getConfig ( ) ;
if ( config . provider === 'noop' ) throw new BoxError ( BoxError . BAD _STATE , 'not enabled' ) ;
2020-06-05 09:26:52 +02:00
2023-08-03 02:06:07 +05:30
await syncUsers ( config , progressCallback ) ;
await syncGroups ( config , progressCallback ) ;
2024-01-19 17:24:35 +01:00
await syncGroupMembers ( config , progressCallback ) ;
2020-06-05 09:26:52 +02:00
2021-09-01 13:09:49 -07:00
progressCallback ( { percent : 100 , message : 'Done' } ) ;
2020-06-05 09:26:52 +02:00
2021-09-01 13:09:49 -07:00
debug ( 'sync: done' ) ;
2019-08-28 18:22:07 +02:00
}
2025-10-08 18:40:27 +02:00