Skip to content

Commit

Permalink
Merge pull request #14 from franciscogouveia/13-async-dataretrievers
Browse files Browse the repository at this point in the history
Async dataretrievers
  • Loading branch information
franciscogouveia committed Feb 17, 2016
2 parents 93a8e63 + 225efae commit f1be0c2
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 35 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,23 @@ Changes to `dataRetrieverRouter` will influence `dataRetrieverRouter1` and `data

Changes to any of `dataRetrieverRouter1` or `dataRetrieverRouter2` will not cause influence on any data retriever routers, but themselves.

Contexts are preserver per data retriever router.
Contexts are preserved per data retriever router.

You can also get data from data retriever router

```js
dataRetrieverRouter.get('credentials:username', (err, result) => {
...
});
```

And you can override the context on get, by passing it in the second argument

```js
dataRetrieverRouter.get('credentials:username', { username: 'the_overrider', group: ['anonymous'] }, (err, result) => {
...
});
```

## Learn more about _Rule Based Access Control_

Expand Down
52 changes: 43 additions & 9 deletions lib/DataRetrievalRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ schemas.DataRetrievalRouter_register_handles = Joi.alternatives().try(
Joi.string().min(1),
Joi.array().items(Joi.string().min(1))
);
schemas.DataRetrievalRouter_register_retriever = Joi.func().arity(3);
schemas.DataRetrievalRouter_register_retriever = Joi.func().minArity(3).maxArity(4);
schemas.DataRetrievalRouter_register_options = Joi.object({
override: Joi.boolean().optional()
}).unknown(false);
Expand All @@ -91,11 +91,22 @@ internals.DataRetrievalRouter.prototype._register = function (handles, retriever
/**
* Obtain data from a retriever.
*
* * source - E.g. 'credentials' to obtain data from credentials document
* * key - Key value from the source (e.g. 'username')
* * context - Context object. Contains the request object.
* * key - Key value from the source (e.g. 'credentials:username')
* * context - (optional) Context object. Contains the request object.
* * callback - Function with the signature (err, result)
**/
internals.DataRetrievalRouter.prototype.get = function (key, context) {
internals.DataRetrievalRouter.prototype.get = function (key, context, callback) {

if (!callback) {
if (context && context instanceof Function) {

callback = context;
context = null;
} else {

throw new Error('Callback not given');
}
}

Joi.assert(key, schemas.DataRetrievalRouter_get_key);
let source;
Expand All @@ -114,14 +125,37 @@ internals.DataRetrievalRouter.prototype.get = function (key, context) {
Joi.assert(subkey, schemas.DataRetrievalRouter_get_key);
Joi.assert(source, schemas.DataRetrievalRouter_get_source);

if (!this.retrievers[source]) {
const fn = this.retrievers[source];

if (!fn) {

if (!this.parent) {
return null;

return callback(null, null);
}
return this.parent.get(key, context || this.context);

return this.parent.get(key, context || this.context, callback);
}

if (fn.length > 3) {

// has callback
try {
return fn(source, subkey, context || this.context, callback);
} catch(e) {
return callback(e);
}
}

let value;

try {
value = fn(source, subkey, context || this.context);
} catch(e) {
return callback(e);
}

return this.retrievers[source](source, subkey, context || this.context);
callback(null, value);
};
schemas.DataRetrievalRouter_get_source = Joi.string().min(1);
schemas.DataRetrievalRouter_get_key = Joi.string().min(1);
65 changes: 50 additions & 15 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,29 +149,64 @@ internals.evaluateTarget = (target, dataRetriever, callback) => {
target = [target];
}

const tasks = [];

for (const index in target) {

const element = target[index];
let fullyMatches = true;

for (const key in element) {
const value = dataRetriever.get(key);
const result = internals._targetApplies(element[key], value);
tasks.push(internals.evaluateTargetElement(dataRetriever, element));
}

if (!result) {
// At least one key didn't match
fullyMatches = false;
break;
}
Async.parallel(tasks, (err, result) => {

if (err) {
return callback(err);
}

if (fullyMatches) {
// At least one element matches
return callback(null, true);
// At least one should apply (OR)
const applicables = result.filter((value) => value);

callback(null, applicables.length > 0);
});
};

internals.evaluateTargetElement = (dataRetriever, element) => {

return (callback) => {

const tasks = [];

for (const key in element) {

tasks.push(internals.evaluateTargetElementKey(dataRetriever, element, key));
}
}

// No matches
return callback(null, false);
Async.parallel(tasks, (err, result) => {

if (err) {
return callback(err);
}

// Should all apply (AND)
const nonApplicable = result.filter((value) => !value);

callback(null, nonApplicable.length === 0);
});
};
};

internals.evaluateTargetElementKey = (dataRetriever, element, key) => {

return (callback) => {

dataRetriever.get(key, null, (err, value) => {

const result = internals._targetApplies(element[key], value);

callback(null, !!result);
});
};
};

/**
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rbac-core",
"version": "1.0.0",
"version": "2.0.0",
"description": "RBAC core from hapi-rbac",
"main": "lib/index.js",
"directories": {
Expand Down
158 changes: 149 additions & 9 deletions test/dataretriever.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ experiment('RBAC internal modular information retrieval', () => {

dataRetriever.register('test', retriever);

expect(dataRetriever.get('test:x')).to.equal('key-x');
dataRetriever.get('test:x', (err, result) => {

done();
expect(err).to.not.exist();
expect(result).to.equal('key-x');
done();
});
});

test('should override a valid retriever (single handler)', (done) => {
Expand All @@ -46,9 +49,14 @@ experiment('RBAC internal modular information retrieval', () => {
dataRetriever.register('test-override', retriever1);
dataRetriever.register('test-override', retriever2, { override: true });

expect(dataRetriever.get('test-override:test', {})).to.equal('test-2');
dataRetriever.get('test-override:test', (err, result) => {

expect(err).to.not.exist();
expect(result).to.equal('test-2');

done();
});

done();
});

test('should not override a valid retriever (single handler)', (done) => {
Expand Down Expand Up @@ -85,12 +93,32 @@ experiment('RBAC internal modular information retrieval', () => {
dataRetriever.register(['test-override-multiple-1', 'test-override-multiple-2', 'test-override-multiple-3'], retriever1);
dataRetriever.register(['test-override-multiple-2', 'test-override-multiple-4'], retriever2, { override: true }); // test-override-multiple-2 collides

expect(dataRetriever.get('test-override-multiple-1:test', { })).to.equal('test-1');
expect(dataRetriever.get('test-override-multiple-2:test', { })).to.equal('test-2');
expect(dataRetriever.get('test-override-multiple-3:test', { })).to.equal('test-1');
expect(dataRetriever.get('test-override-multiple-4:test', { })).to.equal('test-2');
dataRetriever.get('test-override-multiple-1:test', (err, result) => {

done();
expect(err).to.not.exist();
expect(result).to.equal('test-1');

dataRetriever.get('test-override-multiple-2:test', (err, result) => {

expect(err).to.not.exist();
expect(result).to.equal('test-2');


dataRetriever.get('test-override-multiple-3:test', (err, result) => {

expect(err).to.not.exist();
expect(result).to.equal('test-1');

dataRetriever.get('test-override-multiple-4:test', (err, result) => {

expect(err).to.not.exist();
expect(result).to.equal('test-2');

done();
});
});
});
});
});

test('should not override a valid retriever (multiple handlers)', (done) => {
Expand All @@ -111,4 +139,116 @@ experiment('RBAC internal modular information retrieval', () => {
done();
});

test('should register a valid asynchronous retriever', (done) => {

const retriever = (source, key, context, callback) => {

callback(null, 'key-' + key);
};

dataRetriever.register('async-test', retriever);

dataRetriever.get('async-test:x', (err, result) => {

expect(err).to.not.exist();
expect(result).to.equal('key-x');
done();
});
});

test('should use parent asynchronous retriever', (done) => {

const retriever = (source, key, context, callback) => {

callback(null, 'key-' + key);
};

dataRetriever.register('async-parent-test-1', retriever);

const childDataRetriever = dataRetriever.createChild();

childDataRetriever.get('async-parent-test-1:x', (err, result) => {

expect(err).to.not.exist();
expect(result).to.equal('key-x');
done();
});
});

test('should use parent synchronous retriever', (done) => {

const retriever = (source, key, context) => {

return 'key-' + key;
};

dataRetriever.register('sync-parent-test-1', retriever);

const childDataRetriever = dataRetriever.createChild();

childDataRetriever.get('sync-parent-test-1:x', (err, result) => {

expect(err).to.not.exist();
expect(result).to.equal('key-x');
done();
});
});

test('should return null if inexistent prefix on child and parent', (done) => {

const childDataRetriever = dataRetriever.createChild();

childDataRetriever.get('this-does-not-exist-1:x', (err, result) => {

expect(err).to.not.exist();
expect(result).to.not.exist();
done();
});
});

test('should not allow using get with context and without callback', (done) => {

expect(dataRetriever.get.bind(null, 'get-with-context-without-callback:x', {})).to.throw(Error);
done()
});

test('should not allow using get without context and without callback', (done) => {

expect(dataRetriever.get.bind(null, 'get-with-context-without-callback:x', {})).to.throw(Error);
done()
});

test('should return err in callback when an error is thrown (sync)', (done) => {

const retriever = (source, key, context) => {

throw new Error('test');
};

dataRetriever.register('sync-test-err-1', retriever);

dataRetriever.get('sync-test-err-1:x', (err, result) => {

expect(err).to.exist();

done();
});
});

test('should return err in callback when an error is thrown (async)', (done) => {

const retriever = (source, key, context, callback) => {

throw new Error('test');
};

dataRetriever.register('async-test-err-1', retriever);

dataRetriever.get('async-test-err-1:x', (err, result) => {

expect(err).to.exist();

done();
});
});
});

0 comments on commit f1be0c2

Please sign in to comment.