Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

It's pretty easy. Treat each resolver that retrieves data like it's a REST endpoint and secure it, and add a query allowlist that you append items to during your CI builds.

You don't need to touch the AST or understand the context of the rest of the query. Just answer the question "can user ABC see the photos of user XYZ?" in the resolver that fetches the photos. If this is inefficient then prefetch some data or use a dataloader.

Now, if you're using some magic library that turns GraphQL into SQL, that's going to be different.



Still I think this type of thing is much more likely to happen with GraphQL including various N + 1 and even worse performance issues.

Like if you imagine having junior engs they will be much more likely to make the mistake with GraphQL than otherwise and it is harder to review as well.

The permissions checking becomes a real spaghetti and difficult to understand in practice compared to just one by one checks.


The permissions checking is one-by-one checks. It's exactly as hard a mistake to make in GraphQL as it is in REST unless you've got more resolvers than an equivalent REST app would have, which is unlikely and would mean GraphQL wasn't a good choice.

I do think that you've got a good point about how the knowledge isn't widespread yet, that it's easier for frontend engineers to write awful expensive queries, and that GraphQL is very hard to secure against DoS unless you lock it down with query hashes.


> The permissions checking is one-by-one checks

Not true, authorization can be done in middleware. You can deny requests automatically, even scenarios you never considered.


I was responding to someone who was talking about one-by-one checks in REST. It is in fact true that using one-by-one checks in GraphQL is pretty similar to using them in REST.

You can do the equivalent of applying middleware at a routing level in GraphQL by wrapping multiple resolvers, although the semantics will be different because you're not working with a tree of routes and so you'll need to group your resolvers together in some other way. In the Node.js libraries a resolver is just a function, so you can very easily wrap a bunch of them in another function:

    // auth.js
    export const checkParent = (permission, fn) => (parent, args, ctx) => {
      ctx.can(permission, parent); // CASL
      return fn(parent, args, ctx);
    };

    // resolvers.js
    import * as auth from './auth';
    export const resolvers = {
      // could also iterate over all the resolvers within User using Object.entries and apply auth.checkParent if you wanted
      User: {
        photoURLs: auth.checkParent('read', (parent, _, ctx) => {
          return parent.getSignedPhotoURLs();
        }),
      },
      Query: {
        user: async (_, { id }, ctx) => {
          const user = await ctx.db.users.getById(id);
          ctx.can('read', user);
          return user;
        },
      },
    };
I'm not sure what you mean by "deny requests automatically" because there's obviously no manual step here, and equally obviously I'm not sure what you mean by "scenarios [I] never considered". Are you talking about rate limiting or heuristic detection? You can do those in GraphQL too.

Yes, this stuff is slightly different, but it's genuinely not that hard to secure a GraphQL API.


Wouldn't it seem contra to the principles of GraphQL if you treat resolvers like rest endpoints?

At this point, it's just RPC, no? It's not really a graph. Why didn't I just use RPC/Rest the whole time?


You don't treat resolvers like RESTful endpoints. You check that the user has permission to access the object (edit: or other value) which the resolver returns. This has nothing to do with RPC and does not stop you using the "graph" part of GraphQL.

For the purposes of comparing a REST API, where permissions checking is done for every endpoint, to a GraphQL API, where permissions checking is done for any resolver which loads data, it is necessary to compare the number of permissions checks you would need across the two services. This does not mean resolvers are in any way equivalent to RESTful endpoints except for comparing how many times you'd need to write `ctx.can('read', photo);` across the two, and even then the numbers will almost certainly be different because the APIs will be different.


The problem is the 'graph' nature of the system; you can check the permission for the object that the resolver returns but that object might be linked to another object that you're not checking for. Because anything can just link to anything, you would have to recursively check the permissions of the entire graph.


This does not match my experience.

If the root query lets you query a user of type User, and the User object embeds an array photos of type [Photo], then there are two possibilities: either the resolver for user is loading the photos and letting the default resolver return them, in which case you know about it and can check permissions for them, or there's a resolver defined for photos, in which case you can check permissions in that second resolver.

Think about it. GraphQL won't go retrieve rows from your database without either a) you installing some other library to do the magic, in which case we should talk about that library instead, or b) you telling it to query your database, in which case you know what data you're querying in each resolver you write and can check that the user has permission to see it.


How do you guys bridge the abstraction gap/wall between resolvers to prevent N+1 queries? I have the suspicion that GraphQL is great for exposing a really generic API, useful when you have no idea what shape the front end will take (how often is that?). But it comes at a heavy price; genericity is always the opposite of specialization. And optimization can only occur during specialization.

Having worked with it for a bit over a year now, it really feels like GraphQL is just a different protocol for writing the same old REST CRUD, while introducing a huge framework with lots of annoying magic and language level reflection that isn't amendable to extension or modification according to the needs of the developers.

Is that all worth it, just to reduce the amount of HTTP requests? Is it that much of a sacrilege to add specialized REST HTTP endpoints to remedy that otherwise?


Putting a dataloader in front of batch APIs usually works okay. You end up with round trips but they're 1+1 and inside the data centre. I've used AST traversal to compute joins a couple of fields ahead + custom resolvers that only load their data if it wasn't loaded by the parent, but I don't think that's necessary to get decent performance and I wouldn't do it again unless there was a real business need.

I agree that genericity is often the opposite of specialisation. I disagree that it's a heavy price. REST is pretty general. To my mind specialist APIs are things like streaming video, file uploads, anything that relies on caching in an intermediate layer, etc. and these are all examples of where you'd follow the established standards and add some RESTful routes/services. I don't think it's sacrilege to upload files in a different way to how you load your user dashboard or your interface for editing project permissions.


Also if it's really the problem with HTTP Requests, you could still technically abstract multiple REST API/RPC calls into a single HTTP Request.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: