Prisma Auth0 Directive Permissions Example


#1

Building off Prisma’s Instagram clone demo, I made the following repo to match the permissions tutorial requirements (without the roles, but that would be trivial to add)

I’m using a middleware pattern where I first check the authorization token. If it’s invalid, a 401 is thrown and the request stops. If it is valid, the next middleware puts the User type onto the request object. And if there’s no token, the middleware passes with user = null.

I’m not an expert and just put this demo together as a learning experience. If it sucks or I did something wrong, or you know a better / more secure pattern, please speak up!

Demo Video


#2

I just updated my example / starter repo to include the new fangled directive permissions!

What’s that?

You decorate your schema with custom directives, e.g.

directive @hasRole(roles: [String]) on QUERY | FIELD | MUTATION
directive @isOwner(type: String) on QUERY | MUTATION
directive @isAuthenticated on QUERY | FIELD | MUTATION
directive @isOwnerOrHasRole(type: String, roles: [String]) on QUERY | MUTATION

type Query {
  feed: [Post!]!
  drafts: [Post!]! @isAuthenticated
  post(id: ID!): Post @isOwnerOrHasRole(type: "Post", roles: ["ADMIN"])
  me: User @isAuthenticated
}

And then you have a set of “directive resolvers” to implement the logic! e.g.

const directiveResolvers = {
...
hasRole: (next, source, { roles }, ctx) => {
    const { role } = isLoggedIn(ctx)
    if (roles.includes(role)) {
      return next()
    }
    throw new Error(`Unauthorized, incorrect role`)
  }
}

How do we implement this in Yoga? Using graphql-tools and graphql-import… Simple!

const schema = makeExecutableSchema({
  typeDefs: importSchema("./src/schema.graphql"),
  resolvers,
  directiveResolvers
})

const server = new GraphQLServer({
  schema,
  context: req => ({
    ...req,
    db
  })
})

The brilliance is its flexibility. Need to get more granular?

directive @hasScope(scopes: [String]) on QUERY | FIELD | MUTATION

type Mutation {
   createProduct(input: CreateProductInput): CreateProductPayload @hasScope(scopes: ["create:product"])
}

And you can chain them (but they are all ANDs, not ORs in a chain)

type Mutation {
   updatePost(...): Post  @isOwner @hasRole(roles: ["ADMIN"])
}

And for the mic drop, take a look at my Query resolvers now – complete with full authentication and permissions… (drafts, post, and me are protected… but you don’t have to clutter your resolvers with the permissions boilerplate any longer!)

