Absinthe Authorisation Questions / Thoughts


#1

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…


#2

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


#3

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


#4

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).


#5

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.


#6

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.


#7

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