In Dan Schafer's excellent "GraphQL at Facebook" talk from React Europe he goes over how centralizing authorization in business layer models avoids the problem of having to duplicate authorization logic for every edge that leads to an authorized node.
This works fine for something like Todo.getById(1)
which in my case eventually ends up querying a database for SELECT * from todos WHERE id=1
and then verifying authorization with checkCanSee(resultFromDatabase)
.
However, let's say my todos
table now contains 100,000 todos from multiple users, performing authorization purely in the business layer becomes impractical as I'd need to fetch every todo, filter the result using the shared authorization logic and then slicing that to perform pagination.
Am I wrong thinking that the only way to solve this is by letting authorization logic reside in the persistence layer itself?
I think one of the takeaways from Dan’s talk is the difference in how authorization is handled with GraphQL, as opposed to a typical REST endpoint.
In REST, each resource is typically associated with a single endpoint. When a request is made to that endpoint, it makes sense to check whether the requestor is authorized before processing the request. With GraphQL we may be fetching multiple resources within the same request, so this behavior is no longer desirable. As Dan puts it:
We don’t want to completely blow up the request if you can’t see one of [the requested resources].
So the preferred approach with GraphQL is to implement some kind of per-node mechanism for authorization, and to only return the resources the requester is authorized to see. And that is exactly what the example in the talk shows – one way of doing that.
If you store your to-dos in a SQL database table, it would make perfect sense for your code to just make a query like SELECT * from todos WHERE creator_id=${viewer.id}
and omit using a function like checkCanSee
altogether.
Similarly, you can bake pagination right into your query with limit-offset, cursors, etc. And yes, since you’re now letting your DB do the heavy lifting, you could say that we’ve moved into the persistence layer. However, it’s still up to your business logic to take the request, sanitize the inputs, construct an appropriate query and return the results in a form GraphQL can use.
I can’t speak for Dan, but I imagine his intent was not to suggest this was the only (or even optimal) way to implement authorization for a node. I think the bigger point is that if you are, for example, fetching:
{
header
todos {
description
}
quoteOfTheDay
}
even an unauthorized client should still get a response back from the server that it can then use to render a page for the end-user (even if that response includes an empty array of to-dos).
After another round of searching for answers I stumbled upon a few interesting comments from Lee Byron that shed some light on the subject:
The storage layer shouldn't deal with authorization, but it should expose APIs that limit the amount of data that's being returned.
In the above scenario dealing with hundreds of thousands of todos this could be implemented by exposing something
like getTodosByUserId
which returns all todos beloning to a particular user. The business logic layer
would then deal with both authorization and pagination by filtering a slicing the result. But what if
a user has thousands of todos? One would probably add another filter option to the storage layer
such as getTodosByUserId({ userId: 1, completed: false })
.
An important take-away from the "Let's talk about caching" comment by Lee is that results from the storage layer should be easily cachable in something like Redis or memcached. Fetching a thousand todos from Redis and then filtering and slicing them in your business logic layer is probably a lot less expensive than having to query an SQL database for every request.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With