Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LDAP Support #220

Merged
merged 2 commits into from
Jun 24, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ rocketchat:file
rocketchat:highlight
#rocketchat:hubot
#rocketchat:irc
rocketchat:ldap
rocketchat:lib
rocketchat:markdown
rocketchat:me
Expand All @@ -56,4 +57,4 @@ tmeasday:crypto-md5
tmeasday:errors
todda00:friendly-slugs
underscorestring:underscore.string
yasaricli:slugify
yasaricli:slugify
1 change: 1 addition & 0 deletions .meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ rocketchat:autolinker@0.0.1
rocketchat:emojione@0.0.1
rocketchat:file@0.0.1
rocketchat:highlight@0.0.1
rocketchat:ldap@0.0.1
rocketchat:lib@0.0.1
rocketchat:markdown@0.0.1
rocketchat:me@0.0.1
Expand Down
1 change: 1 addition & 0 deletions packages/rocketchat-ldap/.npm/package/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
7 changes: 7 additions & 0 deletions packages/rocketchat-ldap/.npm/package/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
This directory and the files immediately inside it are automatically generated
when you change this package's NPM dependencies. Commit the files in this
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
so that others run the same versions of sub-dependencies.

You should NOT check in the node_modules directory that Meteor automatically
creates; if you are using git, the .gitignore file tells git to ignore it.
69 changes: 69 additions & 0 deletions packages/rocketchat-ldap/.npm/package/npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions packages/rocketchat-ldap/ldap_client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Pass in username, password as normal
// customLdapOptions should be passed in if you want to override LDAP_DEFAULTS
// on any particular call (if you have multiple ldap servers you'd like to connect to)
// You'll likely want to set the dn value here {dn: "..."}
Meteor.loginWithLDAP = function(user, password, customLdapOptions, callback) {
// Retrieve arguments as array
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
// Pull username and password
user = args.shift();
password = args.shift();

// Check if last argument is a function
// if it is, pop it off and set callback to it
if (typeof args[args.length-1] == 'function') callback = args.pop(); else callback = null;

// if args still holds options item, grab it
if (args.length > 0) customLdapOptions = args.shift(); else customLdapOptions = {};

// Set up loginRequest object
var loginRequest = _.defaults({
username: user,
ldapPass: password
}, {
ldap: true,
ldapOptions: customLdapOptions
});

Accounts.callLoginMethod({
// Call login method with ldap = true
// This will hook into our login handler for ldap
methodArguments: [loginRequest],
userCallback: function(error, result) {
if (error) {
callback && callback(error);
} else {
callback && callback();
}
}
});
}
224 changes: 224 additions & 0 deletions packages/rocketchat-ldap/ldap_server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
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,
createNewUser: true,
defaultDomain: false,
searchResultsProfileMap: 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
*/
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 = 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;
}


//Attempt to bind to ldap server with provided info
client.bind(self.options.dn, options.ldapPass, function(err) {
try {
if (err) {
// Bind failure, return error
throw new Meteor.Error(err.code, err.message);
} else {
// Bind auth successful
// Create return object
var retObject = {
username: username,
searchResults: null
};
// Set email on return object
retObject.email = domain ? username + '@' + domain : false;

// Return search results if specified
if (self.options.searchResultsProfileMap) {
client.search(self.options.dn, {}, function(err, res) {

res.on('searchEntry', function(entry) {
// Add entry results to return object
retObject.searchResults = entry.object;

ldapAsyncFut.return(retObject);
});

});
}
// No search results specified, return username and email object
else {
ldapAsyncFut.return(retObject);
}
}
} catch (e) {
ldapAsyncFut.return({
error: e
});
}
});

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 {
// 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
return {
userId: null,
error: "LDAP Authentication succeded, but no user exists in Mongo. Either create a user for this email or set LDAP_DEFAULTS.createNewUser to true"
};
}

return {
userId: userId,
token: stampedToken.token
};
}

return undefined;
});
1 change: 1 addition & 0 deletions packages/rocketchat-ldap/lib/ldapjs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MeteorWrapperLdapjs = Npm.require('ldapjs');
Loading