Move LDAP to being an import and refactor the userQuota update
This commit is contained in:
1 parent
7a2b03a7a1
commit
614c85ae4b
8 files changed
+287
-250
No files matched your search
+1
-1
@@ -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']
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -47,5 +47,3 @@ SandstormDb.periodicCleanup(24 * 60 * 60 * 1000, () => {
|
||||
});
|
||||
|
||||
Meteor.startup(() => { globalDb.migrateToLatest(); });
|
||||
LDAP_DEFAULTS.url = globalDb.getLdapUrl();
|
||||
LDAP_DEFAULTS.base = globalDb.getLdapBase();
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
@@ -33,6 +33,7 @@ const publicAdminSettings = [
|
||||
"whitelabelHideTroubleshooting",
|
||||
"whitelabelUseServerTitleForHomeText",
|
||||
"quotaEnabled",
|
||||
"quotaLdapEnabled",
|
||||
"billingPromptUrl",
|
||||
];
|
||||
|
||||
|
||||
Reference in new issue
Block a user