diff --git a/README.md b/README.md index ac499e8..3e4818e 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,8 @@ fastify }) ``` -If you need composited authentication, such as verifying user account passwords and levels or meeting VIP eligibility criteria. e.g. [(verifyUserPassword `and` verifyLevel) `or` (verifyVIP)] +If you need composite authentication, such as verifying user account passwords and levels or meeting VIP eligibility criteria, you can use nested arrays. +For example if you need the following logic: [(verifyUserPassword `and` verifyLevel) `or` (verifyVIP)], it can be achieved with the code below: ```js fastify .decorate('verifyUserPassword', function (request, reply, done) { @@ -94,7 +95,7 @@ fastify method: 'POST', url: '/auth-multiple', preHandler: fastify.auth([ - [fastify.verifyUserPassword, fastify.verifyLevel], // The arrays within an array always have an AND relationship. + [fastify.verifyUserPassword, fastify.verifyLevel], // The arrays within an array have the opposite relation to the main (default) relation. fastify.verifyVIP ], { relation: 'or' // default relation @@ -107,6 +108,15 @@ fastify }) ``` +If the `relation` (`defaultRelation`) parameter is `and`, then the relation inside sub-arrays will be `or`. +If the `relation` (`defaultRelation`) parameter is `or`, then the relation inside sub-arrays will be `and`. + +| auth code | resulting logical expression | +| ------------- |:-------------:| +| `fastify.auth([f1, f2, [f3, f4]], { relation: 'or' })` | `f1 OR f2 OR (f3 AND f4)` | +| `fastify.auth([f1, f2, [f3, f4]], { relation: 'and' })` | `f1 AND f2 AND (f3 OR f4)` | + + You can use the `defaultRelation` option while registering the plugin, to change the default `relation`: ```js fastify.register(require('@fastify/auth'), { defaultRelation: 'and'} ) diff --git a/auth.js b/auth.js index e171da4..6f07711 100644 --- a/auth.js +++ b/auth.js @@ -40,10 +40,13 @@ function auth (pluginOptions) { /* eslint-disable-next-line no-var */ for (var i = 0; i < functions.length; i++) { if (Array.isArray(functions[i]) === false) { - functions[i] = [functions[i].bind(this)] + functions[i] = functions[i].bind(this) } else { /* eslint-disable-next-line no-var */ for (var j = 0; j < functions[i].length; j++) { + if (Array.isArray(functions[i][j])) { + throw new Error('Nesting sub-arrays is not supported') + } functions[i][j] = functions[i][j].bind(this) } } @@ -61,8 +64,9 @@ function auth (pluginOptions) { obj.options = this.options obj.i = 0 obj.j = 0 - obj.firstResult = null - obj.sufficient = false + obj.currentError = null + obj.skipFurtherErrors = false + obj.skipFurtherArrayErrors = false obj.nextAuth() } @@ -78,80 +82,114 @@ function auth (pluginOptions) { this.request = null this.reply = null this.done = null - this.firstResult = null - this.sufficient = false + this.currentError = null + this.skipFurtherErrors = false + this.skipFurtherArrayErrors = false const that = this this.nextAuth = function nextAuth (err) { - const func = that.functions[that.i][that.j++] + if (!that.skipFurtherErrors) that.currentError = err + const func = that.functions[that.i++] if (!func) { - that.completeAuthArray(err) - return + return that.completeAuth() } - try { - const maybePromise = func(that.request, that.reply, that.onAuth) - - if (maybePromise && typeof maybePromise.then === 'function') { - maybePromise.then(results => that.onAuth(null, results), that.onAuth) - } - } catch (err) { - this.onAuth(err) + if (!Array.isArray(func)) { + that.processAuth(func, (err) => { + if (that.options.run !== 'all') that.currentError = err + + if (that.options.relation === 'and') { + if (err && that.options.run !== 'all') { + that.completeAuth() + } else { + if (err && that.options.run === 'all' && !that.skipFurtherErrors) { + that.skipFurtherErrors = true + that.currentError = err + } + that.nextAuth(err) + } + } else { + if (!err && that.options.run !== 'all') { + that.completeAuth() + } else { + if (!err && that.options.run === 'all') { + that.skipFurtherErrors = true + that.currentError = null + } + that.nextAuth(err) + } + } + }) + } else { + that.j = 0 + that.skipFurtherArrayErrors = false + that.processAuthArray(func, (err) => { + if (that.options.relation === 'and') { // sub-array relation is OR + if (!err && that.options.run !== 'all') { + that.nextAuth(err) + } else { + that.currentError = err + that.nextAuth(err) + } + } else { // sub-array relation is AND + if (err && that.options.run !== 'all') { + that.currentError = err + that.nextAuth(err) + } else { + if (!err && that.options.run !== 'all') { + that.currentError = null + that.completeAuth() + } + that.nextAuth(err) + } + } + }) } } - this.onAuth = function onAuth (err, results) { - if (err) { - return that.completeAuthArray(err) - } + this.processAuthArray = function processAuthArray (funcs, callback, err) { + const func = funcs[that.j++] + if (!func) return callback(err) - return that.nextAuth(err) - } - - this.completeAuthArray = function (err) { - if (err) { - if (that.options.relation === 'and') { - if (that.options.run === 'all') { - that.firstResult = that.firstResult ?? err + that.processAuth(func, (err) => { + if (that.options.relation === 'and') { // sub-array relation is OR + if (!err && that.options.run !== 'all') { + callback(err) } else { - that.firstResult = err - this.completeAuth() - return + if (!err && that.options.run === 'all') { + that.skipFurtherArrayErrors = true + } + that.processAuthArray(funcs, callback, that.skipFurtherArrayErrors ? null : err) } - } else { - that.firstResult = that.sufficient ? null : err + } else { // sub-array relation is AND + if (err && that.options.run !== 'all') callback(err) + else that.processAuthArray(funcs, callback, err) } - } else { - if (that.options.relation === 'or') { - that.sufficient = true - that.firstResult = null + }) + } - if (that.options.run !== 'all') { - this.completeAuth() - return - } - } - } + this.processAuth = function processAuth (func, callback) { + try { + const maybePromise = func(that.request, that.reply, callback) - if (that.i < that.functions.length - 1) { - that.i += 1 - that.j = 0 - return that.nextAuth(err) + if (maybePromise && typeof maybePromise.then === 'function') { + maybePromise.then(() => callback(null), callback) + } + } catch (err) { + callback(err) } - - this.completeAuth() } this.completeAuth = function () { - if (that.firstResult && (!that.reply.raw.statusCode || that.reply.raw.statusCode < 400)) { + if (that.currentError && (!that.reply.raw.statusCode || that.reply.raw.statusCode < 400)) { that.reply.code(401) - } else if (!that.firstResult && that.reply.raw.statusCode && that.reply.raw.statusCode >= 400) { + } else if (!that.currentError && that.reply.raw.statusCode && that.reply.raw.statusCode >= 400) { that.reply.code(200) } - that.done(that.firstResult) + that.done(that.currentError) instance.release(that) } } diff --git a/test/example-composited.js b/test/example-composited.js index c6f580e..46e7cf5 100644 --- a/test/example-composited.js +++ b/test/example-composited.js @@ -12,6 +12,8 @@ function build (opts) { fastify.decorate('verifyNumber', verifyNumber) fastify.decorate('verifyOdd', verifyOdd) fastify.decorate('verifyBig', verifyBig) + fastify.decorate('verifyOddAsync', verifyOddAsync) + fastify.decorate('verifyBigAsync', verifyBigAsync) function verifyNumber (request, reply, done) { const n = request.body.n @@ -49,6 +51,24 @@ function build (opts) { return done() } + function verifyOddAsync (request, reply) { + return new Promise((resolve, reject) => { + verifyOdd(request, reply, (err) => { + if (err) reject(err) + resolve() + }) + }) + } + + function verifyBigAsync (request, reply) { + return new Promise((resolve, reject) => { + verifyBig(request, reply, (err) => { + if (err) reject(err) + resolve() + }) + }) + } + function routes () { fastify.route({ method: 'GET', @@ -78,6 +98,84 @@ function build (opts) { } }) + fastify.route({ + method: 'POST', + url: '/check-composite-and', + preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOdd, fastify.verifyBig]], { relation: 'and' }), + handler: (req, reply) => { + req.log.info('Auth route') + reply.send({ hello: 'world' }) + } + }) + + fastify.route({ + method: 'POST', + url: '/check-composite-and-async', + preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOddAsync, fastify.verifyBigAsync]], { relation: 'and' }), + handler: (req, reply) => { + req.log.info('Auth route') + reply.send({ hello: 'world' }) + } + }) + + fastify.route({ + method: 'POST', + url: '/check-composite-and-run-all', + preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOdd, fastify.verifyBig]], { relation: 'and', run: 'all' }), + handler: (req, reply) => { + req.log.info('Auth route') + reply.send({ + odd: req.odd, + big: req.big, + number: req.number + }) + } + }) + + fastify.route({ + method: 'POST', + url: '/check-composite-or', + preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOdd, fastify.verifyBig]], { relation: 'or' }), + handler: (req, reply) => { + req.log.info('Auth route') + reply.send({ hello: 'world' }) + } + }) + + fastify.route({ + method: 'POST', + url: '/check-two-sub-arrays-or', + preHandler: fastify.auth([[fastify.verifyBigAsync], [fastify.verifyOddAsync]], { relation: 'or' }), + handler: (req, reply) => { + req.log.info('Auth route') + reply.send({ hello: 'world' }) + } + }) + + fastify.route({ + method: 'POST', + url: '/check-composite-or-async', + preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOddAsync, fastify.verifyBigAsync]], { relation: 'or' }), + handler: (req, reply) => { + req.log.info('Auth route') + reply.send({ hello: 'world' }) + } + }) + + fastify.route({ + method: 'POST', + url: '/check-composite-or-run-all', + preHandler: fastify.auth([fastify.verifyNumber, [fastify.verifyOdd, fastify.verifyBig]], { relation: 'or', run: 'all' }), + handler: (req, reply) => { + req.log.info('Auth route') + reply.send({ + odd: req.odd, + big: req.big, + number: req.number + }) + } + }) + fastify.route({ method: 'POST', url: '/checkor', diff --git a/test/example-composited.test.js b/test/example-composited.test.js index 711a0ad..3460d4f 100644 --- a/test/example-composited.test.js +++ b/test/example-composited.test.js @@ -14,7 +14,7 @@ t.before(() => { fastify = build() }) -test('And Relation sucess for single case', t => { +test('And Relation success for single case', t => { t.plan(2) fastify.inject({ @@ -86,7 +86,7 @@ test('And Relation failed for single [Array] case', t => { }) }) -test('Or Relation sucess for single case', t => { +test('Or Relation success for single case', t => { t.plan(2) fastify.inject({ @@ -122,7 +122,7 @@ test('Or Relation failed for single case', t => { }) }) -test('Or Relation sucess for single [Array] case', t => { +test('Or Relation success for single [Array] case', t => { t.plan(2) fastify.inject({ @@ -252,6 +252,80 @@ test('[Array] notation And Relation success', t => { }) }) +test('And Relation with Or relation inside sub-array success', t => { + t.plan(3) + + fastify.inject({ + method: 'POST', + url: '/check-composite-and', + payload: { + n: 11 + } + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.same(payload, { hello: 'world' }) + t.equal(res.statusCode, 200) + }) +}) + +test('And Relation with Or relation inside sub-array failed', t => { + t.plan(2) + + fastify.inject({ + method: 'POST', + url: '/check-composite-and', + payload: { + n: 4 + } + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.same(payload, { + error: 'Unauthorized', + message: '`n` is not big', + statusCode: 401 + }) + }) +}) + +test('And Relation with Or relation inside sub-array with async functions success', t => { + t.plan(3) + + fastify.inject({ + method: 'POST', + url: '/check-composite-and-async', + payload: { + n: 11 + } + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.same(payload, { hello: 'world' }) + t.equal(res.statusCode, 200) + }) +}) + +test('And Relation with Or relation inside sub-array with async functions failed', t => { + t.plan(2) + + fastify.inject({ + method: 'POST', + url: '/check-composite-and-async', + payload: { + n: 4 + } + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.same(payload, { + error: 'Unauthorized', + message: '`n` is not big', + statusCode: 401 + }) + }) +}) + test('Or Relation success under first case', t => { t.plan(3) @@ -360,7 +434,7 @@ test('[Array] notation Or Relation failed for both case', t => { }) }) -test('single [Array] And Relation sucess', t => { +test('single [Array] And Relation success', t => { t.plan(2) fastify.inject({ @@ -396,7 +470,43 @@ test('single [Array] And Relation failed', t => { }) }) -test('[Array] notation & single case Or Relation sucess under first case', t => { +test('Two sub-arrays Or Relation success', t => { + t.plan(2) + + fastify.inject({ + method: 'POST', + url: '/check-two-sub-arrays-or', + payload: { + n: 11 + } + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.same(payload, { hello: 'world' }) + }) +}) + +test('Two sub-arrays Or Relation fail', t => { + t.plan(2) + + fastify.inject({ + method: 'POST', + url: '/check-two-sub-arrays-or', + payload: { + n: 4 + } + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.same(payload, { + error: 'Unauthorized', + message: '`n` is not odd', + statusCode: 401 + }) + }) +}) + +test('[Array] notation & single case Or Relation success under first case', t => { t.plan(2) fastify.inject({ @@ -412,7 +522,7 @@ test('[Array] notation & single case Or Relation sucess under first case', t => }) }) -test('[Array] notation & single case Or Relation sucess under second case', t => { +test('[Array] notation & single case Or Relation success under second case', t => { t.plan(2) fastify.inject({ @@ -448,6 +558,46 @@ test('[Array] notation & single case Or Relation failed', t => { }) }) +test('And Relation with Or relation inside sub-array with run: all', t => { + t.plan(2) + + fastify.inject({ + method: 'POST', + url: '/check-composite-and-run-all', + payload: { + n: 11 + } + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.same(payload, { + odd: true, + big: false, + number: true + }) + }) +}) + +test('Or Relation with And relation inside sub-array with run: all', t => { + t.plan(2) + + fastify.inject({ + method: 'POST', + url: '/check-composite-or-run-all', + payload: { + n: 110 + } + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.same(payload, { + odd: false, + big: true, + number: true + }) + }) +}) + test('Check run all line fail with AND', t => { t.plan(8) @@ -633,6 +783,15 @@ test('Or Relation run all fail', t => { }) }) +test('Nested sub-arrays not supported', t => { + t.plan(1) + try { + fastify.auth([[fastify.verifyBig, [fastify.verifyNumber]]]) + } catch (err) { + t.same(err.message, 'Nesting sub-arrays is not supported') + } +}) + test('And Relation run all', t => { t.plan(2)