Handling User Permissions in GraphQL
Handling permissions in single page application can be tricky.
Having to show controls depending on user's permissions often requires duplicating logic between server and client.
For example:
Let's say we have posts. Who can edit them? I guess their author can edit.
So this expressed as React components will be something like:
<Visible if={post.userId === currentUser.id}>
<EditPostButton post={post}
</Visible>
Then the GraphQL query for the page will be something like:
query PostPage($id: ID!) {
currentUser {
id
}
post(id: $id) {
id
userId
}
}
So far so good. But, can admins also edit this post? Guess so.
Handing this condition is quite simple. ?
<Visible if={post.userId === currentUser.id || currentUser.isAdmin}>
<EditPostButton post={post}
</Visible>
Then the GraphQL query for the page will be:
query PostPage($id: ID!) {
currentUser {
id
isAdmin
}
post(id: $id) {
id
userId
}
}
Now, what about if we have to add support multiple collaborators for a post.
Things start to get messy. ?
<Visible if={post.userId === currentUser.id || currentUser. isAdmin || post.collaboratorIds.indexOf(currrentUser.id) !== -1}>
<EditPostButton post={post}
</Visible>
If we continue to follow this approach, we have to:
- duplicate the permissions knowledge between frontend and backend
- expose the data responsible for calculating the permissions in the GraphQL API (some of this data should be hidden)
- remember to do so ?
At Product Hunt, we had this problem. The solution we came to was to expose the permissions as fields. Then the logic is contained only in the backend.
The GraphQL query will look like:
query PostPage($id: ID!) {
post(id: $id) {
id
canEdit: can(action: "edit")
}
}
Then the frontend code becomes simple.
<Visible if={post.canEdit}>
<EditPostButton post={post}
</Visible>
This is a lot cleaner. ?
Having to remap the field every time was tiring. Knowing which was the action name was tricky. Plus, we were too lazy to define enums for the permissions of every object set.
So we just started to expose every action as can[Action]
field:
query PostPage($id: ID!) {
post(id: $id) {
id
canEdit
}
}
This helps us have better documentation as well since it was self-evident what kind of permissions we had on a given type.