const resolvers = {
  Query: {
    feed(parent, args, ctx, info) {
      return ctx.db.query.posts({ where: { isPublished: true } }, info)
    },
    drafts(parent, args, ctx, info) {
      return ctx.db.query.posts(
        { where: { isPublished: false, user: { id: ctxUser(ctx).id } } },
        info
      )
    },
    post(parent, { id }, ctx, info) {
      return ctx.db.query.post({ where: { id } }, info)
    },
    me(parent, args, ctx, info) {
      return ctx.db.query.user({ where: { id: ctxUser(ctx).id } }, info)
    }
  },
...

I’ve been playing around with this all today and love it. Hopefully we can get a discussion going!


#3

@LawJolla I love your example! This truly looks irrationally simple. :tada:

The only thing I cannot really decide upon is whether I would want to mix my schema with directives, is this a good approach? With graphql-shield (https://github.com/maticzav/graphql-shield), for example, I could achieve the very same functionality but completely separate all the permissions logic. This way, I am also able to just turn down all the permissions and test the flow, which in my opinion comes in very handy when debugging Would that be possible using directives?


#4

Good questions!

Through these discussions, we’ll likely find substantive reasons to use one approach over the other, but I think the main differentiator will be stylistic – do you like decorating your schema with permissions? I do. I can see where and why other’s don’t.

Here’s permissions off. To my understanding of custom directives, they simply fall by the wayside if “unused”. I can confirm that the following turns off permissions without errors, but if there are side effects to having unused directives in the schema, please speak up!

const schema = makeExecutableSchema({
  typeDefs: importSchema("./src/schema.graphql"),
  resolvers,
  // directiveResolvers
})

And if you want to play with the control, and, say turn the isOwnerOrHasRole directive permission off…

const directiveResolversWithisOwnerOrHasRoleRemoved = {
    ...directiveResolvers,
     isOwnerOrHasRole: (next) => next()
}
const schema = makeExecutableSchema({
  typeDefs: importSchema("./src/schema.graphql"),
  resolvers,
  directiveResolvers: directiveResolversWithisOwnerOrHasRoleRemoved
})

Does that make sense/work?


#5

It seems like there is a fundamental flaw. To check the owner of a requested resource, you have to have the id. The client isn’t always going to send the id down. For example:

type User {
    email: String! @isOwner
}

If I execute the query:

users {
    email
}

How can I ensure that the email is still protected? As far as I can tell the directive resolvers don’t get information about each field beside the field itself. Any help here? Unfortunately this seems like a blocking problem for me in using directive permissions.


#6

Good question. You can use info parameter to force the fields to be returned into directive resolver’s source.

return ctx.db.query.users({}, `{ id email }`)

What I’m not sure how to do is combine the resolver info object with the string of required fields. If a binding’s expert could help, that’d be great!


#7

There’s also a discussion of the same issues here: https://github.com/apollographql/graphql-tools/issues/212#issuecomment-371369327


#8

That’s a different issue. How to get fields into source and how to access client args in directive resolvers are separate issues.

I brought up the latter question here, along with a work around (you can take the args from the request).

EDIT

Huge thanks to @agartha and @nilan who set me straight, manipulating info is not the right approach to the users { email @isOwner } permission problem. Here’s the Slack discussion, and once I implement, I’ll update the repo and this thread.

https://graphcool.slack.com/archives/C0MQJ62NL/p1520524513000679


#9

Here’s the update!

@jordan.michael.last is firing some good questions this way! To have truly elegant solutions, we will need GraphQL Tools updated, but here’s my semi, not really hacky fixes. :slight_smile:

First problem. How to protect fields where the client fails to call an id. For instance, say we have:

type User {
   id: ID!
   email: String @isOwner(type: "User")
}

and we get a query of:

users { email }

If the user isn’t the owner, they won’t see the email as intended. But if the user is the owner, they still won’t see the email. That’s because the id isn’t passed into the directive resolver, so there’s no way to validate the isOwner of the field.

Fragments to the rescue!

  User: {
    email: {
      fragment: `fragment UserId on User { id }`,
      resolve: (parent, args, ctx, info) => {
        return parent.email
      }
    }
  },

And now every time email is queried on User, the User id is passed to the directive resolver @isOwner.

That does mean that for every directive permission where a field is required, you’ll need to write a fragment on the type. (Or in my app, I also write the client and there’s no third party, so I’ll simply always query with the id, bypassing the entire issue) I suspect we will have a more elegant solution in the not too distant future.

I’ve updated my boilerplate demo with the fragments. Check it out…

Also, please take a look at my @isOwner directive resolver. If you pass the type, e.g. `@isOwner(type: “Post”), it’ll check to see if the user is the owner of the post. But its logic is getting a little hacky, so if you have a more elegant abstraction, please let us know!


#10

Alright, so it seems true that you will need to write a fragment for the type of each directive permission where a field is required. But, that can be abstracted nicely as follows:

function addFragmentToFieldResolvers(schemaAST, fragmentSelection) {
    return schemaAST.definitions.reduce((result, schemaDefinition) => {
        if (schemaDefinition.kind === 'ObjectTypeDefinition') {
            return {
                ...result,
                [schemaDefinition.name.value]: schemaDefinition.fields.reduce((result, fieldDefinition) => {
                    //TODO this includes check is naive and will break for some strings
                    if (fragmentSelection.includes(fieldDefinition.name.value)) {
                        return result;
                    }

                    return {
                        ...result,
                        [fieldDefinition.name.value]: {
                            fragment: `fragment Fragment on ${schemaDefinition.name.value} ${fragmentSelection}`,
                            resolve: (parent, args, context, info) => {
                                return parent[fieldDefinition.name.value];
                            }
                        }
                    };
                }, {})
            };
        }
        else {
            return result;
        }
    }, {});
}

This will generate resolvers for all of the fields of each type in your schema, automatically adding the fragment. You can pass in the selection that you want on the fragment. For example, here is how I get all of the resolvers for my datamodel.graphl schema:

const preparedFieldResolvers = addFragmentToFieldResolvers(parse(readFileSync('./datamodel.graphql').toString()), `{ id }`)

Now in any directive resolvers I define, I’ll have the id added to the source variable.


#11

Looks like we have some more issues. How do we handle adding directive permissions to the automatically generated mutations that Prisma provides us in prisma.graphql? If you try to override them in your custom schema, you’re going to run into some difficult type dependencies. It’s not easy to grab the types that you need from prisma.graphl. For example, let’s say I only want authorized users to be able to create a new user. Here’s what I will need to do outside of the generated prisma.graphl:

directive @authenticated on FIELD | FIELD_DEFINITION

mutation {
    createUser(data: UserCreateInput!): User! @authenticated 
}

Do you see the problem? UserCreateInput is not defined in my custom schema. It’s only defined in prisma.graphql, and there is no easy way to get that dependency out as far as I know. mergeSchemas from graphql-tools will not pick up on that dependency, and fails when trying to merge prisma.graphql and a custom schema like the one above. Any ideas on this? I’m coming to the conclusion that I’ll just have to do some more AST manipulation to grab any dependencies from prisma.graphql and put them into the custom schema.


#12

Also, the reason this is a problem is because I’m trying to avoid duplicating schema definitions at all costs. Perhaps that isn’t a feasible option, but so far I’ve been able to achieve it.


#13

And we have a solution. The merge-graphql-schemas library does the trick. For example:

const ultimateSchemaString = mergeTypes([
    readFileSync('./datamodel.graphql').toString(),
    readFileSync('./custom.graphql').toString(),
    readFileSync('./generated/prisma.graphql').toString()
], {
    all: true
});
const ultimateSchema = makeExecutableSchema({
    typeDefs: ultimateSchemaString,
    resolvers,
    directiveResolvers
});

You can put any of the mutations or queries that you want custom directives on in custom.graphql, and merge all of the types easily. Then you define one set of resolvers and directiveResolvers. This is just a simple example, and if anyone is interested in how I’ve actually setup my project, just ask. The road is clearing for full permissions with custom directives!


#14

Thanks for all the explanations jordan, I would love to know more about how you’re actually implementing it, I’m moving from AirTable and would like to leverage lawjolla/Auth0 example for AUTH and PERMISSIONS


#15

Oh also thanks @LawJolla for putting up the example code, it’s been my inspiration to move to prisma+auth0 stack from my airtable csv’s., hope I can get my way!


#16

@agusti Sounds good, I’ll prepare something to share come Monday. Have a great weekend. And yes, @LawJolla, thanks for getting all of this started


#17

@agusti @LawJolla I’ve put what I’ve been learning and my project structure into the post: https://medium.com/@lastmjs/advanced-graphql-directive-permissions-with-prisma-fdee6f846044


#18

Thanks for your sample @jordan.michael.last – I admit I’m a bit confused though! Which issue is your sample attacking exactly: a client calling a Query/Mutation with a permission directive without passing a required field (for instance, id), or exposing Prisma’s lower-level Query/Mutation resolvers directly to the client while adding permission directives to them? …or both?


#19

Hi @Sammy, it would be both. I guess I combined a few concepts together. The example shows exposing the entire generated Prisma schema with its resolvers to the client. That gets rid of all of the boilerplate of having to manually hook up each resolver that you want from the generated API to the client API. datamodel.graphql and dataops.graphql are where you place all of the custom directives that you want, in this case to implement permissions. To make the permissions work correctly, we add the fragments automatically to each type. Otherwise the client would have to send down all of the fields needed to do the authorization checks. Does that clear it up?


#20

Thanks for your prompt response @jordan.michael.last, it indeed does clear it up. I do have another question though: why would you want to expose Prisma to the client when you can simply use forwardTo?