Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import assert from 'node:assert' ;
import AuditSource from './auditsource.js' ;
import BoxError from './boxerror.js' ;
import constants from './constants.js' ;
import debugModule from 'debug' ;
import eventlog from './eventlog.js' ;
2026-02-14 15:43:24 +01:00
import ipaddr from './ipaddr.js' ;
import groups from './groups.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import ldap from 'ldapjs' ;
import path from 'node:path' ;
import paths from './paths.js' ;
2026-02-14 15:43:24 +01:00
import reverseProxy from './reverseproxy.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import safe from 'safetydance' ;
2026-02-14 15:43:24 +01:00
import settings from './settings.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import shellModule from './shell.js' ;
2026-02-14 15:43:24 +01:00
import users from './users.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import util from 'node:util' ;
const debug = debugModule ( 'box:directoryserver' ) ;
const shell = shellModule ( 'directoryserver' ) ;
2022-01-07 14:06:13 +01:00
2022-11-28 22:32:34 +01:00
let gServer = null , gCertificate = null ;
2022-01-07 14:06:13 +01:00
const NOOP = function ( ) { } ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
const SET _LDAP _ALLOWLIST _CMD = path . join ( import . meta . dirname , 'scripts/setldapallowlist.sh' ) ;
2022-02-16 12:57:38 -08:00
2023-08-03 02:26:11 +05:30
async function getConfig ( ) {
const value = await settings . get ( settings . DIRECTORY _SERVER _KEY ) ;
if ( value === null ) return {
enabled : false ,
secret : '' ,
2024-01-13 12:30:39 +01:00
allowlist : ''
2023-08-03 02:26:11 +05:30
} ;
return JSON . parse ( value ) ;
}
2022-02-16 12:57:38 -08:00
async function validateConfig ( config ) {
const { enabled , secret , allowlist } = config ;
if ( ! enabled ) return ;
if ( ! secret ) throw new BoxError ( BoxError . BAD _FIELD , 'secret cannot be empty' ) ;
let gotOne = false ;
for ( const line of allowlist . split ( '\n' ) ) {
if ( ! line || line . startsWith ( '#' ) ) continue ;
const rangeOrIP = line . trim ( ) ;
2025-09-08 18:59:47 +02:00
if ( ! ipaddr . isValid ( rangeOrIP ) && ! ipaddr . isValidCIDR ( rangeOrIP ) ) throw new BoxError ( BoxError . BAD _FIELD , ` ' ${ rangeOrIP } ' is not a valid IP or range ` ) ;
2022-02-16 12:57:38 -08:00
gotOne = true ;
}
// only allow if we at least have one allowed IP/range
if ( ! gotOne ) throw new BoxError ( BoxError . BAD _FIELD , 'allowlist must at least contain one IP or range' ) ;
}
2026-02-14 16:34:34 +01:00
async function authorize ( req , res , next ) {
debug ( 'authorize: ' , req . connection . ldap . bindDN . toString ( ) ) ;
2022-02-16 12:57:38 -08:00
2026-02-14 16:34:34 +01:00
// this is for connection attempts without previous bind
if ( req . connection . ldap . bindDN . equals ( 'cn=anonymous' ) ) return next ( new ldap . InsufficientAccessRightsError ( ) ) ;
2022-02-16 12:57:38 -08:00
2026-02-14 16:34:34 +01:00
// we only allow this one DN to pass
if ( ! req . connection . ldap . bindDN . equals ( constants . USER _DIRECTORY _LDAP _DN ) ) return next ( new ldap . InsufficientAccessRightsError ( ) ) ;
2022-08-15 21:08:22 +02:00
2026-02-14 16:34:34 +01:00
return next ( ) ;
}
2023-10-01 13:26:43 +05:30
2026-02-14 16:34:34 +01:00
async function maybeRootDSE ( req , res , next ) {
debug ( ` maybeRootDSE: requested with scope: ${ req . scope } dn: ${ req . dn . toString ( ) } ` ) ;
if ( req . scope !== 'base' ) return next ( new ldap . NoSuchObjectError ( ) ) ; // per the spec, rootDSE search require base scope
if ( ! req . dn || req . dn . toString ( ) !== '' ) return next ( new ldap . NoSuchObjectError ( ) ) ;
res . send ( {
dn : '' ,
attributes : {
objectclass : [ 'RootDSE' , 'top' , 'OpenLDAProotDSE' ] ,
supportedLDAPVersion : '3' ,
vendorName : 'Cloudron LDAP' ,
vendorVersion : '1.0.0' ,
supportedControl : [ ldap . PagedResultsControl . OID ] ,
supportedExtension : [ ]
}
} ) ;
res . end ( ) ;
2022-02-16 12:57:38 -08:00
}
2022-01-07 14:06:13 +01:00
2026-02-14 16:34:34 +01:00
// helper function to deal with pagination
async function userAuth ( req , res , next ) {
// extract the common name which might have different attribute names
const cnAttributeName = Object . keys ( req . dn . rdns [ 0 ] . attrs ) [ 0 ] ;
const commonName = req . dn . rdns [ 0 ] . attrs [ cnAttributeName ] . value ;
if ( ! commonName ) return next ( new ldap . NoSuchObjectError ( 'Missing CN' ) ) ;
2023-08-03 02:26:11 +05:30
2026-02-14 16:34:34 +01:00
// totptoken is passed as the "attribute" using the '+' separator in the first RDNS of the request DN
// when totptoken attribute is present, it signals that we must enforce totp check
// totp check is currently requested by the client. this is the only way to auth against external cloudron dashboard, external cloudron app and external apps
const TOTPTOKEN _ATTRIBUTE _NAME = 'totptoken' ; // This has to be in-sync with externalldap.js
const totpToken = TOTPTOKEN _ATTRIBUTE _NAME in req . dn . rdns [ 0 ] . attrs ? req . dn . rdns [ 0 ] . attrs [ TOTPTOKEN _ATTRIBUTE _NAME ] . value : null ;
const skipTotpCheck = ! ( TOTPTOKEN _ATTRIBUTE _NAME in req . dn . rdns [ 0 ] . attrs ) ;
2023-08-03 02:26:11 +05:30
2026-02-14 16:34:34 +01:00
let verifyFunc ;
if ( cnAttributeName === 'mail' ) {
verifyFunc = users . verifyWithEmail ;
} else if ( commonName . indexOf ( '@' ) !== - 1 ) { // if mail is specified, enforce mail check
verifyFunc = users . verifyWithEmail ;
} else if ( commonName . indexOf ( 'uid-' ) === 0 ) {
verifyFunc = users . verifyWithId ;
} else {
verifyFunc = users . verifyWithUsername ;
}
2024-01-13 12:18:14 +01:00
2026-02-14 16:34:34 +01:00
const [ error , user ] = await safe ( verifyFunc ( commonName , req . credentials || '' , '' , { totpToken , skipTotpCheck } ) ) ;
if ( error && error . reason === BoxError . NOT _FOUND ) return next ( new ldap . NoSuchObjectError ( error . message ) ) ;
if ( error && error . reason === BoxError . INVALID _CREDENTIALS ) return next ( new ldap . InvalidCredentialsError ( error . message ) ) ;
if ( error ) return next ( new ldap . OperationsError ( error . message ) ) ;
2023-08-03 02:26:11 +05:30
2026-02-14 16:34:34 +01:00
req . user = user ;
2024-01-13 12:18:14 +01:00
2026-02-14 16:34:34 +01:00
next ( ) ;
}
async function stop ( ) {
if ( ! gServer ) return ;
debug ( 'stopping server' ) ;
await util . promisify ( gServer . close . bind ( gServer ) ) ( ) ;
gServer = null ;
2023-08-03 02:26:11 +05:30
}
2022-01-07 14:06:13 +01:00
function finalSend ( results , req , res , next ) {
2024-10-14 19:10:31 +02:00
const min = 0 , max = results . length ;
2022-01-07 14:06:13 +01:00
let cookie = null ;
let pageSize = 0 ;
// check if this is a paging request, if so get the cookie for session info
req . controls . forEach ( function ( control ) {
if ( control . type === ldap . PagedResultsControl . OID ) {
pageSize = control . value . size ;
cookie = control . value . cookie ;
}
} ) ;
function sendPagedResults ( start , end ) {
start = ( start < min ) ? min : start ;
end = ( end > max || end < min ) ? max : end ;
let i ;
for ( i = start ; i < end ; i ++ ) {
res . send ( results [ i ] ) ;
}
return i ;
}
if ( cookie && Buffer . isBuffer ( cookie ) ) {
// we have pagination
2022-04-14 17:41:41 -05:00
let first = min ;
2022-01-07 14:06:13 +01:00
if ( cookie . length !== 0 ) {
first = parseInt ( cookie . toString ( ) , 10 ) ;
}
2022-04-14 17:41:41 -05:00
const last = sendPagedResults ( first , first + pageSize ) ;
2022-01-07 14:06:13 +01:00
2022-04-14 17:41:41 -05:00
let resultCookie ;
2022-01-07 14:06:13 +01:00
if ( last < max ) {
resultCookie = Buffer . from ( last . toString ( ) ) ;
} else {
resultCookie = Buffer . from ( '' ) ;
}
res . controls . push ( new ldap . PagedResultsControl ( {
value : {
size : pageSize , // correctness not required here
cookie : resultCookie
}
} ) ) ;
} else {
// no pagination simply send all
results . forEach ( function ( result ) {
res . send ( result ) ;
} ) ;
}
// all done
res . end ( ) ;
next ( ) ;
}
2026-02-14 16:34:34 +01:00
// Will attach req.user if successful
2022-01-07 14:06:13 +01:00
async function userSearch ( req , res , next ) {
debug ( 'user search: dn %s, scope %s, filter %s (from %s)' , req . dn . toString ( ) , req . scope , req . filter . toString ( ) , req . connection . ldap . id ) ;
2022-08-15 19:50:22 +02:00
const [ error , allUsers ] = await safe ( users . list ( ) ) ;
2024-01-03 14:51:00 +01:00
if ( error ) return next ( new ldap . OperationsError ( error . message ) ) ;
2022-01-07 14:06:13 +01:00
2022-08-04 11:22:16 +02:00
const [ groupsError , allGroups ] = await safe ( groups . listWithMembers ( ) ) ;
2024-01-03 14:51:00 +01:00
if ( groupsError ) return next ( new ldap . OperationsError ( groupsError . message ) ) ;
2022-08-04 11:22:16 +02:00
2024-10-14 19:10:31 +02:00
const results = [ ] ;
2022-01-07 14:06:13 +01:00
// send user objects
2022-08-15 19:50:22 +02:00
for ( const user of allUsers ) {
2022-01-07 14:06:13 +01:00
// skip entries with empty username. Some apps like owncloud can't deal with this
2022-08-15 19:50:22 +02:00
if ( ! user . username ) continue ;
2022-01-07 14:06:13 +01:00
2022-07-29 11:17:31 +02:00
const dn = ldap . parseDN ( ` cn= ${ user . id } ,ou=users,dc=cloudron ` ) ;
2022-01-07 14:06:13 +01:00
const displayName = user . displayName || user . username || '' ; // displayName can be empty and username can be null
2024-02-06 16:43:05 +01:00
const { firstName , lastName , middleName } = users . parseDisplayName ( displayName ) ;
2022-01-07 14:06:13 +01:00
2024-02-06 16:43:05 +01:00
// https://datatracker.ietf.org/doc/html/rfc2798
2022-01-07 14:06:13 +01:00
const obj = {
dn : dn . toString ( ) ,
attributes : {
objectclass : [ 'user' , 'inetorgperson' , 'person' ] ,
objectcategory : 'person' ,
2022-02-18 17:31:02 +01:00
cn : displayName ,
2022-01-07 14:06:13 +01:00
uid : user . id ,
entryuuid : user . id , // to support OpenLDAP clients
mail : user . email ,
mailAlternateAddress : user . fallbackEmail ,
displayname : displayName ,
givenName : firstName ,
2024-02-06 16:43:05 +01:00
sn : lastName ,
middleName : middleName ,
2022-01-07 14:06:13 +01:00
username : user . username ,
samaccountname : user . username , // to support ActiveDirectory clients
2022-10-30 15:07:26 +01:00
memberof : allGroups . filter ( function ( g ) { return g . userIds . indexOf ( user . id ) !== - 1 ; } ) . map ( function ( g ) { return ` cn= ${ g . name } ,ou=groups,dc=cloudron ` ; } )
2022-01-07 14:06:13 +01:00
}
} ;
2022-08-02 14:02:35 +02:00
if ( user . twoFactorAuthenticationEnabled ) obj . attributes . twoFactorAuthenticationEnabled = true ;
2022-01-07 14:06:13 +01:00
// ensure all filter values are also lowercase
const lowerCaseFilter = safe ( function ( ) { return ldap . parseFilter ( req . filter . toString ( ) . toLowerCase ( ) ) ; } , null ) ;
2024-01-03 14:51:00 +01:00
if ( ! lowerCaseFilter ) return next ( new ldap . OperationsError ( safe . error . message ) ) ;
2022-01-07 14:06:13 +01:00
if ( ( req . dn . equals ( dn ) || req . dn . parentOf ( dn ) ) && lowerCaseFilter . matches ( obj . attributes ) ) {
results . push ( obj ) ;
}
2022-08-15 19:50:22 +02:00
}
2022-01-07 14:06:13 +01:00
finalSend ( results , req , res , next ) ;
}
async function groupSearch ( req , res , next ) {
debug ( 'group search: dn %s, scope %s, filter %s (from %s)' , req . dn . toString ( ) , req . scope , req . filter . toString ( ) , req . connection . ldap . id ) ;
2022-08-15 19:50:22 +02:00
const [ error , allUsers ] = await safe ( users . list ( ) ) ;
2024-01-03 14:51:00 +01:00
if ( error ) return next ( new ldap . OperationsError ( error . message ) ) ;
2022-01-07 14:06:13 +01:00
const results = [ ] ;
2024-10-14 19:10:31 +02:00
const [ errorGroups , allGroups ] = await safe ( groups . listWithMembers ( ) ) ;
2024-01-03 14:51:00 +01:00
if ( errorGroups ) return next ( new ldap . OperationsError ( errorGroups . message ) ) ;
2022-01-07 14:06:13 +01:00
2022-08-15 19:50:22 +02:00
for ( const group of allGroups ) {
2022-07-29 11:17:31 +02:00
const dn = ldap . parseDN ( ` cn= ${ group . name } ,ou=groups,dc=cloudron ` ) ;
2022-08-15 19:50:22 +02:00
const members = group . userIds . filter ( function ( uid ) { return allUsers . map ( function ( u ) { return u . id ; } ) . indexOf ( uid ) !== - 1 ; } ) ;
2022-01-07 14:06:13 +01:00
const obj = {
dn : dn . toString ( ) ,
attributes : {
objectclass : [ 'group' ] ,
cn : group . name ,
2022-07-29 11:17:31 +02:00
gidnumber : group . id ,
2023-07-31 13:13:02 +02:00
memberuid : members ,
member : members . map ( ( userId ) => ` cn= ${ userId } ,ou=users,dc=cloudron ` ) ,
uniquemember : members . map ( ( userId ) => ` cn= ${ userId } ,ou=users,dc=cloudron ` )
2022-01-07 14:06:13 +01:00
}
} ;
// ensure all filter values are also lowercase
const lowerCaseFilter = safe ( function ( ) { return ldap . parseFilter ( req . filter . toString ( ) . toLowerCase ( ) ) ; } , null ) ;
2024-01-03 14:51:00 +01:00
if ( ! lowerCaseFilter ) return next ( new ldap . OperationsError ( safe . error . message ) ) ;
2022-01-07 14:06:13 +01:00
if ( ( req . dn . equals ( dn ) || req . dn . parentOf ( dn ) ) && lowerCaseFilter . matches ( obj . attributes ) ) {
results . push ( obj ) ;
}
2022-08-15 19:50:22 +02:00
}
2022-01-07 14:06:13 +01:00
finalSend ( results , req , res , next ) ;
}
async function start ( ) {
2023-10-01 13:26:43 +05:30
assert ( gServer === null , 'Already running' ) ;
2022-01-07 14:06:13 +01:00
const logger = {
trace : NOOP ,
debug : NOOP ,
info : debug ,
warn : debug ,
error : debug ,
fatal : debug
} ;
2022-11-28 22:32:34 +01:00
gCertificate = await reverseProxy . getDirectoryServerCertificate ( ) ;
2022-01-07 14:06:13 +01:00
gServer = ldap . createServer ( {
2022-11-28 22:32:34 +01:00
certificate : gCertificate . cert ,
key : gCertificate . key ,
2022-01-07 14:06:13 +01:00
log : logger
} ) ;
gServer . on ( 'error' , function ( error ) {
2023-04-16 10:49:59 +02:00
debug ( 'server startup error: %o' , error ) ;
2022-01-07 14:06:13 +01:00
} ) ;
gServer . bind ( 'ou=system,dc=cloudron' , async function ( req , res , next ) {
debug ( 'system bind: %s (from %s)' , req . dn . toString ( ) , req . connection . ldap . id ) ;
2023-08-03 02:26:11 +05:30
const config = await getConfig ( ) ;
2022-01-07 14:06:13 +01:00
2024-01-03 15:19:03 +01:00
if ( ! req . dn . equals ( constants . USER _DIRECTORY _LDAP _DN ) ) return next ( new ldap . InvalidCredentialsError ( 'Invalid DN' ) ) ;
if ( req . credentials !== config . secret ) return next ( new ldap . InvalidCredentialsError ( 'Invalid Secret' ) ) ;
2022-01-07 14:06:13 +01:00
2022-08-15 19:14:02 +02:00
req . user = { user : 'directoryServerAdmin' } ;
2022-01-07 14:06:13 +01:00
res . end ( ) ;
// if we use next in the callback, ldapjs requires this after res.end();
return next ( ) ;
} ) ;
gServer . search ( 'ou=users,dc=cloudron' , authorize , userSearch ) ;
gServer . search ( 'ou=groups,dc=cloudron' , authorize , groupSearch ) ;
gServer . bind ( 'ou=users,dc=cloudron' , userAuth , async function ( req , res ) {
assert . strictEqual ( typeof req . user , 'object' ) ;
2023-08-26 08:18:58 +05:30
await eventlog . upsertLoginEvent ( req . user . ghost ? eventlog . ACTION _USER _LOGIN _GHOST : eventlog . ACTION _USER _LOGIN , AuditSource . fromDirectoryServerRequest ( req ) , { userId : req . user . id , user : users . removePrivateFields ( req . user ) } ) ;
2022-01-07 14:06:13 +01:00
res . end ( ) ;
} ) ;
2022-10-12 21:58:14 +02:00
gServer . search ( '' , maybeRootDSE ) ; // when '', it seems the callback is called for everything else
2022-01-07 14:06:13 +01:00
// just log that an attempt was made to unknown route, this helps a lot during app packaging
gServer . use ( function ( req , res , next ) {
debug ( 'not handled: dn %s, scope %s, filter %s (from %s)' , req . dn ? req . dn . toString ( ) : '-' , req . scope , req . filter ? req . filter . toString ( ) : '-' , req . connection . ldap . id ) ;
return next ( ) ;
} ) ;
debug ( ` starting server on port ${ constants . USER _DIRECTORY _LDAPS _PORT } ` ) ;
2022-02-15 14:27:51 -08:00
await util . promisify ( gServer . listen . bind ( gServer ) ) ( constants . USER _DIRECTORY _LDAPS _PORT , '::' ) ;
2022-01-07 14:06:13 +01:00
}
2026-02-14 16:34:34 +01:00
// https://ldapwiki.com/wiki/RootDSE / RFC 4512 - ldapsearch -x -h "${CLOUDRON_LDAP_SERVER}" -p "${CLOUDRON_LDAP_PORT}" -b "" -s base
// ldapjs seems to call this handler for everything when search === ''
async function applyConfig ( config ) {
assert . strictEqual ( typeof config , 'object' ) ;
2022-01-07 14:06:13 +01:00
2026-02-14 16:34:34 +01:00
// this is done only because it's easier for the shell script and the firewall service to get the value
if ( config . enabled ) {
if ( ! safe . fs . writeFileSync ( paths . LDAP _ALLOWLIST _FILE , config . allowlist + '\n' , 'utf8' ) ) throw new BoxError ( BoxError . FS _ERROR , safe . error . message ) ;
} else {
safe . fs . unlinkSync ( paths . LDAP _ALLOWLIST _FILE ) ;
}
2022-01-07 14:06:13 +01:00
2026-02-14 16:34:34 +01:00
const [ error ] = await safe ( shell . sudo ( [ SET _LDAP _ALLOWLIST _CMD ] , { } ) ) ;
if ( error ) throw new BoxError ( BoxError . IPTABLES _ERROR , ` Error setting ldap allowlist: ${ error . message } ` ) ;
if ( ! config . enabled ) {
await stop ( ) ;
return ;
}
if ( ! gServer ) await start ( ) ;
}
async function setConfig ( directoryServerConfig , auditSource ) {
assert . strictEqual ( typeof directoryServerConfig , 'object' ) ;
assert ( auditSource && typeof auditSource === 'object' ) ;
if ( constants . DEMO ) throw new BoxError ( BoxError . BAD _STATE , 'Not allowed in demo mode' ) ;
const oldConfig = await getConfig ( ) ;
const config = {
enabled : directoryServerConfig . enabled ,
secret : directoryServerConfig . secret ,
allowlist : directoryServerConfig . allowlist || ''
} ;
await validateConfig ( config ) ;
await settings . setJson ( settings . DIRECTORY _SERVER _KEY , config ) ;
await applyConfig ( config ) ;
await eventlog . add ( eventlog . ACTION _DIRECTORY _SERVER _CONFIGURE , auditSource , { fromEnabled : oldConfig . enabled , toEnabled : config . enabled } ) ;
2022-01-07 14:06:13 +01:00
}
2022-11-11 18:09:10 +01:00
2022-11-30 15:16:16 +01:00
async function checkCertificate ( ) {
2023-10-01 13:26:43 +05:30
assert ( gServer !== null , 'Directory server is not running' ) ;
2022-11-28 22:32:34 +01:00
const certificate = await reverseProxy . getDirectoryServerCertificate ( ) ;
2022-11-30 09:54:35 +01:00
if ( certificate . cert === gCertificate . cert ) {
2022-11-30 15:16:16 +01:00
debug ( 'checkCertificate: certificate has not changed' ) ;
2022-11-28 22:32:34 +01:00
return ;
}
2022-11-30 15:16:16 +01:00
debug ( 'checkCertificate: certificate changed. restarting' ) ;
2022-11-11 18:09:10 +01:00
await stop ( ) ;
await start ( ) ;
}
2026-02-14 15:43:24 +01:00
export default {
getConfig ,
setConfig ,
start ,
stop ,
checkCertificate ,
} ;