happn-3
Version:
pub/sub api as a service using primus and mongo & redis or nedb, can work as cluster, single process or embedded using nedb
413 lines (353 loc) • 13.3 kB
JavaScript
var Promise = require('bluebird');
const CONSTANTS = require('../..').constants;
function SecurityUsers(opts) {
this.log = opts.logger.createLogger('SecurityUsers');
this.log.$$TRACE('construct(%j)', opts);
this.opts = opts;
}
SecurityUsers.prototype.initialize = Promise.promisify(initialize);
SecurityUsers.prototype.clearCaches = clearCaches;
SecurityUsers.prototype.__validate = __validate;
SecurityUsers.prototype.getPasswordHash = getPasswordHash;
SecurityUsers.prototype.upsertUser = Promise.promisify(upsertUser);
SecurityUsers.prototype.__upsertUser = __upsertUser;
SecurityUsers.prototype.deleteUser = Promise.promisify(deleteUser);
SecurityUsers.prototype.getUser = Promise.promisify(getUser);
SecurityUsers.prototype.getUserNoGroups = getUserNoGroups;
SecurityUsers.prototype.listUsers = Promise.promisify(listUsers);
SecurityUsers.prototype.listUserNamesByGroup = listUserNamesByGroup;
SecurityUsers.prototype.listUsersByGroup = Promise.promisify(listUsersByGroup);
SecurityUsers.prototype.__getUserNamesFromGroupLinks = __getUserNamesFromGroupLinks;
SecurityUsers.prototype.prepareUserName = prepareUserName;
/**
* Clones the passed in user, and generates a hash of the users password to be pushed into the data store.
*/
SecurityUsers.prototype.__prepareUserForUpsert = __prepareUserForUpsert;
function initialize(config, securityService, callback) {
try {
this.securityService = securityService;
this.cacheService = this.happn.services.cache;
this.dataService = this.happn.services.data;
this.utilsService = this.happn.services.utils;
this.errorService = this.happn.services.error;
this.cryptoService = this.happn.services.crypto;
this.sessionService = this.happn.services.session;
if (!config.__cache_users)
config.__cache_users = {
max: 1000,
maxAge: 0
};
this.__cache_users = this.cacheService.new('cache_security_users', {
type: 'LRU',
cache: config.__cache_users
});
this.__cache_passwords = this.cacheService.new('cache_security_passwords', {
type: 'LRU',
cache: config.__cache_users
});
//backward compatibility after taking groups methods out
this.unlinkGroup = this.groups.unlinkGroup.bind(this.groups);
this.linkGroup = this.groups.linkGroup.bind(this.groups);
this.getGroup = this.groups.getGroup.bind(this.groups);
this.listGroups = this.groups.listGroups.bind(this.groups);
this.deleteGroup = this.groups.deleteGroup.bind(this.groups);
this.upsertGroup = this.groups.upsertGroup.bind(this.groups);
if (config.usernamesCaseInsensitive) {
if (!Array.isArray(config.usernamesCaseInsensitiveExclude))
config.usernamesCaseInsensitiveExclude = [];
if (config.usernamesCaseInsensitiveExclude.indexOf('_ADMIN') === -1)
config.usernamesCaseInsensitiveExclude.push('_ADMIN');
}
this.config = config;
callback();
} catch (e) {
callback(e);
}
}
function clearCaches() {
return this.__cache_passwords.clear().then(() => {
return this.__cache_users.clear();
});
}
function __validate(validationType, options, obj, callback) {
if (obj.name) this.securityService.validateName(obj.name, validationType);
if (validationType === 'user') {
if (!obj.username) return callback(new Error('validation failure: no username specified'));
if (obj.username.indexOf(':nogroups') > -1)
return callback(
new Error("validation failure: username cannot contain the ':nogroups' directive")
);
if (!obj.password && !obj.publicKey) {
return this.dataService.get('/_SYSTEM/_SECURITY/_USER/' + obj.username, {}, (e, result) => {
if (e) return callback(e);
if (!result)
return callback(
new Error('validation failure: no password or publicKey specified for a new user')
);
this.securityService.checkOverwrite(
validationType,
obj,
'/_SYSTEM/_SECURITY/_USER/' + obj.username,
obj.username,
options,
callback
);
});
}
return this.securityService.checkOverwrite(
validationType,
obj,
'/_SYSTEM/_SECURITY/_USER/' + obj.username,
obj.username,
options,
callback
);
}
return callback(new Error('Unknown validation type: ' + validationType));
}
function getPasswordHash(username, callback) {
this.__cache_passwords.get(username, (e, hash) => {
if (e) return callback(e);
if (hash) return callback(null, hash);
this.dataService.get('/_SYSTEM/_SECURITY/_USER/' + username, (e, user) => {
if (e) return callback(e);
if (!user) return callback(new Error(username + ' does not exist in the system'));
this.__cache_passwords.set(user.data.username, user.data.password, e => {
if (e) return callback(e);
callback(null, user.data.password);
});
});
});
}
function __prepareUserForUpsert(user) {
return new Promise((resolve, reject) => {
this.getUserNoGroups(user.username, (e, existing) => {
if (e) return reject(e);
var clonedUser = this.utilsService.clone(user); //we are passing the back to who knows where and it lives here in the cache...
if (!existing) clonedUser.userid = require('uuid').v4();
if (!user.password) return resolve(clonedUser);
return this.cryptoService.generateHash(
user.password,
this.config.pbkdf2Iterations,
(e, hash) => {
if (e) return reject(e);
clonedUser.password = hash;
resolve(clonedUser);
}
);
});
});
}
function __upsertUser(user) {
return new Promise((resolve, reject) => {
this.__prepareUserForUpsert(user).then(prepared => {
this.dataService.upsert(
'/_SYSTEM/_SECURITY/_USER/' + prepared.username,
prepared,
{ merge: true },
(e, result) => {
if (e) return reject(e);
var upserted = this.securityService.serialize('user', result);
this.securityService.dataChanged(
CONSTANTS.SECURITY_DIRECTORY_EVENTS.UPSERT_USER,
upserted,
user,
() => {
resolve(upserted);
}
);
}
);
});
});
}
function upsertUser(user, options, callback) {
if (typeof options === 'function') callback = options;
if (typeof user !== 'object' || user == null)
return callback(new Error('user is null or not an object'));
user.username = this.prepareUserName(user.username);
this.__validate('user', options, user, async e => {
if (e) return callback(e);
try {
let upserted = await this.__upsertUser(user, options);
callback(null, upserted);
} catch (e) {
callback(e);
}
});
}
function deleteUser(user, options, callback) {
if (typeof options === 'function') callback = options;
if (typeof user !== 'object' || user == null)
return callback(new Error('user is null or not an object'));
const preparedUserName = this.prepareUserName(user.username);
var userPath = '/_SYSTEM/_SECURITY/_USER/' + preparedUserName;
var userTree = '/_SYSTEM/_SECURITY/_USER/' + preparedUserName + '/*';
this.dataService.remove(userTree, {}, (e, result1) => {
if (e) return callback(e);
this.dataService.remove(userPath, {}, (e, result2) => {
if (e) return callback(e);
var deleted = {
obj: result2,
tree: result1
};
this.__cache_users
.remove(preparedUserName)
.then(() => {
return this.__cache_passwords.remove(preparedUserName);
})
.then(() => {
this.securityService.dataChanged(
CONSTANTS.SECURITY_DIRECTORY_EVENTS.DELETE_USER,
deleted,
null,
() => {
callback(null, deleted);
}
);
})
.catch(callback);
});
});
}
function prepareUserName(userName, dontRemoveWildCards) {
let preparedUserName = userName;
if (!dontRemoveWildCards) preparedUserName = userName.replace(/[*]/g, ''); //remove any wildcards
if (
this.config.usernamesCaseInsensitive &&
this.config.usernamesCaseInsensitiveExclude.indexOf(userName) === -1
)
preparedUserName = preparedUserName.toLowerCase();
return preparedUserName;
}
function getUserNoGroups(userName, callback) {
const preparedUserName = this.prepareUserName(userName);
if (this.__cache_users.has(preparedUserName))
return callback(null, this.__cache_users.getSync(preparedUserName));
const userPath = '/_SYSTEM/_SECURITY/_USER/' + preparedUserName;
this.dataService.get(userPath, (e, found) => {
if (e) return callback(e);
if (!found) return callback(null, null);
const password = found.data.password;
const returnUser = this.securityService.serialize('user', found);
this.__cache_users.setSync(preparedUserName, returnUser);
this.__cache_passwords.setSync(preparedUserName, password);
callback(null, returnUser);
});
}
function getUser(userName, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
this.getUserNoGroups(userName, (e, user) => {
if (e) return callback(e);
if (options.includeGroups === false) return callback(null, user || null);
if (!user) return callback(null, null);
user.groups = {};
const userPath = '/_SYSTEM/_SECURITY/_USER/' + user.username;
this.dataService.get(
userPath + '/_USER_GROUP/*',
{
sort: {
path: 1
}
},
(e, userGroups) => {
if (e) return callback(e);
userGroups.forEach(userGroup => {
if (userGroup._meta.path.indexOf(userPath + '/') !== 0) return;
//we have found a group linked to the user, add the group, by its name to the groups object
user.groups[userGroup._meta.path.replace(userPath + '/_USER_GROUP/', '')] = userGroup;
delete userGroup._meta; //remove the _meta
});
callback(null, user);
}
);
});
}
function listUsers(userName, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (typeof userName === 'function') {
callback = userName;
userName = '*';
options = {};
}
if (!options) options = {};
let preparedUserName = this.prepareUserName(userName, true);
if (preparedUserName[preparedUserName.length - 1] !== '*') preparedUserName += '*';
const searchPath = '/_SYSTEM/_SECURITY/_USER/' + preparedUserName;
options.clone = false;
if (!options.criteria) {
options.criteria = {};
}
options.criteria.$and = [{ username: { $exists: true } }];
let searchParameters = {
criteria: options.criteria,
sort: options.sort,
options: {}
};
if (options.limit) searchParameters.options.limit = options.limit;
if (options.skip) searchParameters.options.skip = options.skip;
if (options.collation) searchParameters.options.collation = options.collation;
if (options.count) {
this.dataService.count(searchPath, searchParameters, (e, results) => {
if (e) return callback(e);
callback(null, results.data);
});
return;
}
this.dataService.get(searchPath, searchParameters, (e, results) => {
if (e) return callback(e);
var returnUsers = this.securityService.serializeAll('user', results, options);
callback(null, returnUsers);
});
}
function __getUserNamesFromGroupLinks(userGroupLinks) {
if (userGroupLinks == null || userGroupLinks.length === 0) return [];
return userGroupLinks.map(link => {
return link._meta.path.split('/_SYSTEM/_SECURITY/_USER/')[1].split('/_USER_GROUP/')[0];
});
}
function listUserNamesByGroup(groupName) {
return new Promise((resolve, reject) => {
if (typeof groupName !== 'string')
return reject(new Error('validation error: groupName must be specified'));
this.dataService
.get('/_SYSTEM/_SECURITY/_USER/*/_USER_GROUP/' + groupName, { path_only: false }) // TODO return to true once #180 is resolved
.then(userGroupLinks => {
resolve(this.__getUserNamesFromGroupLinks(userGroupLinks));
})
.catch(reject);
});
}
function listUsersByGroup(groupName, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (typeof groupName !== 'string')
return callback(new Error('validation error: groupName must be specified'));
if (!options) options = {};
var userSearchCriteria = { $and: [] };
if (options.criteria) userSearchCriteria.$and.push(options.criteria);
userSearchCriteria.$and.push({ username: { $exists: true } });
this.listUserNamesByGroup(groupName)
.then(usernames => {
if (usernames.length === 0) return callback(null, []);
userSearchCriteria.$and.push({ username: { $in: usernames } });
this.dataService.get(
'/_SYSTEM/_SECURITY/_USER/*',
{ criteria: userSearchCriteria },
(e, results) => {
if (e) return callback(e);
callback(null, this.securityService.serializeAll('user', results, options));
}
);
})
.catch(callback);
}
module.exports = SecurityUsers;