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])
Thanks for any help for thoughts!
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:
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)
}
} )
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_
Simplified for readability