Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 103 additions & 113 deletions auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,77 @@ function fastifyAuth (fastify, opts, next) {
next()
}

function treeCallback (funcs, predicate, accept, reject, decisiveAccept, earlyStop) {
let i = 0
const decisiveCallback = decisiveAccept ? accept : reject
const earlyExit = earlyStop
? decisiveCallback
: (...args) => {
function exec () {
const func = funcs[i]
i += 1
if (i <= funcs.length) {
return predicate(func, exec, exec)
} else {
return decisiveCallback(...args)
}
}
return exec()
}
function exec () {
const func = funcs[i]
i += 1
if (i < funcs.length) {
if (decisiveAccept) {
return predicate(func, earlyExit, exec)
} else {
return predicate(func, exec, earlyExit)
}
} else {
return predicate(func, accept, reject)
}
}
return exec()
}

function someCallback (funcs, predicate, accept, reject, earlyStop) {
if (funcs.length > 0) {
return treeCallback(funcs, predicate, accept, reject, true, earlyStop)
}
return reject()
}

function everyCallback (funcs, predicate, accept, reject, earlyStop) {
if (funcs.length > 0) {
return treeCallback(funcs, predicate, accept, reject, false, earlyStop)
}
return accept()
}

function vectorPredicate (gate, predicate, earlyStop) {
return function (func, accept, reject) {
if (Array.isArray(func)) {
return gate(func, predicate, accept, reject, earlyStop)
} else {
return predicate(func, accept, reject)
}
}
}

function orGate (funcs, predicate, accept, reject, earlyStop) {
return someCallback(
funcs, vectorPredicate(andGate, predicate, earlyStop),
accept, reject, earlyStop
)
}

function andGate (funcs, predicate, accept, reject, earlyStop) {
return everyCallback(
funcs, vectorPredicate(orGate, predicate, earlyStop),
accept, reject, earlyStop
)
}

/** @param {import('./types/index').FastifyAuthPluginOptions} pluginOptions */
function auth (pluginOptions) {
return function (functions, opts) {
Expand All @@ -41,20 +112,16 @@ function auth (pluginOptions) {
throw new Error('The value of options.run must be \'all\'')
}

const functionsLength = functions.length
for (let i = 0; i < functionsLength; i++) {
if (Array.isArray(functions[i]) === false) {
functions[i] = functions[i].bind(this)
} else {
const subArrayLength = functions[i].length
for (let j = 0; j < subArrayLength; j++) {
if (Array.isArray(functions[i][j])) {
throw new TypeError('Nesting sub-arrays is not supported')
}
functions[i][j] = functions[i][j].bind(this)
const bindall = (funcs) => {
for (const [i, func] of funcs.entries()) {
if (Array.isArray(func)) {
bindall(funcs[i])
} else {
funcs[i] = func.bind(this)
}
}
}
bindall(functions)

const instance = reusify(Auth)

Expand All @@ -66,134 +133,57 @@ function auth (pluginOptions) {
obj.done = done
obj.functions = this.functions
obj.options = this.options
obj.i = 0
obj.j = 0
obj.currentError = null
obj.skipFurtherErrors = false
obj.skipFurtherArrayErrors = false

obj.nextAuth()
obj.doAuth()
}

return _auth.bind({ functions, options })

function Auth () {
this.next = null
this.i = 0
this.j = 0
this.functions = []
this.options = {}
this.request = null
this.reply = null
this.done = null
this.currentError = null
this.skipFurtherErrors = false
this.skipFurtherArrayErrors = false

const that = this

this.nextAuth = function nextAuth (err) {
if (!that.skipFurtherErrors) that.currentError = err

const func = that.functions[that.i++]
if (!func) {
return that.completeAuth()
}

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
return that.completeAuth()
}
that.nextAuth(err)
}
}
})
}
}

