Comments (15)
I'll do some digging this week.
from graphql-shield.
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)))
from graphql-shield.
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?
from graphql-shield.
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! 🙂
from graphql-shield.
Thanks, man! Great feature!
from graphql-shield.
Special thanks from us! This feature will come in very handy to us. We donated something for your work. Thanks again!
from graphql-shield.
@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?
from graphql-shield.
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 graphql-shield.
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.
from graphql-shield.
@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.
from graphql-shield.
@vadistic what is this "R"-Object???
from graphql-shield.
@maticzav what do YOU think about this? Is this a good approach?
from graphql-shield.
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.
from graphql-shield.
I believe this might be a good approach to implement permissions on input types https://github.com/jquense/yup.
from graphql-shield.
🎉 This issue has been resolved in version 5.3.0 🎉
The release is available on:
Your semantic-release bot 📦🚀
from graphql-shield.
Related Issues (20)
- Add the ability to use Fragments and post-execution rules with the single server/schema
- Add Typescript typings to rule args
- Add the ability to attach rules using GraphQL Directives
- CI: add codecov reports
- Provide a way to expose authorization metadata through the graphql schema
- GraphQL Shield Roadmap
- 7.6.4 ESM build broken HOT 5
- Documentation website down HOT 1
- How to use `if` condition in GraphQL Shield HOT 2
- Shield rules type generation based on schema HOT 1
- Feature request: wildcard functionality for field names
- Update [email protected] module to avoid @types/lodash and babel runtime in production deps
- fallbackError loses custom error types
- fallbackRule context information HOT 2
- Question: Is there a way to return objects on rules?
- ..
- Typo in the Docs
- Shield permissions only working properly with 'debug: true' HOT 1
- Performance -- every field wrapped is unnecessarily wrapped in a promise HOT 1
- Documents are not populated when running `graphql-shield`, thus leading to `undefined` permission errors.
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from graphql-shield.