Skip to content
This repository has been archived by the owner on Apr 17, 2023. It is now read-only.

Commit

Permalink
fix: remove dependency on joi, use our own validation logic
Browse files Browse the repository at this point in the history
  • Loading branch information
Dara Hayes committed Jul 11, 2019
1 parent 16707ac commit 6e1808d
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 31 deletions.
8 changes: 2 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,19 @@
},
"dependencies": {
"apollo-server-express": "2.6.7",
"express-session": "1.16.2",
"graphql-tools": "4.0.5",
"joi": "14.3.1",
"keycloak-connect": "6.0.1",
"pino": "5.12.6"
"keycloak-connect": "6.0.1"
},
"devDependencies": {
"@types/express-session": "1.15.13",
"@types/graphql": "14.2.2",
"@types/joi": "14.3.3",
"@types/keycloak-connect": "4.5.1",
"@types/node": "10.14.10",
"@types/pino": "5.20.0",
"@types/sinon": "^7.0.13",
"ava": "2.1.0",
"coveralls": "3.0.4",
"express": "4.17.1",
"express-session": "1.16.2",
"graphql": "14.4.1",
"keycloak-request-token": "^0.1.0",
"nyc": "14.1.1",
Expand Down
37 changes: 17 additions & 20 deletions src/directives/schemaDirectiveVisitors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { defaultFieldResolver, GraphQLSchema } from 'graphql'
import { SchemaDirectiveVisitor } from 'graphql-tools'
import Joi from 'joi'
import { auth, hasRole } from './directiveResolvers'
import { VisitableSchemaType } from 'graphql-tools/dist/schemaVisitor'

Expand Down Expand Up @@ -37,27 +36,25 @@ export class HasRoleDirective extends SchemaDirectiveVisitor {

public visitFieldDefinition (field: any) {
const { resolve = defaultFieldResolver } = field
const { error, value } = this.validateArgs()
if (error) {
throw error
}

const { roles } = value

const roles = this.validateArgs(this.args)
field.resolve = hasRole(roles)(resolve)
}

public validateArgs () {
// joi is dope. Read the docs and discover the magic.
// https://github.com/hapijs/joi/blob/master/API.md
const argsSchema = Joi.object({
role: Joi.array().required().items(Joi.string()).single()
})

const result = argsSchema.validate(this.args)

// result.value.role will be an array so it makes sense to add the roles alias
result.value.roles = result.value.role
return result
// validate a potential string or array of values
// if an array is provided, cast all values to strings
public validateArgs (args: {[name: string]: any}): Array<string> {
const keys = Object.keys(args)

if (keys.length === 1 && keys[0] === 'role') {
const role = args[keys[0]]
if (typeof role == 'string') {
return [role]
} else if (Array.isArray(role)) {
return role.map(val => String(val))
} else {
throw new Error(`invalid hasRole args. role must be a String or an Array of Strings`)
}
}
throw Error('invalid hasRole args. must contain only a \'role\ argument')
}
}
93 changes: 88 additions & 5 deletions test/hasRole.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import test from 'ava'
import sinon from 'sinon'

import Keycloak from 'keycloak-connect'
import { GraphQLSchema } from 'graphql'
import { GraphQLSchema, validate } from 'graphql'
import { VisitableSchemaType } from 'graphql-tools/dist/schemaVisitor'
import { HasRoleDirective } from '../src/directives/schemaDirectiveVisitors'

import { KeycloakContext } from '../src/KeycloakContext'

const createHasRoleDirective = (directiveArgs: any) => {
Expand Down Expand Up @@ -208,7 +208,7 @@ test('if token does not have the required role, then an error is returned and th
}, `User is not authorized. Must have one of the following roles: [${directiveArgs.role}]`)
})

test('if hasRole arguments are invalid, visitSchemaDirective does not throw, but field.resolve will return a generic error to the user and original resolver will not be called', async (t) => {
test('hasRole does not allow unkown arguments, visitFieldDefinition will throw', async (t) => {
const directiveArgs = {
role: 'admin',
some: 'unknown arg'
Expand All @@ -219,8 +219,47 @@ test('if hasRole arguments are invalid, visitSchemaDirective does not throw, but
const field = {
resolve: (root: any, args: any, context: any, info: any) => {
return new Promise((resolve, reject) => {
t.fail('the original resolver should never be called when an auth error is thrown')
return reject(new Error('the original resolver should never be called when an auth error is thrown'))
t.fail('the original resolver should never be called')
})
},
name: 'testField'
}

t.throws(() => {
directive.visitFieldDefinition(field)
})
})

test('hasRole does not allow a non string value for role, visitFieldDefinition will throw', async (t) => {
const directiveArgs = {
role: 123
}

const directive = createHasRoleDirective(directiveArgs)

const field = {
resolve: (root: any, args: any, context: any, info: any) => {
return new Promise((resolve, reject) => {
t.fail('the original resolver should never be called')
})
},
name: 'testField'
}

t.throws(() => {
directive.visitFieldDefinition(field)
})
})

test('hasRole must contain role arg, visitFieldDefinition will throw', async (t) => {
const directiveArgs = {}

const directive = createHasRoleDirective(directiveArgs)

const field = {
resolve: (root: any, args: any, context: any, info: any) => {
return new Promise((resolve, reject) => {
t.fail('the original resolver should never be called')
})
},
name: 'testField'
Expand All @@ -231,6 +270,50 @@ test('if hasRole arguments are invalid, visitSchemaDirective does not throw, but
})
})

test('hasRole role arg can be an array, visitFieldDefinition will not throw', async (t) => {
const directiveArgs = {
role: ['admin', 'developer']
}

const directive = createHasRoleDirective(directiveArgs)

const field = {
resolve: (root: any, args: any, context: any, info: any) => {
return new Promise((resolve, reject) => {
t.fail('the original resolver should never be called')
})
},
name: 'testField'
}

t.notThrows(() => {
directive.visitFieldDefinition(field)
})
})

test('hasRole role arg can be an array, non string values will be converted, visitFieldDefinition will not throw', async (t) => {
t.plan(1)
const directiveArgs = {
role: ['admin', 1, 1.234]
}

const expectedValue = ['admin', '1', '1.234']


const directive = createHasRoleDirective(directiveArgs)

const field = {
resolve: (root: any, args: any, context: any, info: any) => {},
name: 'testField'
}

const validateSpy = sinon.spy(directive, 'validateArgs')

directive.visitFieldDefinition(field)

t.deepEqual(expectedValue, validateSpy.returnValues[0])
})

test('context.auth.hasRole() works even if request is not supplied in context', async (t) => {
t.plan(3)
const directiveArgs = {
Expand Down

0 comments on commit 6e1808d

Please sign in to comment.