Graphql-shield: Reusable rules: hasRole functionality

Created on 30 May 2018  路  12Comments  路  Source: maticzav/graphql-shield

I just updated to graphql-shield v2.0.6 and now I get this error on running my Node.js app:

RangeError: Maximum call stack size exceeded
    at convertRulesToMiddleware (/path-to-server/server/node_modules/graphql-shield/dist/index.js:1:1)
    at leafs.reduce (/path-to-server/server/node_modules/graphql-shield/dist/index.js:232:84)
    at Array.reduce (<anonymous>)
    at convertRulesToMiddleware (/path-to-server/server/node_modules/graphql-shield/dist/index.js:232:30)
    at leafs.reduce (/path-to-server/server/node_modules/graphql-shield/dist/index.js:232:84)
    at Array.reduce (<anonymous>)
    at convertRulesToMiddleware (/path-to-server/server/node_modules/graphql-shield/dist/index.js:232:30)
    at leafs.reduce (/path-to-server/server/node_modules/graphql-shield/dist/index.js:232:84)
    at Array.reduce (<anonymous>)
    at convertRulesToMiddleware (/path-to-server/server/node_modules/graphql-shield/dist/index.js:232:30)
    at leafs.reduce (/path-to-server/server/node_modules/graphql-shield/dist/index.js:232:84)
    at Array.reduce (<anonymous>)
    at convertRulesToMiddleware (/path-to-server/server/node_modules/graphql-shield/dist/index.js:232:30)
    at leafs.reduce (/path-to-server/server/node_modules/graphql-shield/dist/index.js:232:84)
    at Array.reduce (<anonymous>)
    at convertRulesToMiddleware (/path-to-server/server/node_modules/graphql-shield/dist/index.js:232:30)

Most helpful comment

@dakshshah96 I have just noticed it. You are not using graphql-yoga. Because of this, you cannot use middlewares property to assign middleware to your schema. To make this work, you have to use graphql-middleware like this;

import { applyMiddleware } from 'graphql-middleware'

const schema = makeExecutableSchema({ typeDefs, resolvers })
const schemaWithPermissions = applyMiddleware(schema, permissions)

Hope this helps you out! 馃檪

All 12 comments

@dakshshah96 thanks for reporting this! Could you also provide your permission system which caused the error? This would be extremely helpful to reproduce the error and fix it! 馃檪

@maticzav No problem! 馃檪

I didn't get your question though. What exactly do you mean by permission system?

Great! No problem, let me express myself better.

I assume you've written a set of rules and assigned them to specific fields. Could you share the part where you assign rules to your schema using shield function? Rules themselves could be useful too, but are not necessary. 馃檪

@maticzav Got it! Here it is:

const { shield } = require('graphql-shield')
const { permissions } = require('../helpers/auth')
const { makeExecutableSchema } = require('graphql-tools')
const typeDefs = require('./types')
const resolvers = require('./resolvers')

const schema = makeExecutableSchema({ typeDefs, resolvers: shield(resolvers, permissions, { cache: false }) })

module.exports = { schema }

@dakshshah96 I see! The new version of GraphQL Shield introduced significant API changes - that's why we made a major version bump (2.x.x).

You can find more about implementing the new version in a short article I published https://medium.com/@maticzavadlal/graphql-shield-9d1e02520e35 .

Hope this helps you solve the problem 馃檪

Hey @maticzav, thanks for the clarification! Is this the correct way to implement it? None of my permissions are enforced.

auth.js

const { shield } = require('graphql-shield')
const jwt = require('jsonwebtoken')
const fs = require('fs')
const publicKey = fs.readFileSync(`${__dirname}/rsa/public.pem`)

// role based authorization
const hasRole = (allowedRoles) => (parent, args, context, info) => {
  const Authorization = context.get('authorization')
  if (Authorization) {
    const token = Authorization.replace('Bearer ', '')
    try {
      const decoded = jwt.verify(token, publicKey, { issuer: 'server', algorithms: ['RS256'] })
      context.loggedId = decoded.userId
      context.loggedRole = decoded.role
      return allowedRoles.includes(decoded.role)
    } catch (err) {
      return false
    }
  } else {
    return false
  }
}

/*
    permission definitions (none, admin, user)
*/

const Query = {
  Query: {
    // admin
    allAdmins: hasRole(['admin']),
    // user
    user: hasRole(['user']),
    getUser: hasRole(['admin', 'user']),
    allUsers: hasRole(['admin'])
  }
}

