Graphql Absinthe Elixir permission based accessible fields

Viewed 1437

What is the proper way to define fields that may not be accessible to all users.

For example, a general user can query the users and find out another users handle, but only admin users can find out their email address. The user type defines it as a field but it may not be accessible. Should there be a separate type for what a general user can see? How would you define it?

Sorry if that isn't that clear I just don't possess the vocabulary.

2 Answers

@voger gave an awesome answer and I just wanted to post a macro sample based on the accepted question. I'm currently using it to authenticate every field in my schema.

Here's a macro definition:

defmodule MyApp.Notation do
  defmacro protected_field(field, type, viewers, opts \\ []) do
    {ast, other_opts} =
      case Keyword.split(opts, [:do]) do
        {[do: ast], other_opts} ->
          {ast, other_opts}

        {_, other_opts} ->
          {[], other_opts}
      end

    auth_middleware =
      if viewers !== :public do
        quote do
          middleware(MyApp.Middleware.ProtectedField, unquote(viewers))
        end
      end

    quote do
      field(unquote(field), unquote(type), unquote(other_opts)) do
        unquote(auth_middleware)
        middleware(Absinthe.Middleware.MapGet, unquote(field))
        unquote(ast)
      end
    end
  end
end

And then inside of your type definitions, you can do this.

import MyApp.Notation

# ...

object :an_object do
  protected_field(:description, :string, [User, Admin]) do
    middleware(OtherMiddleware)
    resolve(fn _, _, _ ->
      # Custom Resolve
    end)
  end

  protected_field(:name, :stirng, :public, resolve: &custom_resolve/2)
end

Explanation:

It adds an argument that I call viewers that I just forward to my middleware to check if the user type is correct. In this scenario, I actually have different models that I call Admin and User that I can check the current user against. This is just an example of one way to do it, so your solution might be different. I have a special case for :public fields that are just a passthrough.

This is great because I can inject middleware with the extra argument and forward everything else to the original field definition.

I hope this helps add to the answer.

Related