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
683 lines (562 loc) • 19.7 kB
JavaScript
var Promise = require('bluebird');
const CONSTANTS = require('../..').constants;
function SecurityGroups(opts) {
this.log = opts.logger.createLogger('SecurityGroups');
this.log.$$TRACE('construct(%j)', opts);
this.opts = opts;
}
SecurityGroups.prototype.initialize = Promise.promisify(initialize);
SecurityGroups.prototype.clearCaches = clearCaches;
SecurityGroups.prototype.__validate = __validate;
SecurityGroups.prototype.__upsertGroup = __upsertGroup;
SecurityGroups.prototype.upsertGroup = Promise.promisify(upsertGroup);
SecurityGroups.prototype.deleteGroup = Promise.promisify(deleteGroup);
SecurityGroups.prototype.listGroups = Promise.promisify(listGroups);
SecurityGroups.prototype.getGroup = Promise.promisify(getGroup);
SecurityGroups.prototype.linkGroup = Promise.promisify(linkGroup);
SecurityGroups.prototype.unlinkGroup = Promise.promisify(unlinkGroup);
SecurityGroups.prototype.listPermissions = listPermissions;
SecurityGroups.prototype.__attachPermissions = __attachPermissions;
SecurityGroups.prototype.__attachPermissionsAll = __attachPermissionsAll;
SecurityGroups.prototype.upsertPermission = upsertPermission;
//nb internal (starting with __) functions dont emit dataCHanged to the security service
SecurityGroups.prototype.__upsertPermission = __upsertPermission;
SecurityGroups.prototype.__upsertGroupPermissions = __upsertGroupPermissions;
SecurityGroups.prototype.removePermission = removePermission;
SecurityGroups.prototype.__removePermission = __removePermission;
SecurityGroups.prototype.__validatePermissionsPath = __validatePermissionsPath;
SecurityGroups.prototype.__escapePermissionsPath = __escapePermissionsPath;
SecurityGroups.prototype.__unescapePermissionsPath = __unescapePermissionsPath;
var ALLOWED_PERMISSIONS = [
'set',
'get',
'remove',
'on',
'*',
'delete',
'put',
'post',
'head',
'options'
];
function initialize(config, securityService, callback) {
var _this = this;
_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_groups)
config.__cache_groups = {
max: 2500,
maxAge: 0
};
if (!config.__cache_permissions)
config.__cache_permissions = {
max: 5000,
maxAge: 0
};
_this.__cache_groups = _this.cacheService.new('cache_security_groups', {
type: 'LRU',
cache: config.__cache_groups
});
_this.__cache_permissions = _this.cacheService.new('cache_security_permissions', {
type: 'LRU',
cache: config.__cache_permissions
});
if (config.persistPermissions !== false) return callback();
//inject the permissions data provider
_this.dataService._insertDataProvider(
0,
{
name: 'volatile_permissions',
provider: 'memory',
settings: {},
patterns: ['/_SYSTEM/_SECURITY/_PERMISSIONS/*']
},
e => {
if (e) return callback(e);
_this.dataService.addDataProviderPatterns('/_SYSTEM/_SECURITY/_GROUP', [
'/_SYSTEM/_SECURITY/_PERMISSIONS/_*'
]);
callback();
}
);
}
function clearCaches() {
return this.__cache_groups.clear().then(() => {
return this.__cache_permissions.clear();
});
}
function __validate(validationType, options, obj, callback) {
if (validationType === 'user-group') {
var group = options[0];
var user = options[1];
return this.getGroup(group.name)
.then(group => {
if (!group) throw new Error('validation error: group does not exist or has not been saved');
return this.securityService.users.getUser(user.username);
})
.then(user => {
if (!user) throw new Error('validation error: user does not exist or has not been saved');
})
.then(callback)
.catch(callback);
}
if (obj.name) this.securityService.validateName(obj.name, validationType);
if (validationType === 'group') {
if (options.parent) {
if (!options.parent._meta.path)
return callback(
new Error(
'validation error: parent group path is not in your request, have you included the _meta?'
)
);
//path, parameters, callback
return this.dataService.get(options.parent._meta.path, {}, (e, result) => {
if (e) return callback(e);
if (!result)
return callback(
new Error('validation error: parent group does not exist: ' + options.parent._meta.path)
);
this.securityService.checkOverwrite(
validationType,
obj,
options.parent._meta.path + '/' + obj.name,
obj.name,
options,
callback
);
});
}
return this.securityService.checkOverwrite(
validationType,
obj,
'/_SYSTEM/_SECURITY/_GROUP/' + obj.name,
obj.name,
options,
callback
);
}
if (validationType === 'permission') {
var permissionGroup = options[1];
if (!permissionGroup)
return callback(new Error('validation error: you need a group to add a permission to'));
if (!permissionGroup._meta.path)
return callback(
new Error(
'validation error: permission group path is not in your request, have you included the _meta?'
)
);
return this.dataService.get(permissionGroup._meta.path, {}, (e, result) => {
if (e) return callback(e);
if (!result)
return callback(
new Error(
'validation error: permission group does not exist: ' + permissionGroup._meta.path
)
);
callback();
});
}
return callback(new Error('Unknown validation type: ' + validationType));
}
function __upsertGroup(group, options) {
var groupPath;
if (options.parent) groupPath = options.parent._meta.path + '/' + group.name;
else groupPath = '/_SYSTEM/_SECURITY/_GROUP/' + group.name;
var permissions = this.utilsService.clone(group.permissions);
delete group.permissions; //dont want these ending up in the db
return new Promise((resolve, reject) => {
this.dataService.upsert(groupPath, group, async (e, result) => {
try {
group.permissions = permissions; //restore permissions
if (e) return reject(e);
//set the permissions back again
result.data.permissions = permissions;
const returnGroup = this.securityService.serialize('group', result);
if (!permissions) return resolve(returnGroup);
await this.__upsertGroupPermissions(group.name, permissions);
resolve(returnGroup);
} catch (e) {
reject(e);
}
});
});
}
function upsertGroup(group, options, callback) {
if (typeof options === 'function') callback = options;
if (typeof group !== 'object' || group == null)
return callback(new Error('group is null or not an object'));
this.__validate('group', options, group, async e => {
if (e) return callback(e);
try {
let upserted = await this.__upsertGroup(group, options);
this.securityService.dataChanged(
CONSTANTS.SECURITY_DIRECTORY_EVENTS.UPSERT_GROUP,
upserted,
null,
() => {
callback(null, upserted);
}
);
} catch (e) {
callback(e);
}
});
}
function deleteGroup(group, options, callback) {
if (typeof options === 'function') callback = options;
if (typeof group !== 'object' || group == null)
return callback(new Error('group is null or not an object'));
this.getGroup(group.name, {}, (e, group) => {
if (e) return callback(e);
if (!group) return callback(new Error('group you are deleting does not exist'));
try {
var deletePath = '/_SYSTEM/_SECURITY/_PERMISSIONS/' + group.name + '/*';
this.dataService.remove(deletePath, {}, (e, permissionsDeleteResults) => {
if (e) return callback(e);
var deletePath = '/_SYSTEM/_SECURITY/_USER/*/_USER_GROUP/' + group.name;
this.dataService.remove(deletePath, {}, (e, userGroupDeleteResults) => {
if (e) return callback(e);
deletePath = '/_SYSTEM/_SECURITY/_GROUP/' + group.name;
this.dataService.remove(deletePath, {}, (e, groupDeleteResults) => {
if (e) return callback(e);
var deleted = {
removed: groupDeleteResults.data.removed,
obj: group,
links: userGroupDeleteResults,
permissions: permissionsDeleteResults
};
this.securityService.dataChanged(
CONSTANTS.SECURITY_DIRECTORY_EVENTS.DELETE_GROUP,
deleted,
null,
() => {
callback(null, deleted);
}
);
});
});
});
} catch (err) {
callback(err);
}
});
}
function listGroups(groupName, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (typeof groupName === 'function') {
callback = groupName;
groupName = '*';
options = {};
}
if (!options) options = {};
if (!options.criteria) options.criteria = {};
if (!options.sort)
options.sort = {
path: 1
};
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 (groupName[groupName.length - 1] !== '*') groupName += '*';
var searchPath = '/_SYSTEM/_SECURITY/_GROUP/' + groupName;
if (options.count) {
this.dataService.count(searchPath, searchParameters, (e, results) => {
if (e) return callback(e);
callback(null, results.data);
});
return;
}
try {
this.dataService.get(searchPath, searchParameters, (e, groups) => {
if (e) return callback(e);
var extracted = this.dataService.extractData(groups);
if (options.skipPermissions) {
callback(null, extracted);
return;
}
this.__attachPermissionsAll(extracted)
.then(callback(null, extracted))
.catch(callback);
});
} catch (e) {
callback(e);
}
}
function getGroup(groupName, options, callback) {
if (typeof options === 'function') callback = options;
if (!groupName || groupName.indexOf('*') > 0) return callback('invalid group name: ' + groupName);
var cachedGroup = this.__cache_groups.getSync(groupName);
if (cachedGroup) return callback(null, cachedGroup);
this.dataService.get(
'/_SYSTEM/_SECURITY/_GROUP/' + groupName,
{
sort: {
path: 1
}
},
(e, result) => {
if (e) return callback(e);
if (result == null) return callback(null, null);
var group = result.data;
this.__attachPermissions(group)
.then(attached => {
this.__cache_groups.setSync(groupName, attached);
callback(null, attached);
})
.catch(callback);
}
);
}
function linkGroup(group, user, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
this.__validate('user-group', [group, user], options, e => {
if (e) return callback(e);
this.dataService.upsert(
'/_SYSTEM/_SECURITY/_USER/' + user.username + '/_USER_GROUP/' + group.name,
options,
(e, result) => {
if (e) return callback(e);
var upserted = this.securityService.serialize('user-group', result, options);
this.securityService.dataChanged(
CONSTANTS.SECURITY_DIRECTORY_EVENTS.LINK_GROUP,
upserted,
user,
() => {
callback(null, upserted);
}
);
}
);
});
}
function unlinkGroup(group, user, options, callback) {
if (typeof options === 'function') callback = options;
this.__validate('user-group', [group, user], null, e => {
if (e) return callback(e);
var groupLinkPath = '/_SYSTEM/_SECURITY/_USER/' + user.username + '/_USER_GROUP/' + group.name;
this.dataService.remove(groupLinkPath, {}, (e, result) => {
if (e) return callback(e);
this.securityService.dataChanged(
CONSTANTS.SECURITY_DIRECTORY_EVENTS.UNLINK_GROUP,
{ path: groupLinkPath },
user,
() => {
callback(null, result);
}
);
});
});
}
function __attachPermissionsAll(groups) {
var promises = [];
groups.forEach(group => {
promises.push(this.__attachPermissions(group));
});
return Promise.all(promises);
}
function __attachPermissions(group) {
return new Promise((resolve, reject) => {
group.permissions = {};
this.listPermissions(group.name)
.then(permissions => {
permissions.forEach(permission => {
if (permission.authorized) {
if (!group.permissions[permission.path])
group.permissions[permission.path] = { actions: [] };
group.permissions[permission.path].actions.push(permission.action);
}
});
resolve(group);
})
.catch(e => {
reject(e);
});
});
}
function listPermissions(groupName) {
return new Promise((resolve, reject) => {
if (!groupName) return reject(new Error('please supply a groupName'));
if (this.__cache_permissions.has(groupName)) {
return this.__cache_permissions
.get(groupName)
.then(resolve)
.catch(reject);
}
this.dataService.get(
'/_SYSTEM/_SECURITY/_PERMISSIONS/' + groupName + '/*',
{
sort: {
path: 1
}
},
(e, results) => {
if (e) return reject(e);
var deserialized = this.dataService.extractData(results);
this.__cache_permissions.set(groupName, deserialized);
resolve(deserialized);
}
);
});
}
function removePermission(groupName, path, action) {
return new Promise((resolve, reject) => {
if (!groupName) return reject(new Error('please supply a groupName'));
if (!action) action = '*';
if (!path) path = '*';
return this.__removePermission(groupName, path, action)
.then(result => {
if (result.data.removed)
return this.securityService.dataChanged(
CONSTANTS.SECURITY_DIRECTORY_EVENTS.PERMISSION_REMOVED,
{
groupName: groupName,
path: path,
action: action
},
null,
() => {
resolve(result);
}
);
resolve(result);
})
.catch(reject);
});
}
function __removePermission(groupName, path, action) {
return new Promise((resolve, reject) => {
this.dataService.remove(
[
'/_SYSTEM/_SECURITY/_PERMISSIONS',
groupName,
action,
this.__escapePermissionsPath(path)
].join('/'),
(e, result) => {
if (e) return reject(e);
resolve(result);
}
);
});
}
function upsertPermission(groupName, path, action, authorized) {
return new Promise((resolve, reject) => {
return this.__upsertPermission(groupName, path, action, authorized)
.then(result => {
this.securityService.dataChanged(
CONSTANTS.SECURITY_DIRECTORY_EVENTS.PERMISSION_UPSERTED,
{
groupName: groupName,
path: path,
action: action,
authorized: authorized
},
() => {
resolve(result);
}
);
})
.catch(reject);
});
}
function validateGroupPermissions(permissions) {
var errors = [];
Object.keys(permissions).forEach(function(permissionPath) {
var permission = permissions[permissionPath];
if (!permission.actions && !permission.prohibit)
return errors.push('missing allowed actions or prohibit rules: ' + permissionPath);
if (permission.actions)
permission.actions.forEach(function(action) {
if (ALLOWED_PERMISSIONS.indexOf(action) === -1)
return errors.push('unknown action: ' + action + ' for path: ' + permissionPath);
});
if (permission.prohibit)
permission.prohibit.forEach(function(action) {
if (ALLOWED_PERMISSIONS.indexOf(action) === -1)
return errors.push('unknown prohibit action: ' + action + ' for path: ' + permissionPath);
});
});
if (errors.length === 0) return true;
else return errors;
}
function __upsertGroupPermissions(groupName, permissions) {
return new Promise((resolve, reject) => {
var promises = [];
if (!groupName) return reject(new Error('please supply a groupName'));
var permissionsValidation = validateGroupPermissions(permissions);
if (permissionsValidation !== true)
return reject(new Error('group permissions invalid: ' + permissionsValidation.join(',')));
Object.keys(permissions).forEach(permissionPath => {
var permission = permissions[permissionPath];
if (permission.actions)
permission.actions.forEach(action => {
promises.push(this.__upsertPermission(groupName, permissionPath, action));
});
if (permission.prohibit)
permission.prohibit.forEach(action => {
promises.push(this.__upsertPermission(groupName, permissionPath, action, false));
});
});
Promise.all(promises)
.then(responses => {
resolve(responses);
})
.catch(reject);
});
}
function __upsertPermission(groupName, path, action, authorized) {
return new Promise((resolve, reject) => {
if (!groupName) return reject(new Error('please supply a groupName'));
var validPath = this.__validatePermissionsPath(path);
if (validPath !== true) return reject(new Error(validPath));
if (!action) action = '*';
if (authorized == null) authorized = true;
authorized = !!authorized; //must always be stored true or false
this.dataService.upsert(
[
'/_SYSTEM/_SECURITY/_PERMISSIONS',
groupName,
action,
this.__escapePermissionsPath(path)
].join('/'),
{
action: action,
authorized: authorized,
path: path
},
(e, result) => {
if (e) return reject(e);
resolve(result);
}
);
});
}
function __escapePermissionsPath(path) {
return path.replace(/\*/g, '{{w}}');
}
function __unescapePermissionsPath(path) {
return path.replace(/\{\{w}}/g, '*');
}
function __validatePermissionsPath(path) {
if (!path) return 'permission path is null';
if (path.indexOf('{{w}}') > -1)
return 'invalid permission path, cannot contain special string {{w}}';
return true;
}
module.exports = SecurityGroups;