Absinthe Authorisation Questions / Thoughts

The Absinthe book is thought provoking,

https://pragprog.com/book/wwgraphql/craft-graphql-apis-in-elixir-with-absinthe

and has helped me navigate the library.

I was wondering about a few things.

Putting Authorisation in Schema middleware is on the one hand convenient, e.g.

#schema
field :user_profiles, list_of :user_profile do
      middleware Schema.Middleware.Authorise, :some_permission
      (&UserProfileResolver.all/2) |> Utils.handle_errors |> resolve
    end

on the other repetitive (because I have have so many useful ‘edges’ to protect as well as ‘nodes’), e.g.

# types
object :user do
    field :id, non_null :positive_int
    ...
    field :user_profile, :user_profile do
      resolve &UserResolver.user_profile_association/3
    end
    ...
  end

Taking FB’s advice I will push my authorisation into business logic and away from the schema, where I have one source of truth, which seems sensible.

Perhaps it would be good to emphasise this trade-off more in the book?

The book does in fact show a way of pushing authentication context into the business logic & resolvers, e.g.

# schema
field :user_profiles, list_of :user_profile do
      (fn a, b, %{context: context} -> UserProfileResolver.all a, b, context end) 
      |> Utils.handle_errors 
      |> resolve
end

It seems like a good, and potentially popular way to implement things.

Is there a neater way of doing this? I suspect there is, but I cannot figure it out.

It seems v verbose, but perhaps that’s OK.

Here I found a nice way of treating partially errored results, which is really smart, especially when it comes to authorisation.

Is there anywhere I can find some good examples for this?

I suspect it’s the nature of the GraphQL beast that many edges / fields need such a Union type. Perhaps providing something like a ‘maybe’ or ‘option’ type out of the box from the library would be helpful…

4 Likes

I move all of authorisation into what’s currently called “contexts” around here. Not sure if it’s any better though.

1 Like

Do you do something like this?

type fields <-> permissions
schema fields <-> permissions

I think I am better off doing it close to the ecto db queries, and as I said, have one source of truth

Do you do something like this?

type fields <-> permissions
schema fields <-> permissions

Not sure … Web/graphql layer doesn’t actually know anything about authorization in my case.

All of the protected functions in a context accept additional parameters that help authorize the action.

@spec create_resource(attrs, by: User.t) :: {:ok, Resource.t} | {:error, Ecto.Changeset.t} | no_return
def create_resource(attrs, by: %User{id: user_id} = user) do
  Resource.Policy.authorize!(:create, user)

  %Resource{owner_id: user_id}
  |> Resource.changeset(attrs)
  |> Resource.insert()
end

Same for get, update, …

If the action is not authorized, currently I raise an UnauthorizedError with a plug status set. I can catch this exception and return nil or empty list for absinthe to render partially erred results (haven’t tried it yet though).

2 Likes

Yep, this looks similar to my thinking (without the exceptions - I try to avoid those whenever poss, but maybe I should revisit after seeing this!)

Also, I will have a union type, so in MLish pseudocode…

Users = (…, MaybeGroups)
MaybeGroups = Groups | UnauthorisedError
Groups = [Group]

The only thing is, the FB spec recommends returning a null or [], as you suggest, and then include a list of errors in the returned JSON…

This way, I suppose the error will be under groups, i.e.

users = {…, groups: {"error", "unauthorised"}}

which isn’t bad per se, just perhaps not idiomatic.

Hmm.

I’m a bit curious where you saw the Utils.handle_errors pattern. The book would have been promoting a middleware based approach, which isn’t what is happening here. The approach here will fail if any asynchronous execution is used by the resolver.

2 Likes

Good pt! I must’ve picked it up somewhere, I will amend it : ) thanks : )

Is it possible to completely skip sending a response on trigger as opposed to sending back an error? In our use case, we want to send data over subscriptions for only the posts the user is a member of, and not send anything at all in other cases.

Is there a straight-forward way of doing that?

Hey @sheharyarn, this sort of thing should be handled by how you organize your topic so that you aren’t trying to run documents that shouldn’t be sent. For example in your use case, when each user sends a subscription in they could do something like:

subscription {
  postsForMe { title body }
}

And this would generate a topic: "#{user.id}"

Then when a forum post is updated or posted you should:

# roughly
user_ids = post |> Ecto.assoc(:members) |> Repo.all |> Enum.map(& &1.user_id)
Absinthe.Subscription.publish(MyApp.Web, post, posts_for_me: user_ids)

Publishing to a topic that has no documents is very cheap, it’s just an :ets lookup. Running a document that you don’t want to send out is much more expensive.

3 Likes