graph-acl
Version:
An ACL module using Graph databases.
815 lines (739 loc) • 25.5 kB
JavaScript
;
/**
* Graph ACL.
*/
var async = require('async');
/**
* Creates an instance of ACL.
*
* Examples:
* var neo4j = require('node-neo4j');
* var graphACL = require('node-graph-acl');
* var db = new neo4j('http://localhost:7474');
* var connector = new graphACL.neo4jConnector(db);
* var acl = new graphACL(connector);
*
* @constructor
* @this {ACL}
* @param {Connector|Neo4jConnector} connector - A connector to a graph database of your choice
*/
var ACL = function(connector) {
this.connector = connector;
};
/**
* Add roles to a given user.
*
* Examples:
* acl.addUserRoles('user1', 'role1', callback);
* acl.addUserRoles('user1', ['role1', 'role2'], callback);
*
* @param {String|Number} userId - User id.
* @param {String|Array} roles - Role(s) to add to the user.
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype.addUserRoles = function(userId, roles, callback) {
var _this = this;
if (typeof userId === 'undefined') {
callback(new Error('User ID is empty'));
return;
}
if (typeof roles === 'undefined') {
callback(new Error('Roles list is empty'));
return;
}
// When only one role in string format, transform it to array
if (!(roles instanceof Array)) {
roles = [roles];
}
this._createUser(userId, function (err, user) {
async.map(
roles, // Array
function(item, cb){
_this._createRole(item, function(err, role) {
_this._createRelationship(user._id, role._id, 'BELONGS_TO', cb);
});
},
function(err, results){
if (results.length === 1) {
callback(err, results[0]);
} else {
callback(err, results);
}
}
);
});
};
/**
* Remove roles from a given user.
*
* Examples:
* acl.removeUserRoles('user1', 'role1', callback);
* acl.removeUserRoles('user1', ['role1', 'role2'], callback);
*
* @param {String|Number} userId - User id.
* @param {String|Array} roles - Role(s) to remove from the user.
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype.removeUserRoles = function(userId, roles, callback) {
var _this = this;
var userNode;
// When only one role in string format, transform it to array
if (!(roles instanceof Array)) {
roles = [roles];
}
this.connector.getNodesWithLabelsAndProperties(['User'], {uid: userId}, function(err, userNodes) {
userNode = userNodes[0];
_this.connector.getNodeRelationships(userNode._id, {types: ['BELONGS_TO']}, function(err, relationships) {
async.map(
relationships, // Array
function(relationship, cb){
_this.connector.getNode(relationship._end, function(err, roleNode) {
if (roles.indexOf(roleNode.rid) !== -1) {
_this.connector.removeRelationship(relationship._id, cb);
} else {
cb(null, true);
}
});
},
function(err, results){
if (results.length === 1) {
callback(err, results[0]);
} else {
callback(err, results);
}
}
);
});
});
};
/**
* Return all the roles the user belongs to.
*
* Examples:
* acl.userRoles('user1', callback);
*
* @param {String|Number} userId - User id.
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype.userRoles = function(userId, callback) {
var _this = this;
var roles = [];
_this.connector.getNodesWithLabelsAndProperties(['User'], {uid: userId}, function(err, nodes) {
_this.connector.getNodeRelationships(nodes[0]._id, {types: ['BELONGS_TO']}, function(err, relationships) {
async.map(
relationships, // Array
function(relationship, cb){
_this.connector.getNode(relationship._end, function(err, roleNode) {
if (typeof roleNode.rid !== 'undefined') {
roles.push(roleNode);
}
cb(null, true);
});
},
function(err, results){
callback(err, roles);
}
);
});
});
};
/**
* Add one or more parent roles to a role.
*
* Examples:
* acl.addRoleParents('role1', 'role2', callback);
* acl.addRoleParents('role1', ['role2', 'role3'], callback);
*
* @param {String|Number} role - Role id.
* @param {String|Array} parents - Parent role(s) to add to the role.
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype.addRoleParents = function(role, parents, callback) {
var _this = this;
// @todo: Create a relationship of type 'BELONGS_TO' between role and each parent
// When only one parent role in string format, transform it to array
if (!(parents instanceof Array)) {
parents = [parents];
}
this._createRole(role, function (err, role) {
async.map(
parents, // Array
function(item, cb){
_this._createRole(item, function(err, parent) {
_this._createRelationship(role._id, parent._id, 'BELONGS_TO', cb);
});
},
function(err, results){
if (results.length === 1) {
callback(err, results[0]);
} else {
callback(err, results);
}
}
);
});
};
/**
* Remove a role from the system.
*
* Examples:
* acl.removeRole('role1', callback);
*
* @param {String|Number} roleId - Role to be removed
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype.removeRole = function(roleId, callback) {
var _this = this;
var role;
this.connector.getNodesWithLabelsAndProperties(['Role'], {rid: roleId}, function(err, node) {
role = node[0];
_this.connector.getNodeRelationships(role._id, {}, function(err, relationships) {
async.map(
relationships, // Array
function(relationship, cb){
_this.connector.removeRelationship(relationship._id, cb);
},
function(err, results){
_this.connector.deleteNode(role._id, function (err, success) {
callback(err, success);
});
}
);
});
});
// @todo: node-neo4j: Maybe create a new function for erasing node and all its relationships?
};
/**
* Add permissions to roles over resources.
* @todo: alternative syntax with permissions array.
*
* Examples:
* acl.allow('role1', 'resource1', 'permission1', callback);
* acl.allow(['role1', 'role2'], ['resource1', 'resource1'], ['permission1', 'permission2'], callback);
*
* @todo:
* var permissionsArray = [
* {
* roles:'role1',
* allows:[
* {resources:'resource1', permissions:'permission1'},
* {resources:['resource2', 'resource3'], permissions:['permission1','permission2']}
* ]
* },
* {
* roles:['role1', 'role2'],
* allows:[
* {resources:'resource4', permissions:['permission1', 'permission2']},
* {resources:['resource5', 'resource6'], permissions:'permission1'}
* ]
* },
* ];
* acl.allow(permissionsArray, callback);
*
* @param {String|Array} roles - Role(s) to add permission(s) of resource(s) to.
* @param {String|Array} resources - Resource(s) to give permission(s) to.
* @param {String|Array} permissions - Permission(s) of the resource(s).
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype.allow = function(roles, resources, permissions, callback) {
var _this = this;
var createdRoles = [];
var createdResources = [];
var createdRelationships = [];
var permissionsFormatted = {};
if (!(roles instanceof Array)) {
roles = [roles];
}
if (!(resources instanceof Array)) {
resources = [resources];
}
if (!(permissions instanceof Array)) {
permissions = [permissions];
}
permissions.forEach(function (permission) {
permissionsFormatted[permission] = true;
});
// @todo: Allow complex alternative syntax
async.map(
roles, // Array
function(role, cb){
_this._createRole(role, function (err, createdRole) {
cb(null, createdRole);
});
},
function(err, results){
createdRoles = results;
async.map(
resources, // Array
function(resource, cb){
_this._createResource(resource, function (err, createdResource) {
cb(null, createdResource);
});
},
function(err, results){
createdResources = results;
var start = 0;
var end = createdRoles.length * createdResources.length;
createdRoles.forEach(function (role) {
createdResources.forEach(function (resource) {
_this._createRelationship(role._id, resource._id, 'HAS_ACCESS_TO', permissionsFormatted, function (err, relationship) {
createdRelationships.push(relationship);
start += 1;
if (start === end) {
callback(null, createdRelationships);
}
});
});
});
}
);
}
);
};
/**
* Remove permissions from roles over resources.
*
* Examples:
* acl.removeAllow('role1', 'resource1', 'permission1', callback);
* acl.removeAllow('role1', ['resource1', 'resource2'], ['permission1', 'permission2'], callback);
*
* @param {String} role - Role to remove permission(s) of resource(s) from.
* @param {String|Array} resources - Resource(s) to remove permission(s) from.
* @param {String|Array} permissions - Permission(s) of the resource(s).
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype.removeAllow = function(role, resources, permissions, callback) {
var _this = this;
if (!(resources instanceof Array)) {
resources = [resources];
}
if (!(permissions instanceof Array)) {
permissions = [permissions];
}
_this.connector.getNodesWithLabelsAndProperties(['Role'], {rid: role}, function(err, nodes) {
_this.connector.getNodeRelationships(nodes[0]._id, {types: ['HAS_ACCESS_TO']}, function(err, relationships) {
async.map(
relationships, // Array
function(relationship, cb){
_this.connector.getNode(relationship._end, function(err, resourceNode) {
if (resources.indexOf(resourceNode.rsid) > -1) {
var relationshipId = relationship._id;
var newPermissions = relationship;
// @todo: use _getParameters
delete(newPermissions._id);
delete(newPermissions._start);
delete(newPermissions._end);
delete(newPermissions._type);
permissions.forEach(function (permission) {
delete(newPermissions[permission]);
});
_this.connector.updateRelationship(relationshipId, newPermissions, function (err, updatedRelationship) {
cb(null, updatedRelationship);
});
} else {
cb(null, relationship);
}
});
},
function(err, results){
callback(err, results);
}
);
});
});
};
/**
* Get all the permissions the user has to over this resources.
*
* It returns an array of objects where every object maps a
* resource name to a list of permissions for that resource.
*
* @todo: ATENTION! At the moment this does not support Parent Roles!
* @todo: Ideal scenario: Take advantage of graph queries to get all resources
*
* Examples:
* acl.allowedPermissions('user1', 'resource1', callback);
* acl.allowedPermissions('user1', ['resource1', 'resource2'], callback);
*
* @param {String|Number} userId - User.
* @param {String|Array} resources - Resource(s) to ask permissions about.
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype.allowedPermissions = function(userId, resources, callback) {
var _this = this;
var userNode;
var permissions = [];
if (!(resources instanceof Array)) {
resources = [resources];
}
// Get User Node
this.connector.getNodesWithLabelsAndProperties(['User'], {uid: userId}, function(err, userNodes) {
userNode = userNodes[0];
// Get Roles to which the User belongs to
_this.connector.getNodeRelationships(userNode._id, {types: ['BELONGS_TO']}, function(err, relationships) {
async.map(
relationships, // Array
function(relationship, cb){
_this.connector.getNode(relationship._end, function(err, roleNode) {
cb(null, roleNode);
});
},
function(err, roles){
// Get all Resource Nodes the User is connected to
async.map(
roles, // Array
function(role, cb){
_this.connector.getNodeRelationships(role._id, {types: ['HAS_ACCESS_TO']}, function(err, relationships) {
cb(null, relationships);
});
},
function(err, relationships){
// Check if Resource is in the resources array, and if it is,
// check which permissions the Role has on that resource
async.map(
relationships[0], // Array
function(relationship, cb){
_this.connector.getNode(relationship._end, function(err, resourceNode) {
if (resources.indexOf(resourceNode.rsid) > -1) {
var obj = {};
_this.connector._getParameters(relationship, function(err, cleanedRelationship) {
var resourcePermissions = [];
for (var key in cleanedRelationship){
if (cleanedRelationship[key] === true) {
resourcePermissions.push(key);
}
}
obj[resourceNode.rsid] = resourcePermissions;
permissions.push(obj);
});
}
cb(null, true);
});
},
function(err, resources){
callback(err, permissions);
}
);
}
);
}
);
});
});
// Return is
// [{'resource1': ['permission1','permission2', ...]},
// {'resource2': ['permission1','permission2', ...]},
// ...]
};
/**
* Check if a user is allowed to access resource(s) with given
* permission(s) (note: it must fulfil all the permissions).
*
* Examples:
* acl.isAllowed('user1', 'resource1', 'permission1', callback);
* acl.isAllowed('user1', ['resource1', 'resource2'], ['permission1', 'permission2'], callback);
*
* @param {String|Number} userId - User id.
* @param {String|Array} resources - Resource(s) to ask permissions for.
* @param {String|Array} permissions - Permission(s).
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype.isAllowed = function(userId, resources, permissions, callback){
if (!(resources instanceof Array)) {
resources = [resources];
}
if (!(permissions instanceof Array)) {
permissions = [permissions];
}
this.allowedPermissions(userId, resources, function (err, fullPermissions) {
var allowed = true;
var fullPermissionsNormalized = [];
fullPermissions.forEach(function(fullPermission) {
for (var key in fullPermission) {
fullPermissionsNormalized[key] = fullPermission[key];
}
});
for (var key in resources) {
if (fullPermissionsNormalized[resources[key]]) {
for (var key2 in permissions) {
if (fullPermissionsNormalized[resources[key]].indexOf(permissions[key2]) === -1) {
allowed = false;
}
}
} else {
allowed = false;
}
}
callback(null, allowed);
});
};
/**
* Check if any of the role(s) have permission(s) on the resource(s).
*
* Examples:
* acl.areAnyRolesAllowed('role1', 'resource1', 'permission1', callback);
* acl.areAnyRolesAllowed('role1', 'resource1', ['permission1', 'permission2'], callback);
* acl.areAnyRolesAllowed(['role1', role2], 'resource1', 'permission1', callback);
* acl.areAnyRolesAllowed(['role1', role2], 'resource1', ['permission1', 'permission2'], callback);
*
* @param {String|Array} roles - Role(s) ids to check the permissions for.
* @param {String} resource - Resource to ask permissions for.
* @param {String|Array} permissions - Permission(s) over the resource.
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype.areAnyRolesAllowed = function(roles, resource, permissions, callback){
var _this = this;
var allowed = false;
if (!(roles instanceof Array)) {
roles = [roles];
}
if (!(permissions instanceof Array)) {
permissions = [permissions];
}
async.map(
roles, // Array
function(roleId, cb) {
_this.connector.getNodesWithLabelsAndProperties(['Role'], {rid: roleId}, function(err, roleNode) {
cb(null, roleNode[0]);
});
},
function(err, roleNodes){
// Get all Resource Nodes the User is connected to
async.map(
roleNodes, // Array
function(roleNode, cb) {
_this.connector.getNodeRelationships(roleNode._id, {types: ['HAS_ACCESS_TO']}, function(err, relationships) {
cb(null, relationships);
});
},
function(err, relationships){
// Check if Resource is in the resources array, and if it is,
// check which permissions the Role has on that resource
async.map(
relationships, // Array
function(relationship, cb){
_this.connector.getNode(relationship[0]._end, function(err, resourceNode) {
if (resource === resourceNode.rsid) {
for (var key in permissions) {
if (relationship[0][permissions[key]] === true) {
allowed = true;
}
}
}
cb (null, true);
});
},
function(err, resources){
callback(err, allowed);
}
);
}
);
}
);
};
/**
* Get resources the role(s) has permission(s) over.
*
* Examples:
* acl.whatResources('role1', 'permission1', callback);
* acl.whatResources('role1', ['permission1', 'permission2'], callback);
* acl.whatResources(['role1', role2], 'permission1', callback);
* acl.whatResources(['role1', role2], ['permission1', 'permission2'], callback);
*
* @param {String|Array} roles - Roles ids.
* @param {String|Array} permissions - Permissions ids.
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype.whatResources = function(role, callback){
var _this = this;
var roleResources = {};
_this.connector.getNodesWithLabelsAndProperties(['Role'], {rid: role}, function(err, roleNode) {
_this.connector.getNodeRelationships(roleNode[0]._id, {types: ['HAS_ACCESS_TO']}, function(err, relationships) {
async.map(
relationships, // Array
function(relationship, cb){
_this.connector.getNode(relationship._end, function(err, resourceNode) {
_this.connector._getParameters(relationship, function(err, cleanedRelationship) {
var permissions = [];
for (var key in cleanedRelationship) {
permissions[key] = cleanedRelationship[key];
}
roleResources[resourceNode.rsid] = permissions;
cb (null, resourceNode);
});
});
},
function(err, resources){
callback(err, roleResources);
}
);
});
});
// Return is {resource1: [permission1, permission2, ...],
// resource2: [permission1, permission2, ...],
// ...}
};
/**
* Remove a resource from the system.
*
* Examples:
* acl.removeResource('resource1', callback);
*
* @param {String} resourceId - Resource to be removed.
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype.removeResource = function(resourceId, callback){
var _this = this;
var resource;
this.connector.getNodesWithLabelsAndProperties(['Resource'], {rsid: resourceId}, function(err, node) {
resource = node[0];
_this.connector.getNodeRelationships(resource._id, {}, function(err, relationships) {
async.map(
relationships, // Array
function(relationship, cb){
_this.connector.removeRelationship(relationship._id, cb);
},
function(err, results){
_this.connector.deleteNode(resource._id, function (err, success) {
callback(err, success);
});
}
);
});
});
};
/*ACL.prototype.permittedResources = function(roles, permissions, callback){
callback();
};*/
/////////////////////////
// "Private" functions //
/////////////////////////
/**
* Create User. If user already exists, does nothing.
*
* Examples:
* acl._createUser('user1', callback);
*
* @param {String|Number} userId - User id.
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype._createUser = function (userId, callback) {
var _this = this;
this.connector.existsNodeWithLabelsAndProperties(['User'], {uid: userId}, function(err, exists) {
if (exists) {
_this.connector.getNodesWithLabelsAndProperties(['User'], {uid: userId}, function(err, userRetrieved) {
callback(null, userRetrieved[0]);
});
} else {
_this.connector.createNode(['User'], {uid: userId}, function (err, userCreated) {
callback(null, userCreated);
});
}
});
};
/**
* Create Role. If role already exists, does nothing.
*
* Examples:
* acl._createRole('role1', callback);
*
* @param {String|Number} roleId - Role id.
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype._createRole = function (roleId, callback) {
var _this = this;
this.connector.existsNodeWithLabelsAndProperties(['Role'], {rid: roleId}, function(err, exists) {
if (exists) {
_this.connector.getNodesWithLabelsAndProperties(['Role'], {rid: roleId}, function(err, roleRetrieved) {
callback(null, roleRetrieved[0]);
});
} else {
_this.connector.createNode(['Role'], {rid: roleId}, function (err, roleCreated) {
callback(null, roleCreated);
});
}
});
};
/**
* Create Resource. If role already exists, does nothing.
*
* Examples:
* acl._createResource('resource1', callback);
*
* @param {String|Number} resourceId - Resource id.
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype._createResource = function (resourceId, callback) {
var _this = this;
this.connector.existsNodeWithLabelsAndProperties(['Resource'], {rsid: resourceId}, function(err, exists) {
if (exists) {
_this.connector.getNodesWithLabelsAndProperties(['Resource'], {rsid: resourceId}, function(err, resourceRetrieved) {
callback(null, resourceRetrieved[0]);
});
} else {
_this.connector.createNode(['Resource'], {rsid: resourceId}, function (err, resourceCreated) {
callback(null, resourceCreated);
});
}
});
};
/**
* Create Relationship. If relationship already exists, does nothing.
*
* Examples:
* acl._createRelationship(1, 2, 'TYPE', callback);
* acl._createRelationship(1, 2, 'TYPE', {p1:'v1', p2:'v2'}, callback);
*
* @param {String|Number} nodeId1 - First Node ID.
* @param {String|Number} nodeId2 - Second Node ID.
* @param {String} type - Relationship type.
* @param {Object} properties - Relationship properties.
* @param {Function} callback - Callback called when finished.
*/
ACL.prototype._createRelationship = function (nodeId1, nodeId2, type, properties, callback) {
var _this = this;
if (typeof callback === 'undefined') {
if (typeof properties === 'function') {
callback = properties;
properties = {};
}
}
this.connector.existsRelationshipsBetweenNodes(nodeId1, nodeId2, type, function(err, exists) {
if (exists) {
_this.connector.getRelationshipsBetweenNodes(nodeId1, nodeId2, type, function(err, relationshipRetrieved) {
var complete = true;
for (var property in properties) {
if (relationshipRetrieved[property] === undefined) {
complete = false;
}
}
if (complete) {
callback(null, relationshipRetrieved);
} else {
// @todo: should update relationship
var newProperties = relationshipRetrieved;
var relationshipRetrievedId = relationshipRetrieved._id;
// @todo: use _getParameters
delete(newProperties._id);
delete(newProperties._start);
delete(newProperties._end);
delete(newProperties._type);
for (property in properties) {
if (newProperties[property] === undefined) {
newProperties[property] = true;
}
}
_this.connector.updateRelationship(relationshipRetrievedId, newProperties, function (err, updatedRelationship) {
callback(null, updatedRelationship);
});
}
});
} else {
_this.connector.addRelationship(nodeId1, nodeId2, type, properties, function (err, relationshipCreated) {
callback(null, relationshipCreated);
});
}
});
};
module.exports = ACL;