const Mutation = {
  Mutation: {
    // admin
    createAdmin: hasRole(['admin']),
    // user
    createUser: hasRole(['admin']),
    updateUser: hasRole(['admin']),
    verifyUser: hasRole(['admin', 'user']),
    destroyUser: hasRole(['admin'])
  }
}

// merge all permissions
const permissions = shield(Object.assign({}, Query, Mutation), { cache: false })

module.exports = { permissions }

index.js

const { permissions } = require('../helpers/auth')
const { makeExecutableSchema } = require('graphql-tools')
const typeDefs = require('./types')
const resolvers = require('./resolvers')

const schema = makeExecutableSchema({ typeDefs, resolvers, middlewares: [permissions] })

module.exports = { schema }

This is how my /graphql endpoint looks:

app.use('/graphql', bodyParser.json(), graphqlExpress(req => ({ formatError, schema, context: req, validationRules: [NoIntrospection, depthLimit(7)] })))

Sorry for bothering you with this implementation based question 馃檪

@dakshshah96 this looks great! The only problem I noticed is lack of rule assignment. Everything else should work as expected the way you put it; only the cache might be of a problem. I have noticed that you disabled cache, but will post an update and explanation of how to construct rules to make them cachable. 馃檪

const hasRole = (allowedRoles) => rule(`has-role-${allowedRoles}`)(async (parent, args, context, info) => {
  const Authorization = context.get('authorization')
  if (Authorization) {
    const token = Authorization.replace('Bearer ', '')
    try {
      const decoded = jwt.verify(token, publicKey, { issuer: 'server', algorithms: ['RS256'] })
      context.loggedId = decoded.userId
      context.loggedRole = decoded.role
      return allowedRoles.includes(decoded.role)
    } catch (err) {
      return false
    }
  } else {
    return false
  }
})

Let me explain what happens here. Since our rules get redefined every time we assign it to a new field (even if the role is the same), Shield hardly guesses what should and what shouldn't be cached together. Because of this, we have to manually define rule name using

rule(`has-role-${allowedRoles}`)

This tells Shield that we know how our data should be bundled. Therefore, we use a specific name to address it.

@maticzav Thanks, I see!

const { rule, shield } = require('graphql-shield')
const jwt = require('jsonwebtoken')
const fs = require('fs')
const publicKey = fs.readFileSync(`${__dirname}/rsa/public.pem`)

// role based authorization
const hasRole = rule()((allowedRoles) => (parent, args, context, info) => {
  const Authorization = context.get('authorization')
  if (Authorization) {
    const token = Authorization.replace('Bearer ', '')
    try {
      const decoded = jwt.verify(token, publicKey, { issuer: 'server', algorithms: ['RS256'] })
      context.loggedId = decoded.userId
      context.loggedRole = decoded.role
      return allowedRoles.includes(decoded.role)
    } catch (err) {
      return false
    }
  } else {
    return false
  }
})

/*
    permission definitions (none, admin, user)
*/

const Query = {
  Query: {
    // admin
    allAdmins: hasRole(['admin']),
    // user
    user: hasRole(['user']),
    getUser: hasRole(['admin', 'user']),
    allUsers: hasRole(['admin'])
  }
}

const Mutation = {
  Mutation: {
    // admin
    createAdmin: hasRole(['admin']),
    // user
    createUser: hasRole(['admin']),
    updateUser: hasRole(['admin']),
    verifyUser: hasRole(['admin', 'user']),
    destroyUser: hasRole(['admin'])
  }
}

// merge all permissions
const permissions = shield(Object.assign({}, Query, Mutation), { cache: false })

module.exports = { permissions }

How do I use the hasRole function now in the same file? It says that hasRole is not defined.

@dakshshah96 you have to change

const hasRole = rule()((allowedRoles) => (parent, args, context, info) => {...})

to

const hasRole = (allowedRoles) => rule()((parent, args, context, info) => {...})

I'm so sorry for the trouble but my permissions are still not enforced 馃槬 All mutations/queries are allowed.

@dakshshah96 I have just noticed it. You are not using graphql-yoga. Because of this, you cannot use middlewares property to assign middleware to your schema. To make this work, you have to use graphql-middleware like this;

import { applyMiddleware } from 'graphql-middleware'

const schema = makeExecutableSchema({ typeDefs, resolvers })
const schemaWithPermissions = applyMiddleware(schema, permissions)

Hope this helps you out! 馃檪

@maticzav Thank you so much for your patience.

Everything works as expected now 馃挴

Was this page helpful?
0 / 5 - 0 ratings