diff options
author | Elliot Kroo <elliot.kroo@gmail.com> | 2010-01-23 10:41:05 -0800 |
---|---|---|
committer | Elliot Kroo <elliot.kroo@gmail.com> | 2010-01-23 10:41:05 -0800 |
commit | c1894c8e0a52f4e3d2f89fa92f0066bbf0fcf1b1 (patch) | |
tree | 101119747907025bcef1ad10be970bad1d892bfb | |
parent | 0b2d5baa407153d20c7eb08fb4ff9960c1cf13c9 (diff) | |
download | etherpad-c1894c8e0a52f4e3d2f89fa92f0066bbf0fcf1b1.tar.gz etherpad-c1894c8e0a52f4e3d2f89fa92f0066bbf0fcf1b1.tar.xz etherpad-c1894c8e0a52f4e3d2f89fa92f0066bbf0fcf1b1.zip |
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 ;)
Diffstat (limited to '')
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/pro_accounts.js | 100 | ||||
-rw-r--r-- | trunk/etherpad/src/etherpad/pro/pro_ldap_support.js | 217 | ||||
-rw-r--r-- | trunk/infrastructure/framework-src/modules/process.js | 91 |
3 files changed, 403 insertions, 5 deletions
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<attr.size(); i++) { + resultobj[id].push(attr.get(i).toString()); + } + } + } catch (e) { + // naming error + return {'keys': e} + } + + return resultobj; +} + +LDAP.ldapSingleton = false; + +// load in ldap configuration from a file... +function readLdapConfig(file) { + var fileContents = BetterFile.getFileContents(file); + + if(fileContents == null) + return "File not found."; + + var configObject = fastJSON.parse(fileContents); + if(configObject['ldapSuffix']) { + LDAP.ldapSuffix = configObject['ldapSuffix']; + } + return configObject; +} + +// Sample Configuration file: +// { +// "userClass" : "person", +// "url" : "ldap://localhost:10389", +// "principal" : "uid=admin,ou=system", +// "password" : "secret", +// "rootPath" : "ou=users,ou=system", +// "nameAttribute": "displayname", +// "ldapSuffix" : "@ldap" +// } + +// appjet.config['etherpad.useLdapConfiguration'] = "/Users/kroo/Documents/Projects/active/AppJet/ldapConfig.json"; +function getLDAP() { + if (! LDAP.ldapSingleton && + appjet.config['etherpad.useLdapConfiguration']) { + var config = readLdapConfig(appjet.config['etherpad.useLdapConfiguration']); + var error = null; + if(!config) { + config = null; + error = "Error reading LDAP configuration file." + } + LDAP.ldapSingleton = new LDAP(config, error); + } + + return LDAP.ldapSingleton; +}
\ No newline at end of file diff --git a/trunk/infrastructure/framework-src/modules/process.js b/trunk/infrastructure/framework-src/modules/process.js new file mode 100644 index 0000000..48ab62e --- /dev/null +++ b/trunk/infrastructure/framework-src/modules/process.js @@ -0,0 +1,91 @@ +/** + * Simple way to execute external commands through javascript + * + * @example + cmd = exec("cat"); + System.out.println("First: " +cmd.write("this is a loop.").read(Process.READ_AVAILABLE)); // prints "this is a loop." + System.out.println("Second: " +cmd.writeAndClose(" hi there").result()); // prints "this is a loop. hi there" + * + */ + +jimport("java.lang.Runtime"); +jimport("java.io.BufferedInputStream"); +jimport("java.io.BufferedOutputStream"); +jimport("java.lang.System"); + +/* returns a process */ +function exec(process) { + return new Process(process); +}; + +function Process(cmd) { + this.cmd = cmd; + this.proc = Runtime.getRuntime().exec(cmd); + this.resultText = ""; + this.inputStream = new BufferedInputStream(this.proc.getInputStream()); + this.errorStream = new BufferedInputStream(this.proc.getErrorStream()); + this.outputStream = new BufferedOutputStream(this.proc.getOutputStream()); +} + +Process.CHUNK_SIZE = 1024; +Process.READ_ALL = -1; +Process.READ_AVAILABLE = -2; + +Process.prototype.write = function(stdinText) { + this.outputStream.write(new java.lang.String(stdinText).getBytes()); + this.outputStream.flush(); + return this; +}; + +Process.prototype.writeAndClose = function(stdinText) { + this.write(stdinText); + this.outputStream.close(); + return this; +}; + +/* Python file-like behavior: read specified number of bytes, else until EOF*/ +Process.prototype.read = function(nbytesToRead, stream) { + var inputStream = stream || this.inputStream; + var availBytes = inputStream.available(); + if (!availBytes) return null; + + var result = ""; + var nbytes = nbytesToRead || Process.READ_ALL; + var readAll = (nbytes == Process.READ_ALL); + var readAvailable = (nbytes == Process.READ_AVAILABLE); + while (nbytes > 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 || ""; +}; |