ohif-viewer/Packages/accounts-ldap/ldap_server.js
2016-03-22 15:43:06 -04:00

366 lines
13 KiB
JavaScript

Future = Npm.require('fibers/future');
// At a minimum, set up LDAP_DEFAULTS.url and .dn according to
// your needs. url should appear as 'ldap://your.url.here'
// dn should appear in normal ldap format of comma separated attribute=value
// e.g. 'uid=someuser,cn=users,dc=somevalue'
LDAP_DEFAULTS = {
url: false,
port: '389',
dn: false,
searchDN: false,
searchCredentials: false,
createNewUser: true,
defaultDomain: false,
searchResultsProfileMap: false,
base: null,
search: '(objectclass=*)',
ldapsCertificate: false
};
/**
@class LDAP
@constructor
*/
var LDAP = function (options) {
// Set options
this.options = _.defaults(options, LDAP_DEFAULTS);
// Make sure options have been set
try {
check(this.options.url, String);
//check(this.options.dn, String);
} catch (e) {
throw new Meteor.Error('Bad Defaults', 'Options not set. Make sure to set LDAP_DEFAULTS.url and LDAP_DEFAULTS.dn!');
}
// Because NPM ldapjs module has some binary builds,
// We had to create a wraper package for it and build for
// certain architectures. The package typ:ldap-js exports
// 'MeteorWrapperLdapjs' which is a wrapper for the npm module
this.ldapjs = MeteorWrapperLdapjs;
};
/**
* Attempt to bind (authenticate) ldap
* and perform a dn search if specified
*
* @method ldapCheck
*
* @param {Object} options Object with username, ldapPass and overrides for LDAP_DEFAULTS object.
* Additionally the searchBeforeBind parameter can be specified, which is used to search for the DN
* if not provided.
*/
LDAP.prototype.ldapCheck = function (options) {
var self = this;
options = options || {};
if (options.hasOwnProperty('username') && options.hasOwnProperty('ldapPass')) {
var ldapAsyncFut = new Future();
// Create ldap client
var fullUrl = self.options.url + ':' + self.options.port;
var client = null;
if (self.options.url.indexOf('ldaps://') === 0) {
client = self.ldapjs.createClient({
url: fullUrl,
tlsOptions: {
ca: [self.options.ldapsCertificate]
}
});
}
else {
client = self.ldapjs.createClient({
url: fullUrl
});
}
// Slide @xyz.whatever from username if it was passed in
// and replace it with the domain specified in defaults
var emailSliceIndex = options.username.indexOf('@');
var username;
var domain = self.options.defaultDomain;
// If user appended email domain, strip it out
// And use the defaults.defaultDomain if set
if (emailSliceIndex !== -1) {
username = options.username.substring(0, emailSliceIndex);
domain = domain || options.username.substring((emailSliceIndex + 1), options.username.length);
} else {
username = options.username;
}
// If DN is provided, use it to bind
if (self.options.dn) {
// Attempt to bind to ldap server with provided info
client.bind(self.options.searchDN || self.options.dn, self.options.searchCredentials || options.ldapPass, function (err) {
try {
if (err) {
// Bind failure, return error
throw new Meteor.Error(err.code, err.message);
}
var handleSearchProfile = function (retObject, bindAfterSearch) {
retObject.emptySearch = true;
// construct list of ldap attributes to fetch
var attributes = [];
if (self.options.searchResultsProfileMap) {
self.options.searchResultsProfileMap.map(function (item) {
attributes.push(item.resultKey);
});
}
// use base if given, else the dn for the ldap search
var searchBase = self.options.base || self.options.dn;
var searchOptions = {
scope: 'sub',
sizeLimit: 1,
attributes: attributes,
filter: self.options.search
};
client.search(searchBase, searchOptions, function (err, res) {
if (err) {
ldapAsyncFut.return({
error: err
});
}
res.on('searchEntry', function (entry) {
retObject.emptySearch = false;
// Add entry results to return object
retObject.searchResults = entry.object;
if (bindAfterSearch) {
client.bind(retObject.searchResults.dn, options.ldapPass, function (err) {
try {
if (err) {
throw new Meteor.Error(err.code, err.message);
}
ldapAsyncFut.return(retObject);
} catch (e) {
ldapAsyncFut.return({
error: e
});
}
})
} else ldapAsyncFut.return(retObject);
});
// This call causes Future issue: "Future resolved more than once"
// Because it is already called in searchEntry handler
/*res.on('end', function () {
ldapAsyncFut.return(retObject);
});*/
});
};
var retObject = {
username: username,
searchResults: null,
email: domain ? username + '@' + domain : false
};
if (self.options.searchDN) {
handleSearchProfile(retObject, true);
} else if (self.options.searchResultsProfileMap) {
handleSearchProfile(retObject, false);
} else {
ldapAsyncFut.return(retObject);
}
} catch (e) {
ldapAsyncFut.return({
error: e
});
}
});
}
// DN not provided, search for DN and use result to bind
else if (typeof self.options.searchBeforeBind !== undefined) {
// initialize result
var retObject = {
username: username,
email: domain ? username + '@' + domain : false,
emptySearch: true,
searchResults: {}
};
// compile attribute list to return
var searchAttributes = ['dn'];
self.options.searchResultsProfileMap.map(function (item) {
searchAttributes.push(item.resultKey);
});
var filter = self.options.search;
Object.keys(options.ldapOptions.searchBeforeBind).forEach(function (searchKey) {
filter = '&' + filter + '(' + searchKey + '=' + options.ldapOptions.searchBeforeBind[searchKey] + ')';
});
var searchOptions = {
scope: 'sub',
sizeLimit: 1,
filter: filter
};
// perform LDAP search to determine DN
client.search(self.options.base, searchOptions, function (err, res) {
retObject.emptySearch = true;
res.on('searchEntry', function (entry) {
retObject.dn = entry.objectName;
retObject.username = retObject.dn;
retObject.emptySearch = false;
// Return search results if specified
if (self.options.searchResultsProfileMap) {
// construct list of ldap attributes to fetch
self.options.searchResultsProfileMap.map(function (item) {
retObject.searchResults[item.resultKey] = entry.object[item.resultKey];
});
}
// use the determined DN to bind
client.bind(entry.objectName, options.ldapPass, function (err) {
try {
if (err) {
throw new Meteor.Error(err.code, err.message);
}
else {
ldapAsyncFut.return(retObject);
}
}
catch (e) {
ldapAsyncFut.return({
error: e
});
}
});
});
// If no dn is found, return as is.
res.on('end', function (result) {
if (retObject.dn === undefined) {
ldapAsyncFut.return(retObject);
}
});
});
}
return ldapAsyncFut.wait();
} else {
throw new Meteor.Error(403, 'Missing LDAP Auth Parameter');
}
};
// Register login handler with Meteor
// Here we create a new LDAP instance with options passed from
// Meteor.loginWithLDAP on client side
// @param {Object} loginRequest will consist of username, ldapPass, ldap, and ldapOptions
Accounts.registerLoginHandler('ldap', function (loginRequest) {
// If 'ldap' isn't set in loginRequest object,
// then this isn't the proper handler (return undefined)
if (!loginRequest.ldap) {
return undefined;
}
// Instantiate LDAP with options
var userOptions = loginRequest.ldapOptions || {};
var ldapObj = new LDAP(userOptions);
// Call ldapCheck and get response
var ldapResponse = ldapObj.ldapCheck(loginRequest);
if (ldapResponse.error) {
return {
userId: null,
error: ldapResponse.error
};
}
else if (ldapResponse.emptySearch) {
return {
userId: null,
error: new Meteor.Error(403, 'User not found in LDAP')
};
}
else {
// Set initial userId and token vals
var userId = null;
var stampedToken = {
token: null
};
// Look to see if user already exists
var user = Meteor.users.findOne({
username: ldapResponse.username
});
// Login user if they exist
if (user) {
userId = user._id;
// Create hashed token so user stays logged in
stampedToken = Accounts._generateStampedLoginToken();
var hashStampedToken = Accounts._hashStampedToken(stampedToken);
// Update the user's token in mongo
Meteor.users.update(userId, {
$push: {
'services.resume.loginTokens': hashStampedToken
}
});
}
// Otherwise create user if option is set
else if (ldapObj.options.createNewUser) {
var userObject = {
username: ldapResponse.username
};
// Set email
if (ldapResponse.email) userObject.email = ldapResponse.email;
// Set profile values if specified in searchResultsProfileMap
if (ldapResponse.searchResults && ldapObj.options.searchResultsProfileMap.length > 0) {
var profileMap = ldapObj.options.searchResultsProfileMap;
var profileObject = {};
// Loop through profileMap and set values on profile object
for (var i = 0; i < profileMap.length; i++) {
var resultKey = profileMap[i].resultKey;
// If our search results have the specified property, set the profile property to its value
if (ldapResponse.searchResults.hasOwnProperty(resultKey)) {
profileObject[profileMap[i].profileProperty] = ldapResponse.searchResults[resultKey];
}
}
// Set userObject profile
userObject.profile = profileObject;
}
userId = Accounts.createUser(userObject);
} else {
// Ldap success, but no user created
console.log('LDAP Authentication succeeded for ' + ldapResponse.username + ', but no user exists in Meteor. Either create the user manually or set LDAP_DEFAULTS.createNewUser to true');
return {
userId: null,
error: new Meteor.Error(403, 'User found in LDAP but not in application')
};
}
return {
userId: userId,
token: stampedToken.token
};
}
});