From c1894c8e0a52f4e3d2f89fa92f0066bbf0fcf1b1 Mon Sep 17 00:00:00 2001 From: Elliot Kroo Date: Sat, 23 Jan 2010 10:41:05 -0800 Subject: Applied LDAP patches See http://bit.ly/5BTvub for details. This patch provides two large changes of note. The first of which provides a general purpose library for external process execution (process.js), and the second of which provides LDAP and SSO authentication through a fairly simple LDAP library (pro_ldap_support.js, and pro_accounts.js). Patches and harsh unwarranted criticism welcome ;) --- trunk/etherpad/src/etherpad/pro/pro_accounts.js | 100 +++++++++- .../etherpad/src/etherpad/pro/pro_ldap_support.js | 217 +++++++++++++++++++++ .../framework-src/modules/process.js | 91 +++++++++ 3 files changed, 403 insertions(+), 5 deletions(-) create mode 100644 trunk/etherpad/src/etherpad/pro/pro_ldap_support.js create mode 100644 trunk/infrastructure/framework-src/modules/process.js (limited to 'trunk') diff --git a/trunk/etherpad/src/etherpad/pro/pro_accounts.js b/trunk/etherpad/src/etherpad/pro/pro_accounts.js index dff4846..cdecd0d 100644 --- a/trunk/etherpad/src/etherpad/pro/pro_accounts.js +++ b/trunk/etherpad/src/etherpad/pro/pro_accounts.js @@ -30,11 +30,15 @@ import("etherpad.utils.*"); import("etherpad.pro.domains"); import("etherpad.control.pro.account_control"); import("etherpad.pro.pro_utils"); +import("etherpad.pro.pro_ldap_support.*"); import("etherpad.pro.pro_quotas"); import("etherpad.pad.padusers"); import("etherpad.log"); import("etherpad.billing.team_billing"); +import("process.*"); +import("fastJSON") + jimport("org.mindrot.BCrypt"); jimport("java.lang.System.out.println"); @@ -82,18 +86,23 @@ function validateEmailDomainPair(email, domainId) { } /* if domainId is null, then use domainId of current request. */ -function createNewAccount(domainId, fullName, email, password, isAdmin) { +function createNewAccount(domainId, fullName, email, password, isAdmin, skipValidation) { if (!domainId) { domainId = domains.getRequestDomainId(); } + if (!skipValidation) { + skipValidation = false; + } email = trim(email); isAdmin = !!isAdmin; // convert to bool // validation - var e; - e = validateEmail(email); if (e) { throw Error(e); } - e = validateFullName(fullName); if (e) { throw Error(e); } - e = validatePassword(password); if (e) { throw Error(e); } + if (!skipValidation) { + var e; + e = validateEmail(email); if (e) { throw Error(e); } + e = validateFullName(fullName); if (e) { throw Error(e); } + e = validatePassword(password); if (e) { throw Error(e); } + } // xss normalization fullName = toHTML(fullName); @@ -212,6 +221,27 @@ function doesAdminExist() { }); } +function attemptSingleSignOn() { + if(!appjet.config['etherpad.SSOScript']) return null; + + // pass request.cookies to a small user script + var file = appjet.config['etherpad.SSOScript']; + + var cmd = exec(file); + + // note that this will block until script execution returns + var result = cmd.write(fastJSON.stringify(request.cookies)).result(); + var val = false; + + // we try to parse the result as a JSON string, if not, return null. + try { + if(!!(val=fastJSON.parse(result))) { + return val; + } + } catch(e) {} + return null; +} + function getSessionProAccount() { if (sessions.isAnEtherpadAdmin()) { return getEtherpadAdminAccount(); @@ -231,6 +261,25 @@ function isAccountSignedIn() { if (getSessionProAccount()) { return true; } else { + // if the user is not signed in, check to see if he should be signed in + // by calling an external script. + if(appjet.config['etherpad.SSOScript']) { + var ssoResult = attemptSingleSignOn(); + if(ssoResult && ('email' in ssoResult)) { + var user = getAccountByEmail(ssoResult['email']); + if (!user) { + var email = ssoResult['email']; + var pass = ssoResult['password'] || ""; + var name = ssoResult['fullname'] || "unnamed"; + createNewAccount(null, name, email, pass, false, true); + user = getAccountByEmail(email, null); + } + + signInSession(user); + return true; + } + } + return false; } } @@ -289,6 +338,47 @@ function requireAdminAccount() { /* returns undefined on success, error string otherise. */ function authenticateSignIn(email, password) { + // blank passwords are not allowed to sign in. + if (password == "") return "Please provide a password."; + + // If the email ends with our ldap suffix... + var isLdapSuffix = getLDAP() && getLDAP().isLDAPSuffix(email); + + if(isLdapSuffix && !getLDAP()) { + return "LDAP not yet configured. Please contact your system admininstrator."; + } + + // if there is an error in the LDAP configuration, return the error message + if(getLDAP() && getLDAP().error) { + return getLDAP().error + " Please contact your system administrator."; + } + + if(isLdapSuffix && getLDAP()) { + var ldapuser = email.substr(0, email.indexOf(getLDAP().getLDAPSuffix())); + var ldapResult = getLDAP().login(ldapuser, password); + + if (ldapResult.error == true) { + return ldapResult.message + ""; + } + + var accountRecord = getAccountByEmail(email, null); + + // if this is the first time this user has logged in, create a user + // for him/her + if (!accountRecord) { + // password to store in database -- a blank password means the user + // cannot authenticate normally (e.g. must go through SSO or LDAP) + var ldapPass = ""; + + // create a new user (skipping validation of email/users/passes) + createNewAccount(null, ldapResult.getFullName(), email, ldapPass, false, true); + accountRecord = getAccountByEmail(email, null); + } + + signInSession(accountRecord); + return undefined; // success + } + var accountRecord = getAccountByEmail(email, null); if (!accountRecord) { return "Account not found: "+email; diff --git a/trunk/etherpad/src/etherpad/pro/pro_ldap_support.js b/trunk/etherpad/src/etherpad/pro/pro_ldap_support.js new file mode 100644 index 0000000..a657af1 --- /dev/null +++ b/trunk/etherpad/src/etherpad/pro/pro_ldap_support.js @@ -0,0 +1,217 @@ +import("fastJSON"); + +jimport("net.appjet.common.util.BetterFile") + +jimport("java.lang.System.out.println"); +jimport("javax.naming.directory.DirContext"); +jimport("javax.naming.directory.SearchControls"); +jimport("javax.naming.directory.InitialDirContext"); +jimport("javax.naming.directory.SearchResult"); +jimport("javax.naming.NamingEnumeration"); +jimport("javax.naming.Context"); +jimport("java.util.Hashtable"); + +function LDAP(config, errortext) { + if(!config) + this.error = errortext; + else + this.error = false; + + this.ldapConfig = config; +} + +function _dmesg(m) { + // if (!isProduction()) { + println(new String(m)); + // } +} + +/** + * an ldap result object + * + * will either have error = true, with a corrisponding error message, + * or will have error = false, with a corrisponding results object message + */ +function LDAPResult(msg, error, ldap) { + if(!ldap) ldap = getLDAP(); + if(!error) error = false; + this.message = msg; + this.ldap = ldap; + this.error = error; +} + +/** + * returns the full name attribute, as specified by the 'nameAttribute' config + * value. + */ +LDAPResult.prototype.getFullName = function() { + return this.message[this.ldap.ldapConfig['nameAttribute']][0]; +} + +/** + * Handy function for creating an LDAPResult object + */ +function ldapMessage(success, msg) { + var message = msg; + if(typeof(msg) == String) { + message = "LDAP " + + (success ? "Success" : "Error") + ": " + msg; + } + + var result = new LDAPResult(message); + result.error = !success; + return result; +} + +// returns the associated ldap results object, with an error flag of false +var ldapSuccess = + function(msg) { return ldapMessage.apply(this, [true, msg]); }; + +// returns a helpful error message +var ldapError = + function(msg) { return ldapMessage.apply(this, [false, msg]); }; + +/* build an LDAP Query (searches for an objectClass and uid) */ +LDAP.prototype.buildLDAPQuery = function(queryUser) { + if(queryUser && queryUser.match(/[\w_-]+/)) { + return "(&(objectClass=" + + this.ldapConfig['userClass'] + ")(uid=" + + queryUser + "))" + } else return null; +} + +LDAP.prototype.login = function(queryUser, queryPass) { + var query = this.buildLDAPQuery(queryUser); + if(!query) { return ldapError("invalid LDAP username"); } + + try { + var context = LDAP.authenticate(this.ldapConfig['url'], + this.ldapConfig['principal'], + this.ldapConfig['password']); + + if(!context) { + return ldapError("could not authenticate principle user."); + } + + var ctrl = new SearchControls(); + ctrl.setSearchScope(SearchControls.SUBTREE_SCOPE); + var results = context.search(this.ldapConfig['rootPath'], query, ctrl); + + // if the user is found + if(results.hasMore()) { + var result = results.next(); + + // grab the absolute path to the user + var userResult = result.getNameInNamespace(); + var authed = !!LDAP.authenticate(this.ldapConfig['url'], + userResult, + queryPass) + + // return the LDAP info on the user upon success + return authed ? + ldapSuccess(LDAP.parse(result)) : + ldapError("Incorrect password. Please try again."); + } else { + return ldapError("User "+queryUser+" not found in LDAP."); + } + + // if there are errors in the search, log them and return "unknown error" + } catch (e) { + _dmesg(e); + return ldapError(new String(e)) + } +}; + +LDAP.prototype.isLDAPSuffix = function(email) { + return email.indexOf(this.ldapConfig['ldapSuffix']) == + (email.length-this.ldapConfig['ldapSuffix'].length); +} + +LDAP.prototype.getLDAPSuffix = function() { + return this.ldapConfig['ldapSuffix']; +} + +/* static function returns a DirContext, or undefined upon authentation err */ +LDAP.authenticate = function(url, user, pass) { + var context = null; + try { + var env = new Hashtable(); + env.put(Context.INITIAL_CONTEXT_FACTORY, + "com.sun.jndi.ldap.LdapCtxFactory"); + env.put( Context.SECURITY_PRINCIPAL, user ); + env.put( Context.SECURITY_CREDENTIALS, pass ); + env.put(Context.PROVIDER_URL, url); + context = new InitialDirContext(env); + } catch (e) { + // bind failed. + } + return context; +} + +/* turn a res */ +LDAP.parse = function(result) { + var resultobj = {}; + try { + var attrs = result.getAttributes(); + var ids = attrs.getIDs(); + + while(ids.hasMore()) { + var id = ids.next().toString(); + resultobj[id] = []; + + var attr = attrs.get(id); + + for(var i=0; i 0 || readAll || readAvailable) { + var chunkSize = readAll ? Process.CHUNK_SIZE : + readAvailable ? Process.CHUNK_SIZE : nbytes; + + // allocate a java byte array + var bytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, chunkSize); + + var len = inputStream.read(bytes, 0, chunkSize); + + // at end of stream, or when we run out of data, stop reading in chunks. + if (len == -1) break; + if (nbytes > 0) nbytes -= len; + + result += new java.lang.String(bytes); + + if (readAvailable && inputStream.available() == 0) break; + } + + this.resultText += new String(result); + return new String(result); +}; + +Process.prototype.result = function() { + this.outputStream.close(); + this.proc.waitFor(); + this.read(Process.READ_ALL, this.inputStream); + return new String(this.resultText); +}; + +Process.prototype.resultOrError = function() { + this.proc.waitFor(); + this.read(Process.READ_ALL, this.inputStream); + var result = this.resultText; + if(!result || result == "") result = this.read(Process.READ_ALL, this.errorStream); + return result || ""; +}; -- cgit v1.2.3-1-g7c22