Graphql-tools: Accessing Query Variables Inside Directive Resolvers

Created on 21 Feb 2018  路  12Comments  路  Source: ardatan/graphql-tools

Thank you Apollo/MDG for such incredible tools and work! I'm a huge admirer.

Use case

Directive resolvers are incredible for permissions, but I cannot figure out how to add an @isOwner permission without access to the query variables. Say that I only want the owner of a draft post to be able to read the post.

directive @isOwner(type: String) on QUERY

type Query {
    draft(id: ID!): PostPayload @isOwner(type: "Post")
}

And for the directive resolver...

// Prisma exists function, but you can imagine other methods here

const isRequestingUserAlsoOwner = ({ ctx, userId, type, typeId }) =>
    ctx.db.exists[type]({ id: typeId, user: {id: userId}})

// ...

const directiveResolvers {
 // ...
 isOwner: async (next, source, args, ctx) => {
      const user = ctx.response.user
      const isOwner = await isRequestingUserAlsoOwner({ ctx, userId: user.id, type: args.type, typeId: ??? } )
      if (isOwner) {
        return next()
      }
      throw new Error(`Unauthorized, must be owner`)
  }
}

Is there a workaround? I guess they could be sucked from the request, but that seems clunky -- particularly when the needed value could be sent as a request.body.variables or in the request.query.

I see that the query variables were explicitly left out of the API, so there's probably a good reason for it. (It's args[1])

https://github.com/apollographql/graphql-tools/blob/80b19b6adad2b138c46f9206db647e07596af6e3/src/schemaGenerator.ts#L704

Thanks for any help for thoughts!

Most helpful comment

I know this issue is closed however it came up while I was having this question so I though I'd leave here a solution.

As of today, the SchemaDirectiveVisitor class makes the query variables available at the field's resolver from the argument info as info.variableValues

Here's a simple example given the _context of permissions_ that checks if the request comes from the _owner_

class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    // destructuring the `resolve` property off of `field`, while providing a default value `defaultFieldResolver` in case `field.resolve` is undefined.
    // destructuring the field's `name` to use in the Error message
    const { resolver = defaultFieldResolver, name } = field

    field.resolve = async function (source, args, context, info) {

      // the query variables are available at info.variableValues
      const {
        variableValues: { userID }
      } = info

      // checks that the logged in user is the same as the one in the query variable `userID`
      if (context.user.id !== userID)
        throw new ForbiddenError(`Unauthorized field ${name}`)

      // runs if the condition above passes
      const result = await resolver.call(this, source, args, context, info)
      return result
    }
  }
}

Simplified for readability

All 12 comments

I agree, not having access to the variables severely limits the utility of custom directives. I was hoping to use them for the following use cases:

  • authorization of access to data/resources identified in variables (similar to @LawJolla)
  • include the variable values in error logs

Right now custom directive seem to work well for authentication and authorization against a static set of claims.

@mjdickinson Take a look at this article, it should provide some insight into how to do authorization against a dynamic set of claims: https://medium.com/@lastmjs/advanced-graphql-directive-permissions-with-prisma-fdee6f846044

Looks like this question went the way of Schema Directives!

@lawjolla I haven't been following the latest developments, can you post a link with more info?

@lastmjs

Absolutely! https://www.apollographql.com/docs/graphql-tools/schema-directives.html
https://dev-blog.apollodata.com/reusable-graphql-schema-directives-131fb3a177d1

I haven't used it either, but it looks like visitInputField and its progeny should provide the hooks for these issues.

@LawJolla , @lastmjs, @mjdickinson Hey guys, what do you think about doing something like this.

class WithIDDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resovle = defaultFieldResolver } = field;
    field.resolve = async (...args) => {
      const ctx = args[2]; // context
      const { tokenService, Account } = ctx;
      const token = ctx.req.request.headers.authorization; // get the token from headers

      if (!token) throw new Error("unauthorised");

      const accountID = tokenService.verifyAuthToken(token); // verify and decode the token

      const account = await Account.findById(accountID).exec(); // find the account
      if (!account) throw new Error("Account does not exist!");

      args[1] = Object.assign({ accountID }, args[1]); // add accountID to the input args

      return resolve.apply(this, args);
    };
  }
}

And then verify the "Ownership" in the actual field resolver ?

Cheers

I know this issue is closed however it came up while I was having this question so I though I'd leave here a solution.

As of today, the SchemaDirectiveVisitor class makes the query variables available at the field's resolver from the argument info as info.variableValues

Here's a simple example given the _context of permissions_ that checks if the request comes from the _owner_

class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    // destructuring the `resolve` property off of `field`, while providing a default value `defaultFieldResolver` in case `field.resolve` is undefined.
    // destructuring the field's `name` to use in the Error message
    const { resolver = defaultFieldResolver, name } = field

    field.resolve = async function (source, args, context, info) {

      // the query variables are available at info.variableValues
      const {
        variableValues: { userID }
      } = info

      // checks that the logged in user is the same as the one in the query variable `userID`
      if (context.user.id !== userID)
        throw new ForbiddenError(`Unauthorized field ${name}`)

      // runs if the condition above passes
      const result = await resolver.call(this, source, args, context, info)
      return result
    }
  }
}

Simplified for readability

Okay, so I'm finally digging into schema directives. I've been playing around for a bit now, and I still haven't been able to figure out how to add fields to a resolver that were not requested by the user. @Rennos and @antoniojps , your solutions do not provide a way to grab arbitrary fields that the user did not request and use them for authorization (if I'm mistaken, please point it out). So, we're kind of back where we started. I have a working solution that uses directive permissions, but the world is moving on to schema directives. I don't have a working solution there yet, if anyone has insights please let me know. In each field resolver, we need access to any field from the parent object essentially.

@lastmjs That's all done via fragments on the root resolvers. Remember, the root resolver is called before the directive resolver. You can take a look here at the index.js resolvers with fragments and the directives. https://github.com/LawJolla/prisma-auth0-example/tree/master/server/src

Thanks @LawJolla , that makes sense. Though it's still difficult to work with when trying to generalize fragment addition. It would be really nice if schema directives could somehow add fragments to resolvers, since the schema directive itself runs at load time before any resolvers are called (I don't even think there is a concept of a directive resolver with schema directives anymore, you only manipulate the regular resolvers through the directive). This describes what I wish could happen: https://github.com/apollographql/graphql-tools/issues/984

hey! directiveResolvers will be deprecated in favor of SchemaDirectiveVisitor (https://www.apollographql.com/docs/graphql-tools/schema-directives.html#What-about-directiveResolvers)

How can we trigger a Graphql function inside hook visitObject?

class AuthDirective extends SchemaDirectiveVisitor {
  visitObject(type) {
    // I would like to check if user has croorect role. how can we get acces to ctx? So doing something like:

    // const requestingUserIsAdmin = await ctx.db.exists.User({
    //   id: userId,
    //   role: 'ADMIN',
    // })
  }

alan345 - here is a way i found to get the context

const fields = type.getFields ()

Object.keys ( fields ).forEach ( f => {
const field = fields [ f ]

f.resolve = async function ( ...args ){
const [ , , ctx ] = args
return await resolve.apply ( this,args)
}
} )

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tonyxiao picture tonyxiao  路  4Comments

benjaminhon picture benjaminhon  路  3Comments

MehrdadKhnzd picture MehrdadKhnzd  路  3Comments

ericclemmons picture ericclemmons  路  4Comments

BassT picture BassT  路  3Comments