With Prisma, I can write nested mutations like so:
gql
mutation{
createPost(data: {
body: "Hello World"
tags: {
create: [
{name: "cats"}
],
}
}){
body
tags{
name
}
}
}
Using Shield, I can set rules for createPost but any such rule bypasses all my rules on createTag. So basically if a user has permission to create a post, they can by extension create anything else that a post connects to. ie. It's impossible to allow a user to updatePosts without also allowing them to update any tag, user, comment etc connected to that post. I think this is an inherent limitation of graqhql middleware and Prisma--they are not aware of these nested mutations and thus do not hit any mutation middleware after the top level.
Am I correct or missing something? How do people handle the case where a user is connected to a post? Can anyone update that user who can update the post?
Thanks!
Hey @nolandg 馃憢,
I wouldn't say this is a "limitation" of graphql-middleware and prisma because GraphQL does not support nested mutations as such. What prisma does is use the arguments of a function to change the nodes internally. In that sense, there are no "nested-mutations" only the Prisma API which handles connections in such a manner.
Considering this, the majority of the systems rely on unwrapped Prisma servers, if I name them so, which means that they change the schema to their needs and use prisma only as a delegation layer which happens to use GraphQL as well. I believe this might be connected to #113. So far, I haven't come across a meaningful example which would persuade me into implementing such functionality. As a note, I would say that exposing Prisma API is far too risky because you have no control over connect and create arguments, as you already mentioned, plus you usually only need about a fifth of all the exposed functionality.
Nevertheless, if you believe this is your only option, I would go about implementing it as an argument checker. Take a look at the example below.
const hasPermission = rule({ cache: 'strict' })(
async (parent, args, ctx, info) => {
if (args.data.tags.create !== undefined) {
return isUserAuthenticated(ctx)
}
return true
},
)
I hope this helps you solve your problem. 馃檪
@nolandg I have some idea for you. It just something I thought about yesterday so it may be complete bs, but why not share it :)
Solution: just use regex to cut some functionality from your prisma generated SDL before exposing it to your server. (If you'll need some of it later you can explicitly import/declare/whatever it.)
Prisma generated schemas have quite predictable names based on kind of relationships.
So first level input arguments look like this Name + (Update|Create|Delete)Input
input TaskUpdateInput {
owners: UserUpdateManyWithoutTasksInput # captured
candidate: CandidateUpdateOneWithoutTasksInput # captured
title: String
description: String
dueAt: DateTime
}
So you can cut (e.g via String.prototype.replace()) all 'nested' inputs fields using regex like this. And now you have no nested fields :tada:
/(?!input [A-Z][a-z]+ UpdateInput { \n .*)([a-z]+: [A-z][a-z]+(?:UpdateOne|UpdateMany)Without).*/mg
Or you can target your second and more nested level inputs to specifically disallow just e.g. deleteMany/delete
/(?!input [A-Z][a-z]+Update(Many|One)Without[A-Z][a-z]+Input {\n.*)(delete|deleteMany): .*/mg
input TaskUpdateManyWithoutCandidateInput {
create: [TaskCreateWithoutCandidateInput!]
delete: [TaskWhereUniqueInput!] # captured
connect: [TaskWhereUniqueInput!]
disconnect: [TaskWhereUniqueInput!]
update: [TaskUpdateWithWhereUniqueWithoutCandidateInput!]
upsert: [TaskUpsertWithWhereUniqueWithoutCandidateInput!]
deleteMany: [TaskScalarWhereInput!] # captured
updateMany: [TaskUpdateManyWithWhereNestedInput!]
}
Those are just 5 minutes regexes, but with a bit of tinkering you can create some sort of config/builder/whitelister/validator for them. It's a bit hacky, but also - is it really? All of the cases are fairly predictable. Also you don't need to necessarily just cut the fields - you can modify them - and delegation to prisma via binding works with changed type names - as long as the shapes are compatible (or at least it worked few months ago^^).
Then you can use @maticzav idea to guard input arguments on fields you'll actually need in your schema :)
As example: I wrote tiny tool like that yesterday for a bit different thing (using regex for importing specific fields etc.) but it may give you idea how to implement this: https://github.com/vadistic/gql-import
@vadistic if you're interested in modifying and exposing a subset of a schema as a new schema I recommend you check out https://github.com/mitoai/gintonic
Thanks for the suggestions guys. I'm basically already doing what @maticzav suggests. My framework has its own schema that's used to generate Prisma config and bindings, graphql, and middleware for every query and mutation. In the middleware I whitelist every field based its schema type and permissions. Oh course this solution is tightly coupled to Prisma and depends on interpreting Prisma mutation args. I was wondering if anyone else had solved this in a more general way but I don't think that's possible.
Hey :wave:,
We saw you use our package. To keep our code as accessible as possible, we decided to open source it and are now helping others with their questions.
In a pursuit to continue our work, help us by donating to our collective! :heart: