Graphql Absinthe Elixir permission based accessible fields

1.6k Views Asked by At

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

There are 2 best solutions below

0
On BEST ANSWER

Edit: Caution: Graphql documentation disagrees with this approach. Use with caution. Wherever you need a private field you must include the appropriate middlewares.

Use absinthe middleware.

Here is some code how to do it. In this example the authenticated user can see the email addresses. The anonymous user can't. You can adjust the logic to require whatever permissions you want.

defmodule MySite.Middleware.RequireAuthenticated do
  @behaviour Absinthe.Middleware

  @moduledoc """
  Middleware to require authenticated user
  """

  def call(resolution, config) do
    case resolution.context do
      %{current_user: _} ->
        resolution
      _ ->
        Absinthe.Resolution.put_result(resolution, {:error, "unauthenticated"})
    end
  end
end

and then you define your object:

  object :user do
    field :id, :id
    field :username, :string 
    field :email, :string do
      middleware MySite.Middleware.RequireAuthenticated
      middleware Absinthe.Middleware.MapGet, :email
    end
  end

So our field email is protected by the RequireAuthenticated middleware. But according to the link above

One use of middleware/3 is setting the default middleware on a field, replacing the default_resolver macro.

This happens also by using the middleware/2 macro on the field. This is why we need to also add

  middleware Absinthe.Middleware.MapGet, :email

to the list of middlewares on the field.

Finally when we perform a query

query {
  user(id: 1){
    username
    email
    id
  }
}

We get the response with the open fields filled and the protected fields nullified

{
  "errors": [
    {
      "message": "In field \"email\": unauthenticated",
      "locations": [
        {
          "line": 4,
          "column": 0
        }
      ]
    }
  ],
  "data": {
    "user": {
      "username": "MyAwesomeUsername",
      "id": "1",
      "email": null
    }
  }
}

You can also use the middleware/3 callback so your object don't get too verbose

  def middleware(middleware, %{identifier: :email} = field, _object) do
    [MySite.Middleware.RequireAuthenticated] ++
      [{Absinthe.Middleware.MapGet, :email}] ++
      middleware
  end

With a bit of creative use of the __using__/1 callback you can get a bunch of such functions out of your main schema file.

0
On

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