Skip to content
This repository has been archived by the owner on Jan 15, 2025. It is now read-only.

Commit

Permalink
Merge pull request #7 from chris-armstrong/check-cycles
Browse files Browse the repository at this point in the history
Check for cycles
  • Loading branch information
em0ney authored May 17, 2019
2 parents 8bd589c + 17cbd8c commit 1aa83a4
Show file tree
Hide file tree
Showing 6 changed files with 413 additions and 110 deletions.
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
language: node_js
node_js:
- "5"
- "4"
- "8"
- "10"
- "12"
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2017 GorillaStack
Copyright (c) 2019 GorillaStack

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
160 changes: 154 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,67 @@ function set(obj, path, value) {
obj[components[components.length - 1]] = value;
}

function find(list, predicate) {
// 1. Let O be ? ToObject(list value).
if (list === null || typeof list === 'undefined') {
throw new TypeError('"list" is null or not defined');
}

var o = Object(list);

// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;

// 3. If IsCallable(predicate) is false, throw a TypeError exception.
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}

// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
var thisArg = arguments[2];

// 5. Let k be 0.
var k = 0;

// 6. Repeat, while k < len
while (k < len) {
// a. Let Pk be ! ToString(k).
// b. Let kValue be ? Get(O, Pk).
// c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
// d. If testResult is true, return kValue.
var kValue = o[k];
if (predicate.call(thisArg, kValue, k, o)) {
return kValue;
}
// e. Increase k by 1.
k++;
}

// 7. Return undefined.
return undefined;
}

function partition(collection, predicate) {
var matches = [], fails = [];
var i;
for (i in collection) {
var item = collection[i];
if (predicate(item)) {
matches.push(item);
} else {
fails.push(item);
}
}
return [matches, fails];
}

function itemNameMatching(name) {
return function(item) {
var existingName = Prop.get(item, 'name');
return existingName === name;
};
}

var Prop = {
set: set,
get: get
Expand Down Expand Up @@ -601,9 +662,9 @@ Acl.prototype.inheritsResource = function(resource, inherit, onlyParent) {
*/
Acl.prototype.removeResource = function(resource) {
var resourceId = this.getResource(resource).getResourceId();

var resourcesRemoved = [resourceId];

var resourceParent = this.resources[resourceId]['parent'];
if (resourceParent) {
delete this.resources[resourceParent.getResourceId()]['children'][resourceId];
Expand Down Expand Up @@ -1380,6 +1441,90 @@ Acl.prototype.getResources = function() {
return Object.keys(this.resources);
};

/**
* @private
*
* Checks the given list of names, that
* all exist in the list of resources/roles
*
* @param {array<object>} list list of roles / resources
* @param {array<string>} names names to check
*
* @returns {boolean} true when all the names exist
*/
Acl._isResolved = function(list, names) {
if (!names) {
return true;
}
names = typeof names === 'string' ? [names] : names;

var i, j;
for (i in names) {
var name = names[i];
var exists = find(list, itemNameMatching(name));

if (!exists) {
return false;
}
}
return true;
}

/**
* @private
*
* Checks that the given list of parent names all exist in
* the allNames list. Throws an exception if this is not the
* case.
*/
Acl._checkAllExists = function(allNames, parents) {
parents = typeof parents === 'string' ? [parents] : parents;
parents.forEach(function(parent) {
if (allNames.indexOf(parent) < 0) {
throw new Error("parent '" + parent + "' does not exist");
}
});
}

/**
* @private
*
* This function orders a list of roles or resources based
* on their parent structures, checking for cycles and
* non-existent parent references.
*/
Acl._orderAndCycleCheck = function(list) {
// check for non-existent parents
var allNames = list.map(function (item) { return item.name; });

var unresolved = [].concat(list);
var resolved = [];

var firstTime = true;

while (unresolved.length > 0) {
var results = partition(unresolved, function(item) {
var parents = Prop.get(item, 'parent');
if (firstTime && parents) {
Acl._checkAllExists(allNames, parents);
}
return Acl._isResolved(resolved, parents);
});
firstTime = false;

var newResolved = results[0];

// when nothing is resolved, it could be a cycle
if (newResolved.length === 0) {
throw new Error('cycle detected');
}

resolved = resolved.concat(newResolved);
unresolved = results[1];
}
return resolved;
}

/**
* Loads permissions into the ACL.
*/
Expand All @@ -1392,17 +1537,20 @@ Acl.prototype.load = function(permissions) {

var i;

for (i in permissions.roles) {
var role = permissions.roles[i];
var roles = Acl._orderAndCycleCheck(permissions.roles);
var resources = Acl._orderAndCycleCheck(permissions.resources);

for (i in roles) {
var role = roles[i];

var name = Prop.get(role, 'name');
var parent = Prop.get(role, 'parent');

this.addRole(name, parent);
}

for (i in permissions.resources) {
var resource = permissions.resources[i];
for (i in resources) {
var resource = resources[i];

var name = Prop.get(resource, 'name');
var parent = Prop.get(resource, 'parent');
Expand Down
57 changes: 25 additions & 32 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"author": "gorillastack.com",
"license": "MIT",
"devDependencies": {
"jasmine": "^2.5.3"
"jasmine": "^3.4.0"
}
}
Loading

0 comments on commit 1aa83a4

Please sign in to comment.