-
-
Save kroo/10061b4b213619816db5 to your computer and use it in GitHub Desktop.
Patch to etherpad to enable ldap and single-sign-on support
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff -r 78a2f7962089 trunk/etherpad/src/etherpad/pro/pro_accounts.js | |
--- a/trunk/etherpad/src/etherpad/pro/pro_accounts.js Tue Dec 22 14:51:36 2009 -0500 | |
+++ b/trunk/etherpad/src/etherpad/pro/pro_accounts.js Mon Jan 04 07:34:56 2010 -0800 | |
@@ -30,11 +30,15 @@ | |
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 @@ | |
} | |
/* 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 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 @@ | |
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 @@ | |
/* 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 -r 78a2f7962089 trunk/etherpad/src/etherpad/pro/pro_ldap_support.js | |
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 | |
+++ b/trunk/etherpad/src/etherpad/pro/pro_ldap_support.js Mon Jan 04 07:34:56 2010 -0800 | |
@@ -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 -r 78a2f7962089 trunk/infrastructure/framework-src/modules/process.js | |
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 | |
+++ b/trunk/infrastructure/framework-src/modules/process.js Mon Jan 04 07:34:56 2010 -0800 | |
@@ -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 || ""; | |
+}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment