Skip to content

Commit

Permalink
Implemented inherits framework
Browse files Browse the repository at this point in the history
  • Loading branch information
taoyuan committed Nov 7, 2016
1 parent 0d4f1c4 commit d3e250b
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 24 deletions.
58 changes: 44 additions & 14 deletions lib/acl.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ACL {
}

//noinspection JSUnresolvedVariable
this.ds =this.datasource = this.dataSource = ds;
this.ds = this.datasource = this.dataSource = ds;

this.models = schema.loadModels(ds);
Object.assign(this, this.models);
Expand All @@ -45,10 +45,10 @@ class ACL {
}

findUserRoles(user, scope) {
const {Mapping, Role} = this;
const {Mapping, Roles} = this;
const userId = utils.getId(user);
return Mapping.find({where: {userId}}).then(mappings => mappings.map(mapping => mapping.roleId))
.then(roleIds => Role.findByScope(scope || '*', {where: {id: {inq: roleIds}}}));
.then(roleIds => Roles.findByScope(scope || '*', {where: {id: {inq: roleIds}}}));
}

addUserRoles(user, roles) {
Expand Down Expand Up @@ -246,7 +246,6 @@ class ACL {
.then(permissions => _.unionBy(permissions, p => (p.type || '_') + (p.id || '_')));
}


//-----------------------------------------------------------------------------
//
// Private methods
Expand All @@ -256,13 +255,22 @@ class ACL {
/**
*
* @param roles
* @param scope
* @returns {*|Array}
* @private
*/
_allRoles(roles, scope) {
// TODO join inherited roles
return roles || [];
_allRoles(roles) {
contract(arguments)
.params('object|array')
.end();

roles = utils.sureArray(roles);

return this.Roles.getRolesParents(roles).then(parents => {
if (parents && parents.length > 0) {
return this._allRoles(parents).then(pps => _.unionBy(roles, pps, item => item.id))
}
return roles;
});
}


Expand All @@ -279,18 +287,32 @@ class ACL {
.params('object|string')
.end();

return this.findUserRoles(user, scope).then(roles => this._allRoles(roles, scope));
return this.findUserRoles(user, scope).then(roles => this._allRoles(roles));
}

/**
*
* @param subjects
* @param roles
* @param resource
* @returns {*}
* @private
*/
_recurseResourcePermissions(subjects, resource) {
return this._resourcePermissions(subjects, resource);
_recurseResourcePermissions(roles, resource) {
if (!roles || roles.length === 0) {
return Promise.resolve([]);
}

const {Roles} = this;
return this._resourcePermissions(roles, resource).then(actions => {
return Roles.getRolesParents(roles).then(parents => {
if (parents || parents.length) {
return this._recurseResourcePermissions(parents, resource).then(pas => {
return _.union(actions, pas);
});
}
return actions;
})
});
}

/**
Expand Down Expand Up @@ -343,8 +365,16 @@ class ACL {
* @private
*/
_recurseCheckPermissions(roles, resource, actions) {
// TODO check inherits
return this._checkPermissions(roles, resource, actions);
const {Roles} = this;
return this._checkPermissions(roles, resource, actions).then(hasPermission => {
if (hasPermission) return true;
return Roles.getRolesParents(roles).then(parents => {
if (parents && parents.length) {
return this._recurseCheckPermissions(parents, resource, actions);
}
return false;
})
})
}

/**
Expand Down
47 changes: 45 additions & 2 deletions lib/models/acl-role.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"use strict";

const _ = require('lodash');
const assert = require('assert');
const shortid = require('shortid');
const Promise = require('bluebird');
const utils = require('../utils');

module.exports = function (Role) {
Expand Down Expand Up @@ -38,8 +41,48 @@ module.exports = function (Role) {
return Role.find(filter, options);
};

Role.removeByName = function (scope, name) {
return this.remove({scope, name});
Role.prototype._idsForParents = function (parents) {
const ids = [];
parents = utils.sureArray(parents).filter(p => {
if (!p) return false;
if (typeof p === 'string') {
ids.push(p);
return false;
}

return (p.scopeType === this.scopeType) && (p.scopeId === this.scopeId);
});

const where = ids.length && {id: {inq: ids}, scopeType: this.scopeType, scopeId: this.scopeId};
return Promise.resolve(where && Role.find({where: where, fields: ['id']}))
.then(roles => {
if (roles && roles.length) {
parents = parents.concat(roles)
}
return parents;
})
.filter(p => p.id !== this.id) // filter self
.map(p => p.id).then(_.union);
};

Role.prototype.inherit = function (parents) {
return this._idsForParents(parents)
.then(parents => {
this.parentIds = _.union(this.parentIds, parents)
})
.then(() => this.save());
};

Role.prototype.uninherit = function (parents) {
return this._idsForParents(parents)
.then(parents => this.parentIds = _.without(this.parentIds, ...parents))
.then(() => this.save());
};

Role.prototype.setInherits = function (parents) {
return this._idsForParents(parents)
.then(parents => this.parentIds = parents)
.then(() => this.save());
};

return Role;
Expand Down
42 changes: 41 additions & 1 deletion lib/services/roles.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"use strict";

const assert = require('assert');
const _ = require('lodash');
const Promise = require('bluebird');
const g = require('strong-globalize')();
const utils = require('../utils');
const contract = require('../contract');

module.exports = class Roles {
module.exports = class {

/**
*
Expand Down Expand Up @@ -72,6 +75,8 @@ module.exports = class Roles {
.params('string')
.end();

assert(scope !== '*', 'scope * is not allowed');

const {Role} = this.acl;
const where = utils.resolvePolymorphic(Role, 'scope', scope || null) || {};
where.name = name;
Expand All @@ -90,4 +95,39 @@ module.exports = class Roles {
return Role.count(where);
}

getRolesParents(roles) {
contract(arguments)
.params('object|array')
.end();

roles = utils.sureArray(roles);
return Promise.map(roles, role => Promise.fromCallback(cb => role.inherits(cb)))
.then(data => _(data).flatten().uniqBy(item => item.id).value());
}

inherit(role, parents) {
contract(arguments)
.params('string|object', 'string|array|object')
.end();

return Promise.resolve(typeof role === 'string' ? this.findById(role) : role)
.then(role => role.inherit(parents));
}

uninherit(role, parents) {
contract(arguments)
.params('string|object', 'string|array|object')
.end();
return Promise.resolve(typeof role === 'string' ? this.findById(role) : role)
.then(role => role.uninherit(parents));
}

setInherits(role, parents) {
contract(arguments)
.params('string|object', 'string|array|object')
.end();
return Promise.resolve(typeof role === 'string' ? this.findById(role) : role)
.then(role => role.setInherits(parents));
}

};
95 changes: 88 additions & 7 deletions test/acl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const test = require('ava');
const assert = require('chai').assert;
const Promise = require('bluebird');
const s = require('./support');
const sacl = require('..');

const ctx = {};
test.before(t => s.setup(ctx));
Expand Down Expand Up @@ -194,6 +193,36 @@ test('should remove role by name', t => {
});
});

test('should get all roles includes inherits', () => {
const acl = ctx.acl;
const {Roles} = acl;
return Promise.all([
Roles.create('A'),
Roles.create('B'),
Roles.create('C'),
Roles.create('D'),
Roles.create('ABC'),
Roles.create('BCD'),
Roles.create('ABCD'),
]).then(([A, B, C, D, ABC, BCD, ABCD]) => {
return Promise.all([
Roles.inherit(ABC, [A, B, C]),
Roles.inherit(BCD, [B, C, D]),
Roles.inherit(ABCD, [ABC, BCD]),
]).then(() => {
return acl._allRoles([ABC, BCD])
.then(roles => {
assert.sameDeepMembers(roles.map(p => p.name), ['A', 'B', 'C', 'D', 'ABC', 'BCD']);
})
.then(() => acl._allRoles(ABCD))
.then(roles => {
assert.sameDeepMembers(roles.map(p => p.name), ['A', 'B', 'C', 'D', 'ABC', 'BCD', 'ABCD']);
})
});
});
});


test('should allow', t => {
const acl = ctx.acl;
const {Roles, Permission} = acl;
Expand Down Expand Up @@ -298,7 +327,7 @@ test('should is allowed for the resource that not exited in permission collectio
});
});

test('should get allowed resources for all resource types', t => {
test('should get allowed resources for all resource types', () => {
const acl = ctx.acl;
const {Roles} = acl;
return Promise.all([
Expand All @@ -325,7 +354,7 @@ test('should get allowed resources for all resource types', t => {
});
});

test('should get allowed resources for specified type', t => {
test('should get allowed resources for specified type', () => {
const acl = ctx.acl;
const {Roles} = acl;
return Promise.all([
Expand All @@ -351,7 +380,33 @@ test('should get allowed resources for specified type', t => {
});
});

test('should get disallowed resources for all resource types', t => {
test('should get allowed resources includes inherits', () => {
const acl = ctx.acl;
const {Roles} = acl;
return Promise.all([
Roles.create('A'),
Roles.create('B'),
Roles.create('AB'),
]).then(([A, B, AB]) => {
return Promise.each([
() => Roles.inherit(AB, [A, B]),
() => acl.addUserRoles('tom', [AB]),
() => acl.allow(A, 'article:1', ['view']),
() => acl.allow(B, 'photo:1', ['view']),
() => acl.allow('tom', 'report', ['view']),
], fn => fn())
.then(() => acl.allowedResources('tom', 'view'))
.then(resources => {
assert.sameDeepMembers(resources, [
{type: 'article', id: '1'},
{type: 'photo', id: '1'},
{type: 'report', id: null}
]);
});
});
});

test('should get disallowed resources for all resource types', () => {
const acl = ctx.acl;
const {Roles} = acl;
return Promise.all([
Expand Down Expand Up @@ -381,7 +436,33 @@ test('should get disallowed resources for all resource types', t => {
});
});

test('should allowed resources for specified type', t => {
test('should get disallowed resources includes inherits', () => {
const acl = ctx.acl;
const {Roles} = acl;
return Promise.all([
Roles.create('A'),
Roles.create('B'),
Roles.create('C'),
Roles.create('AB'),
]).then(([A, B, C, AB]) => {
return Promise.each([
() => Roles.inherit(AB, [A, B]),
() => acl.addUserRoles('tom', [AB]),
() => acl.allow(A, 'article:1', ['view']),
() => acl.allow(B, 'photo:1', ['view']),
() => acl.allow(C, 'folder:1', ['view']),
() => acl.allow('tom', 'report', ['view']),
], fn => fn())
.then(() => acl.disallowedResources('tom', 'view'))
.then(resources => {
assert.sameDeepMembers(resources, [
{type: 'folder', id: '1'},
]);
});
});
});

test('should allowed resources for specified type', () => {
const acl = ctx.acl;
const {Roles} = acl;
return Promise.all([
Expand All @@ -406,7 +487,7 @@ test('should allowed resources for specified type', t => {
});
});

test('should remove all permissions for resource type', t => {
test('should remove all permissions for resource type', () => {
const acl = ctx.acl;
const {Roles} = acl;
return Promise.all([
Expand All @@ -430,7 +511,7 @@ test('should remove all permissions for resource type', t => {
});
});

test('should remove all permissions for individual resource', t => {
test('should remove all permissions for individual resource', () => {
const acl = ctx.acl;
const {Roles} = acl;
return Promise.all([
Expand Down

0 comments on commit d3e250b

Please sign in to comment.