Graphql-shield: Doubt: Authorization constraints in resolvers or shield permissions

Created on 16 Feb 2019  路  5Comments  路  Source: maticzav/graphql-shield

Question about GraphQL concepts

  1. Is it right to assume that I only need to care about auth and permissions on scalar fields only? By this, I mean:
type Customer {
  id: ID!
  name: String!
  sensitiveData: UserSensitiveData!
  ...
}
type Customer SensitiveData {
  id: ID!
  customer : Customer !
  data1: String!  # authorization verification from DB here in resolvers
  data2: ... # authorization verification from DB here in resolvers
}
Rules:
export const permissions = shield({
  Customer : allow,
  CustomerSensitiveData: isUserCustomer
})

EDIT: I now have a feeling/understanding that this could leak fields like id in related type for an authenticated, but unauthorized access. For instance, id in Product is public, so purchasedProducts in type Customer, if not verified for authorization, could leak that id (and other product identifier) to an authenticated customer access trying to access other customer's resources. So, authorization should be on the edges, not on the nodes.

  1. How do I ensure that an authorized user is deleting say, wishlisht of its own? (Right now, I am using $exists from prisma client in resolvers to check if the user has that item, then only allow deletion). Should this constraint be somehow placed in the shield rule?

Question about GraphQL Shield

  1. What are fragments in shield? Are they the one to use when I have to constraint some fields of a type to be public, and some to be authorized (but these could be achieved with field level rule defs, couldnt they?)
  • [x] I have checked other questions and found none that matches mine.
kinquestion

Most helpful comment

Hey 馃憢,

I am not sure I understand your question entirely. In particular, what are you referring to with AuthZ? Maybe the example below could give you some ideas on how to tackle the issue.

type Query {
  viewer: User
}

type User {
    id: ID!
    email: String!
    tickets: [Ticket!]!
}

type Ticket {
  id: ID!
  event: PublicEvent!
  owner: User!
  isValidated: Boolean!
  isExpired: Boolean!
}
const isUserTicketOwner = rule({
  cache: 'strict',
  fragment: 'fragment TicketId on Ticket { id } ',
})(async ({ id }, args, ctx: Context) => {
  try {
    const userId = getUserId(ctx)

    return ctx.prisma.exists.Ticket({
      id: id,
      owner: { id: userId },
    })
  } catch (err) {
    return false
  }
})

export const permissions = shield(
  {
    Query: {
      viewer: allow,
    },
    User: isUserAuthenticated,
    Ticket: or(isUserAdministrator, isUserModerator, isUserTicketOwner),
  },
  {
    debug: process.env.NODE_ENV !== 'production',
    fallbackError: 'To pa ne gre...',
    fallbackRule: deny,
  },
)

That's how I would approach writing permissions. As you can see, User only checks whether user is indeed authenticated, while Ticket also verifies that current user owns it.

I hope this helps you solve your problem 馃檪

All 5 comments

Hey 馃憢,

I am not sure I understand your question entirely. In particular, what are you referring to with AuthZ? Maybe the example below could give you some ideas on how to tackle the issue.

type Query {
  viewer: User
}

type User {
    id: ID!
    email: String!
    tickets: [Ticket!]!
}

type Ticket {
  id: ID!
  event: PublicEvent!
  owner: User!
  isValidated: Boolean!
  isExpired: Boolean!
}
const isUserTicketOwner = rule({
  cache: 'strict',
  fragment: 'fragment TicketId on Ticket { id } ',
})(async ({ id }, args, ctx: Context) => {
  try {
    const userId = getUserId(ctx)

    return ctx.prisma.exists.Ticket({
      id: id,
      owner: { id: userId },
    })
  } catch (err) {
    return false
  }
})

export const permissions = shield(
  {
    Query: {
      viewer: allow,
    },
    User: isUserAuthenticated,
    Ticket: or(isUserAdministrator, isUserModerator, isUserTicketOwner),
  },
  {
    debug: process.env.NODE_ENV !== 'production',
    fallbackError: 'To pa ne gre...',
    fallbackRule: deny,
  },
)

That's how I would approach writing permissions. As you can see, User only checks whether user is indeed authenticated, while Ticket also verifies that current user owns it.

I hope this helps you solve your problem 馃檪

@maticzav Sorry for playing cool :( By authZ, I meant "authorization". Have updated my post, sorry!
Also, I had taken ideas from your code repo and have done similar thing, except that against point 2, I have a generic function that takes as argument what to check for ownership in the DB (since, a user could have multiple relations, like tickets: [Ticket!] from your example).
So, basically I was asking then if such a single could do, or should I instead write individual ones e.g. isUserTicketOwner, isUserPostOwner, isUserAddressOwner, isUserPresidentOfUSA, etc.
Please feel free to point me to any more of my stupidity in this approach. Thanks a lot, you have been always very helpful :)

@devautor no worries, I could have guessed that! 馃檲 What you could do is set up an interface type and somehow validate the ownership that way, however, I am not entirely sure how would that work.

@maticzav Doing something similar without type validation in this step, like I added that with a function.

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:

https://opencollective.com/graphql-shield

Was this page helpful?
0 / 5 - 0 ratings