this.processAuthArray = function processAuthArray (funcs, callback, err) {
const func = funcs[that.j++]
if (!func) return callback(err)

that.processAuth(func, (err) => {
if (that.options.relation === 'and') { // sub-array relation is OR
if (!err && that.options.run !== 'all') {
callback(err)
} else {
if (!err && that.options.run === 'all') {
that.skipFurtherArrayErrors = true
}
that.processAuthArray(funcs, callback, that.skipFurtherArrayErrors ? null : err)
}
} else { // sub-array relation is AND
if (err && that.options.run !== 'all') callback(err)
else that.processAuthArray(funcs, callback, err)
}
})
this.doAuth = function doAuth () {
const earlyStop = that.options.run !== 'all'
const gate = that.options.relation === 'or' ? orGate : andGate
return gate(
that.functions,
that.processAuth,
that.acceptAuth,
that.rejectAuth,
earlyStop
)
}

this.processAuth = function processAuth (func, callback) {
this.processAuth = function processAuth (func, accept, reject) {
try {
const maybePromise = func(that.request, that.reply, callback)
const maybePromise = func(that.request, that.reply, (err) => err ? reject(err) : accept())

if (maybePromise && typeof maybePromise.then === 'function') {
maybePromise.then(() => callback(null), callback)
maybePromise.then(accept, reject)
}
} catch (err) {
callback(err)
reject(err)
}
}

this.completeAuth = function completeAuth () {
if (that.currentError && (!that.reply.raw.statusCode || that.reply.raw.statusCode < 400)) {
this.rejectAuth = function rejectAuth (err) {
if (!that.reply.raw.statusCode || that.reply.raw.statusCode < 400) {
that.reply.code(401)
} else if (!that.currentError && that.reply.raw.statusCode && that.reply.raw.statusCode >= 400) {
that.reply.code(200)
}
that.done(err || new Error('sentinel'))
instance.release(that)
}

that.done(that.currentError)
this.acceptAuth = function acceptAuth () {
if (that.reply.raw.statusCode && that.reply.raw.statusCode >= 400) {
that.reply.code(200)
}
that.done()
instance.release(that)
}
}
Expand Down
10 changes: 0 additions & 10 deletions test/example-composited.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -842,16 +842,6 @@ test('Or Relation run all fail', (t, done) => {
})
})

test('Nested sub-arrays not supported', (t, done) => {
t.plan(1)
try {
fastify.auth([[fastify.verifyBig, [fastify.verifyNumber]]])
} catch (err) {
t.assert.deepStrictEqual(err.message, 'Nesting sub-arrays is not supported')
done()
}
})

test('And Relation run all', (t, done) => {
t.plan(2)

Expand Down
88 changes: 88 additions & 0 deletions test/logic-gate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use strict'

const auth = require('../auth')

const { test } = require('node:test')

let fuzzTryAuth
test.before(() => {
const fakify = {
decorate (str, obj) {
this[str] = obj
}
}

auth(fakify, {}, (err) => {
if (err) {
console.log('Next function callback called with an error', err)
}
})

const t = (_a, _b, done) => done()
const f = (_a, _b, done) => done(new Error('false'))
const evalOr = (v) => v.some(vi => Array.isArray(vi) ? evalAnd(vi) : vi)
const evalAnd = (v) => v.every(vi => Array.isArray(vi) ? evalOr(vi) : vi)
const boolsAsFunc = (arr) => arr.map((bi) => Array.isArray(bi) ? boolsAsFunc(bi) : (bi ? t : f))

function tryAuthWith (arr, relation) {
test(`Relation ${relation} for expression ${JSON.stringify(arr)}`, (t, done) => {
t.plan(1)
const expectedResult = relation === 'or' ? evalOr(arr) : evalAnd(arr)
const funcs = boolsAsFunc(arr)
const authFunc = fakify.auth(funcs, { relation })
const req = {}
const reply = {
raw: { statusCode: undefined },
code: (value) => value
}
let result
authFunc(req, reply, err => { result = !err })
t.assert.equal(result, expectedResult)
done()
})
}

function * fuzzShape (arr) {
if (!Array.isArray(arr)) {
if (arr === 1) yield * [false, true]
else yield arr
return
}
if (arr.length === 0) {
yield []
return
}
for (const head of fuzzShape(arr[0])) {
for (const tail of fuzzShape(arr.slice(1))) {
yield [head, ...tail]
}
}
}

fuzzTryAuth = function (shape) {
for (const arr of fuzzShape(shape)) {
tryAuthWith(arr, 'or')
tryAuthWith(arr, 'and')
}
}
})

for (const shape of
[
[1, 1],
[1, [1, 1], 1, 1],
[1, [1, 1], 1],
[1, [1, 1], [1, 1]],
[1, [1, 1]],
[1],
[[1, 1], 1],
[[1, 1], [1, 1]],
[[1, 1]],
[[1], 1],
[[1]],
[[]],
[1, [[1, 1], 1]],
]
) {
fuzzTryAuth(shape)
}
Loading