Move LDAP to being an import and refactor the userQuota update

This commit is contained in:
Jason Paryani committed 2016-08-12 16:45:55 -07:00
1 parent 7a2b03a7a1
commit 614c85ae4b
8 files changed
+287 -250

No files matched your search

+1 -1
View File
@@ -8,5 +8,5 @@
"requireShorthandArrowFunctions": false,
"disallowQuotedKeysInObjects": false, # https://github.com/jscs-dev/node-jscs/issues/2183
"disallowEmptyBlocks": { "allExcept": [ "comments" ] },
"excludeFiles": ['.meteor', '**/node_modules', 'public/*', '**/*.css']
"excludeFiles": ['.meteor', '**/node_modules', 'public/*', '**/*.css', 'imports/db-deprecated.js']
}
+21 -1
View File
@@ -23,7 +23,27 @@
// the `globalDb` global should NOT go with it; the package should expect a SandstormDb to be
// passed in, thus allowing mocking the database for unit tests.
globalDb = new SandstormDb();
let quotaManager;
if (Meteor.isServer) {
import { LDAP } from "/imports/server/accounts/ldap.js";
// Imports are usually not allowed to occur in a block. However, it is the only way to do
// this under Meteor. Using // jscs:disable doesn't work for what it considers syntax violations,
// and so we've added this file to .jscsrc's excludedFiles explicitly.
quotaManager = new LDAP();
} else {
quotaManager = {
updateUserQuota(db, user) {
return {
storage: user.cachedStorageQuota || 0,
grains: Infinity,
compute: Infinity,
};
},
};
}
globalDb = new SandstormDb(quotaManager);
Packages = globalDb.collections.packages;
DevPackages = globalDb.collections.devPackages;
+233
View File
@@ -0,0 +1,233 @@
const Future = Npm.require("fibers/future");
import ldapjs from "ldapjs";
// 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,
searchDN: false,
searchCredentials: false,
createNewUser: true,
defaultDomain: false,
searchResultsProfileMap: false,
base: null,
search: "(objectclass=*)",
ldapsCertificate: false,
};
/**
@class LDAP
@constructor
*/
function LDAP() {
// Set options
this.options = _.clone(LDAP_DEFAULTS);
};
/**
* 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.
* Additionally the searchBeforeBind parameter can be specified, which is used to search for the DN
* if not provided.
*/
LDAP.prototype.ldapCheck = function (db, options) {
let _this = this;
options = options || {};
if ((options.hasOwnProperty("username") && options.hasOwnProperty("ldapPass")) ||
options.hasOwnProperty("searchUsername")) {
_this.options.base = db.getLdapBase();
_this.options.url = db.getLdapUrl();
_this.options.searchBeforeBind = {};
_this.options.searchBeforeBind[db.getLdapSearchUsername()] = options.searchUsername ||
options.username;
_this.options.filter = db.getLdapFilter() || "(objectclass=*)";
_this.options.searchBindDn = db.getLdapSearchBindDn();
_this.options.searchBindPassword = db.getLdapSearchBindPassword();
let resolved = false;
let ldapAsyncFut = new Future();
// Create ldap client
let fullUrl = db.getLdapUrl();
let client = null;
let errorFunc = function (err) {
if (err) {
if (resolved) return;
resolved = true;
ldapAsyncFut.return({
error: err,
});
}
};
if (fullUrl.indexOf("ldaps://") === 0) {
client = ldapjs.createClient({
url: fullUrl,
tlsOptions: {
ca: [_this.options.ldapsCertificate],
},
}, errorFunc);
} else {
client = ldapjs.createClient({
url: fullUrl,
}, errorFunc);
}
client.on("error", errorFunc);
let username = options.username;
let domain = _this.options.defaultDomain;
if (!options.hasOwnProperty("searchUsername")) {
// Slide @xyz.whatever from username if it was passed in
// and replace it with the domain specified in defaults
let emailSliceIndex = options.username.indexOf("@");
// 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;
}
}
if (_this.options.searchBindDn) {
let ldapBindFut = new Future();
client.bind(_this.options.searchBindDn, _this.options.searchBindPassword,
function (err) {
if (err) {
ldapBindFut.throw(err);
} else {
ldapBindFut.return();
}
}
);
try {
ldapBindFut.wait();
} catch (err) {
return { error: err };
}
}
// initialize result
let retObject = {
username: username,
email: domain ? username + "@" + domain : false,
emptySearch: true,
searchResults: {},
};
let filter = _this.options.filter;
Object.keys(_this.options.searchBeforeBind).forEach(function (searchKey) {
filter = "&" + filter + "(" + searchKey + "=" + _this.options.searchBeforeBind[searchKey] + ")";
});
let searchOptions = {
scope: "sub",
sizeLimit: 1,
filter: filter,
};
// perform LDAP search to determine DN
client.search(_this.options.base, searchOptions, function (err, res) {
if (err) {
if (resolved) return;
resolved = true;
ldapAsyncFut.return({
error: err,
});
return;
}
retObject.emptySearch = true;
res.on("searchEntry", function (entry) {
retObject.dn = entry.objectName;
retObject.username = retObject.dn;
retObject.emptySearch = false;
retObject.searchResults = _.omit(entry.object, "userPassword");
if (options.hasOwnProperty("searchUsername")) {
// This was only a search, return immediately
resolved = true;
ldapAsyncFut.return(retObject);
return;
}
// use the determined DN to bind
client.bind(entry.objectName, options.ldapPass, function (err) {
try {
if (err) {
throw new Meteor.Error(err.code, err.message);
} else {
resolved = true;
ldapAsyncFut.return(retObject);
}
}
catch (e) {
if (resolved) return;
resolved = true;
ldapAsyncFut.return({
error: e,
});
}
});
});
res.on("error", errorFunc);
// If no dn is found, return as is.
res.on("end", function (result) {
if (retObject.dn === undefined) {
resolved = true;
ldapAsyncFut.return(retObject);
}
});
});
return ldapAsyncFut.wait();
} else {
throw new Meteor.Error(403, "Missing LDAP Auth Parameter");
}
};
LDAP.prototype.updateUserQuota = function (db, user) {
const fallback = user.cachedStorageQuota || 0;
const setting = db.collections.settings.findOne({ _id: "quotaLdapAttribute" });
if (!setting) return fallback;
// TODO(someday): don't just assume the first login identity is the primary identity?
const email = db.getPrimaryEmail(user._id, user.loginIdentities[0].id);
if (!email) return fallback;
const ldapUser = this.ldapCheck(db, { searchUsername: email, });
if (!ldapUser || !ldapUser.searchResults) return fallback;
const newStorageQuota = +ldapUser.searchResults[setting.value];
Meteor.users.update({ _id: user._id }, { $set: { cachedStorageQuota: newStorageQuota } });
// TODO(someday): cache timestamp as well and only check/update if greater than 60s ago
return {
storage: newStorageQuota || fallback,
grains: Infinity,
compute: Infinity,
};
};
export { LDAP };
+22 -38
View File
@@ -937,7 +937,17 @@ roleAssignmentPattern = {
removePermissions: Match.Optional([Boolean]),
};
SandstormDb = function () {
SandstormDb = function (quotaManager) {
// quotaManager is an object with the following method:
// updateUserQuota: It is provided two arguments
// db: This SandstormDb object
// user: A collections.users account object
// and returns a quota object:
// storage: A number (can be Infinity)
// compute: A number (can be Infinity)
// grains: A number (can be Infinity)
this.quotaManager = quotaManager;
this.collections = {
// Direct access to underlying collections. DEPRECATED.
//
@@ -1789,31 +1799,21 @@ _.extend(SandstormDb.prototype, {
return setting && setting.value && this.isFeatureKeyValid();
},
updateUserQuotaFromLdap: function (user) {
const fallback = user.cachedStorageQuota || 0;
if (Meteor.isClient) return fallback;
isQuotaLdapEnabled: function () {
const setting = this.collections.settings.findOne({ _id: "quotaLdapEnabled" });
return setting && setting.value && this.isFeatureKeyValid();
},
const setting = this.collections.settings.findOne({ _id: "quotaLdapAttribute" });
if (!setting) return fallback;
const ldap = new LDAP();
// TODO(someday): don't just assume the first login identity is the primary identity?
const email = this.getPrimaryEmail(user._id, user.loginIdentities[0].id);
if (!email) return fallback;
const ldapUser = ldap.ldapCheck(this, { searchUsername: email, });
if (!ldapUser || !ldapUser.searchResults) return fallback;
const newStorageQuota = +ldapUser.searchResults[setting.value];
Meteor.users.update({ _id: user._id }, { $set: { cachedStorageQuota: newStorageQuota } });
// TODO(someday): cache timestamp as well and only check/update if greater than 60s ago
return newStorageQuota || fallback;
updateUserQuota: function (user) {
if (this.quotaManager) {
return this.quotaManager.updateUserQuota(this, user);
}
},
getUserQuota: function (user) {
if (this.isReferralEnabled()) {
// TODO(someday): isReferralEnabled isn't quite the right check
if (this.isQuotaLdapEnabled()) {
return this.quotaManager.updateUserQuota(this, user);
} else {
const plan = Plans.findOne(user.plan || "free");
const referralBonus = calculateReferralBonus(user);
const bonus = user.planBonus || {};
@@ -1823,13 +1823,6 @@ _.extend(SandstormDb.prototype, {
compute: plan.compute + (bonus.compute || 0),
};
return userQuota;
} else {
const ldapQuota = this.updateUserQuotaFromLdap(user);
return {
storage: ldapQuota,
grains: Infinity,
compute: Infinity,
};
}
},
@@ -2617,12 +2610,3 @@ Meteor.methods({
}
},
});
if (Meteor.isServer) {
Meteor.methods({
updateQuota() {
return this.connection.sandstormDb.updateUserQuotaFromLdap(Meteor.user());
// This is a no-op if settings aren't enabled
},
});
}
+1 -1
View File
@@ -199,7 +199,7 @@ Accounts.validateLoginAttempt(function (attempt) {
throw new Meteor.Error(403, "User not in organization.");
}
db.updateUserQuotaFromLdap(user); // This is a no-op if settings aren't enabled
db.updateUserQuota(user); // This is a no-op if settings aren't enabled
} else {
if (db.getOrganizationDisallowGuests() &&
!db.isIdentityInOrganization(user)) {
-2
View File
@@ -47,5 +47,3 @@ SandstormDb.periodicCleanup(24 * 60 * 60 * 1000, () => {
});
Meteor.startup(() => { globalDb.migrateToLatest(); });
LDAP_DEFAULTS.url = globalDb.getLdapUrl();
LDAP_DEFAULTS.base = globalDb.getLdapBase();
+8 -207
View File
@@ -1,210 +1,4 @@
const Future = Npm.require("fibers/future");
import ldapjs from "ldapjs";
// 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,
searchDN: false,
searchCredentials: false,
createNewUser: true,
defaultDomain: false,
searchResultsProfileMap: false,
base: null,
search: "(objectclass=*)",
ldapsCertificate: false,
};
/**
@class LDAP
@constructor
*/
LDAP = function () {
// Set options
this.options = _.clone(LDAP_DEFAULTS);
};
/**
* 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.
* Additionally the searchBeforeBind parameter can be specified, which is used to search for the DN
* if not provided.
*/
LDAP.prototype.ldapCheck = function (db, options) {
let _this = this;
options = options || {};
if ((options.hasOwnProperty("username") && options.hasOwnProperty("ldapPass")) ||
options.hasOwnProperty("searchUsername")) {
_this.options.base = db.getLdapBase();
_this.options.searchBeforeBind = {};
_this.options.searchBeforeBind[db.getLdapSearchUsername()] = options.searchUsername ||
options.username;
_this.options.filter = db.getLdapFilter() || "(objectclass=*)";
_this.options.searchBindDn = db.getLdapSearchBindDn();
_this.options.searchBindPassword = db.getLdapSearchBindPassword();
let resolved = false;
let ldapAsyncFut = new Future();
// Create ldap client
let fullUrl = db.getLdapUrl();
let client = null;
let errorFunc = function (err) {
if (err) {
if (resolved) return;
resolved = true;
ldapAsyncFut.return({
error: err,
});
}
};
if (fullUrl.indexOf("ldaps://") === 0) {
client = ldapjs.createClient({
url: fullUrl,
tlsOptions: {
ca: [_this.options.ldapsCertificate],
},
}, errorFunc);
} else {
client = ldapjs.createClient({
url: fullUrl,
}, errorFunc);
}
client.on("error", errorFunc);
let username = options.username;
let domain = _this.options.defaultDomain;
if (!options.hasOwnProperty("searchUsername")) {
// Slide @xyz.whatever from username if it was passed in
// and replace it with the domain specified in defaults
let emailSliceIndex = options.username.indexOf("@");
// 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;
}
}
if (_this.options.searchBindDn) {
let ldapBindFut = new Future();
client.bind(_this.options.searchBindDn, _this.options.searchBindPassword,
function (err) {
if (err) {
ldapBindFut.throw(err);
} else {
ldapBindFut.return();
}
}
);
try {
ldapBindFut.wait();
} catch (err) {
return { error: err };
}
}
// initialize result
let retObject = {
username: username,
email: domain ? username + "@" + domain : false,
emptySearch: true,
searchResults: {},
};
let filter = _this.options.filter;
Object.keys(_this.options.searchBeforeBind).forEach(function (searchKey) {
filter = "&" + filter + "(" + searchKey + "=" + _this.options.searchBeforeBind[searchKey] + ")";
});
let searchOptions = {
scope: "sub",
sizeLimit: 1,
filter: filter,
};
// perform LDAP search to determine DN
client.search(_this.options.base, searchOptions, function (err, res) {
if (err) {
if (resolved) return;
resolved = true;
ldapAsyncFut.return({
error: err,
});
return;
}
retObject.emptySearch = true;
res.on("searchEntry", function (entry) {
retObject.dn = entry.objectName;
retObject.username = retObject.dn;
retObject.emptySearch = false;
retObject.searchResults = _.omit(entry.object, "userPassword");
if (options.hasOwnProperty("searchUsername")) {
// This was only a search, return immediately
resolved = true;
ldapAsyncFut.return(retObject);
return;
}
// use the determined DN to bind
client.bind(entry.objectName, options.ldapPass, function (err) {
try {
if (err) {
throw new Meteor.Error(err.code, err.message);
} else {
resolved = true;
ldapAsyncFut.return(retObject);
}
}
catch (e) {
if (resolved) return;
resolved = true;
ldapAsyncFut.return({
error: e,
});
}
});
});
res.on("error", errorFunc);
// If no dn is found, return as is.
res.on("end", function (result) {
if (retObject.dn === undefined) {
resolved = true;
ldapAsyncFut.return(retObject);
}
});
});
return ldapAsyncFut.wait();
} else {
throw new Meteor.Error(403, "Missing LDAP Auth Parameter");
}
};
import { LDAP } from "/imports/server/accounts/ldap.js";
// Register login handler with Meteor
Accounts.registerLoginHandler("ldap", function (loginRequest) {
@@ -248,3 +42,10 @@ Accounts.registerLoginHandler("ldap", function (loginRequest) {
}
});
Meteor.methods({
updateQuota() {
return this.connection.sandstormDb.updateUserQuota(Meteor.user());
// This is a no-op if settings aren't enabled
},
});
+1
View File
@@ -33,6 +33,7 @@ const publicAdminSettings = [
"whitelabelHideTroubleshooting",
"whitelabelUseServerTitleForHomeText",
"quotaEnabled",
"quotaLdapEnabled",
"billingPromptUrl",
];