GithubHelp home page GithubHelp logo

Comments (15)

frankdugan3 avatar frankdugan3 commented on April 27, 2024 7

I'll do some digging this week.

from graphql-shield.

vadistic avatar vadistic commented on April 27, 2024 2

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.

Hitabis avatar Hitabis commented on April 27, 2024 1

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.

maticzav avatar maticzav commented on April 27, 2024 1

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.

boenni23 avatar boenni23 commented on April 27, 2024 1

Thanks, man! Great feature!

from graphql-shield.

Hitabis avatar Hitabis commented on April 27, 2024 1

Special thanks from us! This feature will come in very handy to us. We donated something for your work. Thanks again!

from graphql-shield.

maticzav avatar maticzav commented on April 27, 2024

@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.

boenni23 avatar boenni23 commented on April 27, 2024

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.

frankdugan3 avatar frankdugan3 commented on April 27, 2024

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.

Hitabis avatar Hitabis commented on April 27, 2024

@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.

Hitabis avatar Hitabis commented on April 27, 2024

@vadistic what is this "R"-Object???

from graphql-shield.

boenni23 avatar boenni23 commented on April 27, 2024

@maticzav what do YOU think about this? Is this a good approach?

from graphql-shield.

stale avatar stale commented on April 27, 2024

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.

maticzav avatar maticzav commented on April 27, 2024

I believe this might be a good approach to implement permissions on input types https://github.com/jquense/yup.

from graphql-shield.

maticzav avatar maticzav commented on April 27, 2024

🎉 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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.