Closed frankdugan3 closed 5 years ago
@frankdugan3 this is so cool! I would love to work on this with you. I am not sure how input
types are treated internally by GraphQL or how they come into play in schema, but let's explore and keep this issue open until we find out. Do you have any leads?
I'll do some digging this week.
I'm also interested in this feature. I'm just confused. The Readme says:
🎯 Per-Type: Write permissions for your schema, types or specific fields (check the example below).
Are write permissions on fields supported? If yes, how would it be differenet with input types?
if I try something like
User: { password: deny },
it only effects the read permissions.
From what I was able to gather, it looks like input objects are treated pretty similarly to object types and can be processed with isInputObjectType
. I think it's possible, but I haven't had a chance to do a mock-up.
Hello, I'm just tackling this issue in my project and I thought I will present my progress for inspiration.
I would be glad for any pointers how to improve it. Also - I have literally no idea how this construct affect performance and caching...
So my idea is to treat each input field permissions as separate rule and join them with and()
. Here's example (using ramda a bit, hope it's still readable)
// Let's prep some syntax sugar
interface FieldRuleMap {
[key: string]: (field: string) => IRule
}
const fieldRules = (fieldRuleMap: FieldRuleMap) => {
const rulesArr = Object.entries(fieldRuleMap).map(([key, fn]) => fn(key))
return and(...rulesArr)
}
// Then define my input field rule
const hasPermission = (fieldName: string) =>
rule()(async (parent, args, ctx: Context) => {
// shortcircuit for performance if args has none of this prop
if (!R.has(fieldName, args)) {
return true
}
// check your privileges
const res = complicatedCalculations()
return res
})
// And voila!
const rulesWithFields: IRules = {
Mutation: {
createUser: and(isAuthorized, fieldRules({
name: hasPermission
})),
},
}
const permissions = shield(rulesWithFields)
If you don't mind wall of code - here's some real example - I'm forwarding a lot's of resolvers to Prisma, but I need to check input arguments for connections validity. Here I validate if some target user is a member of the same 'workspace` as user. It's one-to-many case, for one-to-one I will need to use slightly modified rule.
const verifyConnection = (target: string) => (field: string) =>
rule()(async (parent, args, ctx: Context) => {
// shortcircuit
if (!R.has(field, args.data)) {
return true
}
// I do a lot of this checks so I keep workspaceId in JWT
const workspaceId = getId(ctx).workspaceId
// Get id[] from args
const targetConnect = args.data[field].connect
// Now I mapping ids to array of promises checking each provided target id
const validationP = targetConnect.map(connect => {
return ctx.db.exists[target]({
AND: {
id: connect.id,
workspace: { id: workspaceId },
},
// Later I'm using Promise.all() and I want it to stop executing on first `false` so I'm adding reject
}).then(res => (res === true ? Promise.resolve(true) : Promise.reject('Not Authorized')))
})
// Await for all promises and test if all elements yield true
// (bit unnecessary since I'm rejecting those promises in case of false, but nvm)
const result = R.all(R.equals(true), await Promise.all(validationP).catch(rej => [false]))
return result
})
// same syntetic sugar as previous example
interface ConnectionRuleMap {
[key: string]: (field: string) => IRule
}
const verifyConnections = (connectionRuleMap: ConnectionRuleMap) => {
const rulesArr = Object.entries(connectionRuleMap).map(([key, fn]) => fn(key))
return and(...rulesArr)
}
// rules
export const rules: IRules = {
Mutation: {
createTask: verifyConnections({
owners: verifyConnection('User'),
subscribers: verifyConnection('User'),
})
}
}
const permissions = shield(rules)
export default permissions
What y'all think? 😃
// EDIT Btw. I did not tackle how to nicely use logic rules on fields yet without rewiring them
// this will fullfil my needs, but there should be a better way
const fieldAnd = (fieldName: string) => (...args: FieldRule[]) =>
and(...args.map(arg => arg(fieldName)))
@vadistic great inspiration. thanks!
I will look into this, but I would prefer a solution with existing rules. like
Mutation: {
createThing: fieldRules({
field1: deny,
field2: allow,
}),
}
I will write when I figured it out.
@vadistic what is this "R"-Object???
OK. Here my first attempt:
const fieldRules = (fieldRuleMap, path = []) => {
const rulesArr = Object.entries(fieldRuleMap).map(([key, rule]) => {
// Has no rule / is object
if(!(rule.rules && Array.isArray(rule.rules))) {
return fieldRules(rule, [...path, key])
}
// has rule
return checkRule(key, rule, path)
})
return and(...rulesArr)
}
const checkRule = (fieldName, customRule, path) => {
return rule()(async (parent, args) => {
try {
// exit if args dont have this field
if (!objectValueByPath(args, fieldName, path)) {
return true
}
return customRule
} catch (error) {
return false
}
})
}
const objectValueByPath = (object, fieldName, path) => {
let current = object
path.forEach(key => {
if(!current[key]) {
// false = not in tree
return false
}
current = current[key]
})
return current[fieldName]
}
You can use it with
createThing: fieldRules({
data: {
field1: deny,
subfields: {
subfield1: isOwner,
}
}
}),
what do you all think?
@maticzav what do YOU think about this? Is this a good approach?
Hey 👋,
I let this discussion evolve itself but I think we steered a bit out of the initial idea. Therefore, I would like to focus on tackling the overall problem that we are addressing not the implementation details themself.
Firstly, I want to clarify what I understood as a proposal to input
type scoped permissions. GraphQL allows the definition of so-called input
types. Their primary focus is making more complex data structures accessible in arguments and not only in types themselves. An example of input
-types usage would be a signup mutation, for example, which accepts multiple arguments. Because data can be nested or in any other way co-dependent, it makes sense to allow JSON-like input structures which we can test before execution (validation step).
Hence, this issue does not address how one should go about implementing scalar arguments validation but rather how we can make permissions on complex cross-field arguments reusable.
To corroborate the idea, let's examine the schema below. At first sight, we can notice we are creating a simple social network where one can create a single or even multiple events at the same time. There's a GroupInput
input type that we use in two different places.
type Mutation {
createGroup(data: GroupInput): Group
createMultipleGroups(data: [GroupInput!]!): [Group!]!
}
input GroupInput {
name: String!
members: [ID!]!
}
type Group {
id: ID!
name: String!
members: [Member!]!
}
Furthermore, groups require a unique name. Currently one would tackle such a problem by implementing the same code in two different places. To give a brief notion of how one could achieve this with the current graphql-shield
features, let us examine the following code;
const permissions = shield({
Mutation: {
createGroup: rule()(async (parent, { data }, ctx, info) => {
if (canGroupBeCreated(data)) {
return true
}
return false
}),
createMultipleGroups: rule()(async (parent, { data }, ctx, info) => {
if (data.every(group => canGroupBeCreated(group))) {
return true
}
return false
}),
},
})
Horrible!
Now, the idea of this issue is to find a way to make the above syntax far more appealing than the example we just witnessed. To give an idea of what seems the right direction, let's examine the last chunk of code on this particular topic;
const permissions = shield({
InputTypes: {
GroupInput: {
name: val => isUniqueName(val)
}
},
Mutation: {
createGroup: rule()(async (parent, { data }, ctx, info) => {
// if (canGroupBeCreated(data)) {
// return true
// }
// return false
//
// We already know that arguments passed - we can focus on other restrictions!
}),
createMultipleGroups: rule()(async (parent, { data }, ctx, info) => {
// if (data.every(group => canGroupBeCreated(group))) {
// return true
// }
// return false
//
// We already know that arguments passed - we can focus on other restrictions!
}),
},
})
I hope I made it a bit more clear of what I believe this proposal is aiming for. I hope we can find a genuinely concise and ingenious approach to tackling the problem as I think this could help, as can also be seen from the activeness of the discussion, many developers.
I think I covered the first topic. 😄
Now, the second topic I want to discuss is a response to a comment made by... it seems like it's not here anymore... anyways, I think this is a great place to share it!
With GraphQL you are always querying.
There's been a numerous amount of questions addressing how one could foresee which fields the client is interested in and stop the execution of a query upfront if need be. This has been especially common in two particular scenarios; mutations and resolvers using schemaDelagation
. I think this conceptual fatuity is a result of our approach to GraphQL - we are still thinking in REST.
I believe schemaDelegation
is quite often considered a somehow similar concept to REST-designed application. However, it is not! It's quite evident why people make this obvious mistake; delegation happens in one of the resolvers, and its result is forwarded to the resolver execution chain. Addressing the issue we can see why the relation seems complementary. Long story short, don't be fooled into thinking schemaDelegation
is in any way similar to REST. It's not! Every value returned by the remote server is reprocessed once again locally.
To conclude, we often mingle REST with GraphQL when it comes to schemaDelegation
. Nevertheless, they are not the same thing, far from it in fact. Thinking about "foreseeing" requested fields is, therefore, redundant - your logic shouldn't depend on the content of the processed query.
Furthermore, as the quote above humbly suggests, we are always querying. There's no distinct difference between Query
and Mutation
- in fact, there's only one difference; one is processed serially and the other is processed asynchronously. The beauty of the graph is that one can compose a relational network and ideally access all fields from whichever vertex they desire. Fields have to be independent.
Summing it all up, I believe a notion of "foreseeing" request content and relating function execution based on it is ridiculous. When we make internal changes as a result of arguments of Query
or Mutation
they shouldn't depend on the queried content.
I hope this is somewhat helpful contribution to our discussion. I would love to hear your thoughts on it as well! 🙂
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
I believe this might be a good approach to implement permissions on input types https://github.com/jquense/yup.
:tada: This issue has been resolved in version 5.3.0 :tada:
The release is available on:
Your semantic-release bot :package::rocket:
Thanks, man! Great feature!
Special thanks from us! This feature will come in very handy to us. We donated something for your work. Thanks again!
A use case I'm interested in is putting permissions on input types and individual fields of input types. This would simplify mutation permissions because you could use a generic mutation like
updateUser
but limit access to modifying certain fields ofUser
.Is this supported or possible to add? I'd be happy to contribute if interested.