aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorElliot Kroo <elliot.kroo@gmail.com>2010-01-23 10:41:05 -0800
committerElliot Kroo <elliot.kroo@gmail.com>2010-01-23 10:41:05 -0800
commitc1894c8e0a52f4e3d2f89fa92f0066bbf0fcf1b1 (patch)
tree101119747907025bcef1ad10be970bad1d892bfb
parent0b2d5baa407153d20c7eb08fb4ff9960c1cf13c9 (diff)
downloadetherpad-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.js100
-rw-r--r--trunk/etherpad/src/etherpad/pro/pro_ldap_support.js217
-rw-r--r--trunk/infrastructure/framework-src/modules/process.js91
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 || "";
+};