elixirphoenix-frameworkgraphqlabsinthe

Graphql Absinthe Elixir permission based accessible fields


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.


Solution

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