diff --git a/src/ldap.js b/src/ldap.js index db203aa92..c7a131837 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -40,99 +40,149 @@ function getAppByRequest(req, callback) { }); } +function userSearch(req, res, next) { + debug('user search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString()); + + user.list(function (error, result) { + if (error) return next(new ldap.OperationsError(error.toString())); + + // send user objects + result.forEach(function (entry) { + var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron'); + + var groups = [ GROUP_USERS_DN ]; + if (entry.admin) groups.push(GROUP_ADMINS_DN); + + var displayName = entry.displayName || entry.username; + var nameParts = displayName.split(' '); + var firstName = nameParts[0]; + var lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists + + var obj = { + dn: dn.toString(), + attributes: { + objectclass: ['user'], + objectcategory: 'person', + cn: entry.id, + uid: entry.id, + mail: entry.email, + displayname: displayName, + givenName: firstName, + username: entry.username, + samaccountname: entry.username, // to support ActiveDirectory clients + memberof: groups + } + }; + + // http://www.zytrax.com/books/ldap/ape/core-schema.html#sn has 'name' as SUP which is a DirectoryString + // which is required to have atleast one character if present + if (lastName.length !== 0) obj.attributes.sn = lastName; + + // ensure all filter values are also lowercase + var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase()); + + if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { + res.send(obj); + } + }); + + res.end(); + }); +} + +function groupSearch(req, res, next) { + debug('group search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString()); + + user.list(function (error, result){ + if (error) return next(new ldap.OperationsError(error.toString())); + + var groups = [{ + name: 'users', + admin: false + }, { + name: 'admins', + admin: true + }]; + + groups.forEach(function (group) { + var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron'); + var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result; + + var obj = { + dn: dn.toString(), + attributes: { + objectclass: ['group'], + cn: group.name, + memberuid: members.map(function(entry) { return entry.id; }) + } + }; + + // ensure all filter values are also lowercase + var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase()); + + if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { + res.send(obj); + } + }); + + res.end(); + }); +} + +function userBind(req, res, next) { + debug('user bind: %s', req.dn.toString()); + + // extract the common name which might have different attribute names + var attributeName = Object.keys(req.dn.rdns[0])[0]; + var commonName = req.dn.rdns[0][attributeName]; + if (!commonName) return next(new ldap.NoSuchObjectError(req.dn.toString())); + + var api; + // if mail is specified, enforce mail check + if (commonName.indexOf('@') !== -1 || attributeName === 'mail') { + api = user.verifyWithEmail; + } else if (commonName.indexOf('uid-') === 0) { + api = user.verify; + } else { + api = user.verifyWithUsername; + } + + // TODO this should be done after we verified the app has access to avoid leakage of user existence + api(commonName, req.credentials || '', function (error, userObject) { + if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); + if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString())); + if (error) return next(new ldap.OperationsError(error)); + + getAppByRequest(req, function (error, app) { + if (error) return next(error); + + if (!app) { + debug('no app found for this container, allow access'); + return res.end(); + } + + apps.hasAccessTo(app, userObject, function (error, result) { + if (error) return next(new ldap.OperationsError(error.toString())); + + // we return no such object, to avoid leakage of a users existence + if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString())); + + eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: userObject.id }); + + res.end(); + }); + }); + }); +} + function start(callback) { assert.strictEqual(typeof callback, 'function'); gServer = ldap.createServer({ log: gLogger }); - gServer.search('ou=users,dc=cloudron', function (req, res, next) { - debug('user search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString()); - - user.list(function (error, result) { - if (error) return next(new ldap.OperationsError(error.toString())); - - // send user objects - result.forEach(function (entry) { - var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron'); - - var groups = [ GROUP_USERS_DN ]; - if (entry.admin) groups.push(GROUP_ADMINS_DN); - - var displayName = entry.displayName || entry.username; - var nameParts = displayName.split(' '); - var firstName = nameParts[0]; - var lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists - - var obj = { - dn: dn.toString(), - attributes: { - objectclass: ['user'], - objectcategory: 'person', - cn: entry.id, - uid: entry.id, - mail: entry.email, - displayname: displayName, - givenName: firstName, - username: entry.username, - samaccountname: entry.username, // to support ActiveDirectory clients - memberof: groups - } - }; - - // http://www.zytrax.com/books/ldap/ape/core-schema.html#sn has 'name' as SUP which is a DirectoryString - // which is required to have atleast one character if present - if (lastName.length !== 0) obj.attributes.sn = lastName; - - // ensure all filter values are also lowercase - var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase()); - - if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { - res.send(obj); - } - }); - - res.end(); - }); - }); - - gServer.search('ou=groups,dc=cloudron', function (req, res, next) { - debug('group search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString()); - - user.list(function (error, result){ - if (error) return next(new ldap.OperationsError(error.toString())); - - var groups = [{ - name: 'users', - admin: false - }, { - name: 'admins', - admin: true - }]; - - groups.forEach(function (group) { - var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron'); - var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result; - - var obj = { - dn: dn.toString(), - attributes: { - objectclass: ['group'], - cn: group.name, - memberuid: members.map(function(entry) { return entry.id; }) - } - }; - - // ensure all filter values are also lowercase - var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase()); - - if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { - res.send(obj); - } - }); - - res.end(); - }); - }); + gServer.search('ou=users,dc=cloudron', userSearch); + gServer.search('ou=groups,dc=cloudron', groupSearch); + gServer.bind('ou=users,dc=cloudron', userBind); // this is the bind for the mail addon to authorize apps gServer.bind('ou=sendmail,dc=cloudron', function(req, res, next) { @@ -155,53 +205,6 @@ function start(callback) { res.end(); }); - // this is the bind for users - gServer.bind('ou=users,dc=cloudron', function(req, res, next) { - debug('user bind: %s', req.dn.toString()); - - // extract the common name which might have different attribute names - var attributeName = Object.keys(req.dn.rdns[0])[0]; - var commonName = req.dn.rdns[0][attributeName]; - if (!commonName) return next(new ldap.NoSuchObjectError(req.dn.toString())); - - var api; - // if mail is specified, enforce mail check - if (commonName.indexOf('@') !== -1 || attributeName === 'mail') { - api = user.verifyWithEmail; - } else if (commonName.indexOf('uid-') === 0) { - api = user.verify; - } else { - api = user.verifyWithUsername; - } - - // TODO this should be done after we verified the app has access to avoid leakage of user existence - api(commonName, req.credentials || '', function (error, userObject) { - if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); - if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString())); - if (error) return next(new ldap.OperationsError(error)); - - getAppByRequest(req, function (error, app) { - if (error) return next(error); - - if (!app) { - debug('no app found for this container, allow access'); - return res.end(); - } - - apps.hasAccessTo(app, userObject, function (error, result) { - if (error) return next(new ldap.OperationsError(error.toString())); - - // we return no such object, to avoid leakage of a users existence - if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString())); - - eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: userObject.id }); - - res.end(); - }); - }); - }); - }); - gServer.listen(config.get('ldapPort'), callback); }