absinthe-graphql/absinthe_relay

node field resolution errors when schema-level middleware puts an error result in

dpehrson opened this issue · 3 comments

I was attempting to apply a schema-wide middleware that would restrict access to all mutations/query fields by default, white-listing only certain ones to non-authenticated/administrative users.

The middleware in question looks like this:

defmodule MyApp.Graph.Private.Middleware.AdminEnforcement do
  @behaviour Absinthe.Middleware

  def call(resolution, _config) do
    case resolution.context do
      %{current_user: %MyApp.User{is_admin: true}} -> resolution
      _ -> Absinthe.Resolution.put_result(resolution, {:error, "Internal error"})
    end
  end
end

I attempted to configure the middleware at the schema level to only allow non-admins to do the following:

  • Query only this portion of the graph: query { viewer { isAuthenticated, isAuthorized } } which would allow a client to check only whether they were logged in, and if so, whether they were in any way authorized to use this graph
  • Perform only the mutations necessary to authenticate/deauthenticate via mutation { authenticate } and mutation { relinquishToken }

The following middleware/3 definition was put in place at the schema level with help from @benwilson512

def middleware(middleware, field, object) do
  middleware
  |> apply_middleware(:admin_enforcement, field, object)
end

# Restrict all root query fields except `viewer` to administrators
defp apply_middleware(middleware, :admin_enforcement, %{identifier: identifier}, %{identifier: :query})
  when not identifier in [:viewer]
do
  [Middleware.AdminEnforcement | middleware]
end

# Restrict all viewer fields except `isAuthenticated` and `isAuthorized` to administrators
defp apply_middleware(middleware, :admin_enforcement, %{identifier: identifier}, %{identifier: :viewer})
  when not identifier in [:is_authorized, :is_authenticated]
do
  [Middleware.AdminEnforcement | middleware]
end

# Restrict all mutations except `authenticate` and `relinquishToken` to administrators
defp apply_middleware(middleware, :admin_enforcement, %{identifier: identifier}, %{identifier: :mutation})
  when not identifier in [:authenticate, :relinquish_token]
do
  [Middleware.AdminEnforcement | middleware]
end

defp apply_middleware(middleware, _name, _field, _object), do: middleware

However, when issuing the following query:

query { node(id: "...") { id }

The following error is thrown:

** (FunctionClauseError) no function clause matching in Absinthe.Relay.Node.resolve_with_global_id/2
    (absinthe_relay) lib/absinthe/relay/node.ex:116: Absinthe.Relay.Node.resolve_with_global_id(#Absinthe.Resolution<...>)

To work around this, I was able to exempt node from the enforcement at the schema-level just like viewer and then apply the same middleware manually within the node field definition in the query root definition.

For some reason applying this middleware at the field-level works, but applying it at the schema-level does not. After talking with @benwilson512 the assumption is that somewhere in absinthe_relay's node resolution code, it is not checking to see if resolution has been halted.

I hope this is enough to understand the potential issue, let me know if I can answer any other questions.

This morning I was able to reduce this to it's essence. The following is the minimum schema required to trigger this bug:

defmodule Schema do
  defmodule BadMiddleware do
    @behaviour Absinthe.Middleware

    def call(resolution, _config) do
      Absinthe.Resolution.put_result(resolution, {:error, "Internal error"})
    end
  end

  defmodule Root do
    defstruct id: "root"
  end

  use Absinthe.Schema
  use Absinthe.Relay.Schema

  def middleware(middleware, field, object) do
    [BadMiddleware | middleware]
  end

  node interface do
    resolve_type fn
      %Root{}, _ -> :root
      _, _ -> nil
    end
  end

  node object :root do

  end

  query do
    node field do
      resolve fn
        %{type: :root}, _info -> {:ok, %Root{}}
      end
    end
  end
end

Issuing the following query against that schema will result in the error:

query {
  node(id: "Um9vdDpyb290") {
    id
  }
}

Simply removing the middleware will allow the schema to behave properly.

Thanks! I'll take a look at it as soon as I can.

@benwilson512 np, I'll be gone until Tuesday for a family thing, but if you haven't solved it by then I'll try to get a PR up with at least a failing test and ideally a solution.