diff --git a/README.md b/README.md index 392c4b4..0997df2 100644 --- a/README.md +++ b/README.md @@ -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_ diff --git a/lib/DataRetrievalRouter.js b/lib/DataRetrievalRouter.js index aef5290..89578e8 100644 --- a/lib/DataRetrievalRouter.js +++ b/lib/DataRetrievalRouter.js @@ -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); @@ -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; @@ -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); diff --git a/lib/index.js b/lib/index.js index af1c2b2..448e863 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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); + }); + }; }; /** diff --git a/package.json b/package.json index 2fc3713..d391611 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/test/dataretriever.js b/test/dataretriever.js index abb58ab..f15fd53 100644 --- a/test/dataretriever.js +++ b/test/dataretriever.js @@ -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) => { @@ -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) => { @@ -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) => { @@ -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(); + }); + }); });