AE
Ash Elixirโ€ข3y ago
Blibs

AshGraphql generated mutations are bypassing schema middleware

I have this middleware in my GraphQL schema to check if the user is authenticated:
def middleware(middleware, field, obj) do
if obj.identifier in [:query, :subscription, :mutation] and
field.identifier not in @not_auth_apis do
IO.puts("got here 1!")
[Middlewares.EnsureAuthenticated | middleware]
else
IO.puts("got here 2!")
middleware
end
end
def middleware(middleware, field, obj) do
if obj.identifier in [:query, :subscription, :mutation] and
field.identifier not in @not_auth_apis do
IO.puts("got here 1!")
[Middlewares.EnsureAuthenticated | middleware]
else
IO.puts("got here 2!")
middleware
end
end
All my custom made queries/mutations will reach that middleware function without a problem. Now, I have this in one of my resources:
graphql do
type :offer

queries do
end

mutations do
create :place_offer, :place_offer
end
end
graphql do
type :offer

queries do
end

mutations do
create :place_offer, :place_offer
end
end
If I call that mutation, it will not call my middleware function at all. If I create an equivalent for that mutation by hand, then the middleware is called as expected.
36 Replies
ZachDaniel
ZachDanielโ€ข3y ago
๐Ÿค” strange can I see how you're adding the middleware to the schema? This might be a question we need to ask the absinthe folks about
Blibs
BlibsOPโ€ข3y ago
I just added the middleware function to the schema:
defmodule Marketplace.GraphQL.Schema do
alias Marketplace.GraphQL.Middlewares

alias Marketplace.Accounts

use Absinthe.Schema

@apis [Marketplace.Markets, Marketplace.Accounts]

use AshGraphql, apis: @apis

@not_auth_apis [
:register_with_password,
:sign_in_with_password,
:password_reset_with_password
]

import_types Accounts.User.GraphQL
import_types Marketplace.Markets.Property.Offer.GraphQL

query do
import_fields :accounts_user_queries
end

mutation do
import_fields :accounts_user_mutations
import_fields :markets_property_offer_mutations
end

def middleware(middleware, field, obj) do
if obj.identifier in [:query, :subscription, :mutation] and
field.identifier not in @not_auth_apis do
IO.puts("got here 1!")
[Middlewares.EnsureAuthenticated | middleware]
else
IO.puts("got here 2!")
middleware
end
end

def context(context), do: AshGraphql.add_context(context, @apis)

def plugins, do: [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
end
defmodule Marketplace.GraphQL.Schema do
alias Marketplace.GraphQL.Middlewares

alias Marketplace.Accounts

use Absinthe.Schema

@apis [Marketplace.Markets, Marketplace.Accounts]

use AshGraphql, apis: @apis

@not_auth_apis [
:register_with_password,
:sign_in_with_password,
:password_reset_with_password
]

import_types Accounts.User.GraphQL
import_types Marketplace.Markets.Property.Offer.GraphQL

query do
import_fields :accounts_user_queries
end

mutation do
import_fields :accounts_user_mutations
import_fields :markets_property_offer_mutations
end

def middleware(middleware, field, obj) do
if obj.identifier in [:query, :subscription, :mutation] and
field.identifier not in @not_auth_apis do
IO.puts("got here 1!")
[Middlewares.EnsureAuthenticated | middleware]
else
IO.puts("got here 2!")
middleware
end
end

def context(context), do: AshGraphql.add_context(context, @apis)

def plugins, do: [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
end
As far as I know, it should be called for every query/mutation/subscription used in the schema Unless the way ash adds its queries/mutations are different than the way I'm adding my custom ones
ZachDaniel
ZachDanielโ€ข3y ago
Thats the only thing I can think of really but it would be a pain for that to work that way TBH looking into it now
Blibs
BlibsOPโ€ข3y ago
I was wondering, can't we change this (see image) to allow the user to add custom middlewares per query/mutation? That way it will be able to set a middleware per query/mutation no?
No description
ZachDaniel
ZachDanielโ€ข3y ago
We could ๐Ÿ˜„ but to some degree it gets into how much configurability is too much configurability. Ash should provide the tools to do these kinds of things in a way that works for graphql or json api or other transports. I've just pushed an experimental option up to main
use AshGraphql, apis: @apis, action_middleware: [Middlewares.EnsureAuthenticated]
use AshGraphql, apis: @apis, action_middleware: [Middlewares.EnsureAuthenticated]
Blibs
BlibsOPโ€ข3y ago
Hmm, but I think that would also be problematic no? I mean, I need to check the obj and field variables to see if I will apply the EnsureAuthenticated middleware or not (since I also do sign-up/in using graphql). I'm not sure if I have that information inside the middleware itself
ZachDaniel
ZachDanielโ€ข3y ago
Yeah, you'd have to put this logic:
if obj.identifier in [:query, :subscription, :mutation] and
field.identifier not in @not_auth_apis do
if obj.identifier in [:query, :subscription, :mutation] and
field.identifier not in @not_auth_apis do
inside the middleware. Is that bad? FWIW if you want to have your special error when a user is not present you could just put that in the action
Blibs
BlibsOPโ€ข3y ago
Hmm, let me check the middleware documentation again, I'm not sure I have these variables inside the middleware
ZachDaniel
ZachDanielโ€ข3y ago
create :create do
change RequireActor
end
create :create do
change RequireActor
end
but the action middleware option that I provided only runs at the top level on queries/mutations build by Ash So if you want a user present for all actions run through Ash, that would be the way to do it. It doesn't happen for fields
Blibs
BlibsOPโ€ข3y ago
I think I can add the logic to the middleware using resolution.definition.schema_node with will contain : place_offer in this case
ZachDaniel
ZachDanielโ€ข3y ago
So you do only want this for specific actions? or for all actions? If its for specific actions, why not this?
create :place_offer do
change RequireActor
end
create :place_offer do
change RequireActor
end
Blibs
BlibsOPโ€ข3y ago
Ah, I think I got the whole thing wrong So, it is not like the middleware function is not being called, it is. It is called when the system starts. For some reason, it is not called when I run the query/mutation itself (it is called when I run my custom ones). But the middleware will be called anyway. The odd thing is that the middleware will be called, it will set the result but the call will never reach AshGraphQL.Graphql.Resolver it will go straight to the policies and fail there I guess the question is, why it does that, I would expect that it will first call resolve inside the AshGraphql.Graphql.Resolver module no?
ZachDaniel
ZachDanielโ€ข3y ago
๐Ÿค” okay so I get what you're saying about the middleware callback firing when we build the schema not every time. But I'm not sure what you mean about the rest resolve runs the action which runs the policies
Blibs
BlibsOPโ€ข3y ago
I added this to the Resolver module:
defmodule AshGraphql.Graphql.Resolver do
@moduledoc false

require Ash.Query
require Logger
import AshGraphql.TraceHelpers

def resolve(resolution, _) do
IO.inspect(resolution.state, label: ">>>>>>>>>>>>>>>> CATCH ALL")
resolution
end
...
defmodule AshGraphql.Graphql.Resolver do
@moduledoc false

require Ash.Query
require Logger
import AshGraphql.TraceHelpers

def resolve(resolution, _) do
IO.inspect(resolution.state, label: ">>>>>>>>>>>>>>>> CATCH ALL")
resolution
end
...
And recompiled that module. Then, I run my mutation with playground, it will never reach that resolve function, instead it will fail in one of my policies I will run it with dbg to see if I can pry there
ZachDaniel
ZachDanielโ€ข3y ago
um...are you doing mix deps.compile ash_graphql after changing the dependency? I think this might be off topic
Blibs
BlibsOPโ€ข3y ago
Yep
ZachDaniel
ZachDanielโ€ข3y ago
Lets clarify one thing: Do you want: 1. to require a user for every single action that calls your apis, except your custom authentication mutations/queries. 2. to require a user for specific actions
Blibs
BlibsOPโ€ข3y ago
I want to require a user for a list of queries/mutations regardless if they are custom or created with Ash. Right now, using middleware in my schema, I can achieve this partially since it works fine with any custom query that I'm using. The problem is that the middleware is not working with AshGraphql generated queries/mutations. To be more specific, I can see that the middleware is being called, it is just that the AshGraphql.Graphql.Resolver resolve function is never called before it reaches the policies, so I will never get the middleware error
ZachDaniel
ZachDanielโ€ข3y ago
Okay, I'm on board with everything up until the last part, I'm not convinced that is the error. You can't get a policy failure without calling the resolver Are you on the latest ash_graphql from github?
Blibs
BlibsOPโ€ข3y ago
I added a catch all function to the resolver, it even complains that I will not reach the other resolve functions because of that when I compile it:
sezdocs@pop-os:~/projects/rebuilt/marketplace$ mix deps.compile ash_graphql
==> ash_graphql
Compiling 2 files (.ex)
warning: this clause for resolve/2 cannot match because a previous clause at line 8 always matches
lib/graphql/resolver.ex:13

warning: this clause for resolve/2 cannot match because a previous clause at line 8 always matches
lib/graphql/resolver.ex:18

warning: this clause for resolve/2 cannot match because a previous clause at line 8 always matches
lib/graphql/resolver.ex:140

warning: this clause for resolve/2 cannot match because a previous clause at line 8 always matches
lib/graphql/resolver.ex:227
sezdocs@pop-os:~/projects/rebuilt/marketplace$ mix deps.compile ash_graphql
==> ash_graphql
Compiling 2 files (.ex)
warning: this clause for resolve/2 cannot match because a previous clause at line 8 always matches
lib/graphql/resolver.ex:13

warning: this clause for resolve/2 cannot match because a previous clause at line 8 always matches
lib/graphql/resolver.ex:18

warning: this clause for resolve/2 cannot match because a previous clause at line 8 always matches
lib/graphql/resolver.ex:140

warning: this clause for resolve/2 cannot match because a previous clause at line 8 always matches
lib/graphql/resolver.ex:227
And still it doesn't reaches that function Yes, I'm using the main branch to test that action_middleware change you made
ZachDaniel
ZachDanielโ€ข3y ago
huh weird
Blibs
BlibsOPโ€ข3y ago
I will rollback to the latest version and see if I get the same problem
ZachDaniel
ZachDanielโ€ข3y ago
Is there a stacktrace or anything for the policy error?
Blibs
BlibsOPโ€ข3y ago
I made my policy authorize all now just to see if it would reach the resolver, but it doesn't, it will go straight to the action Here is the stacktrace:
[error] c7d26166-e1aa-4af8-be04-4ed209423bde: Exception raised while resolving query.

** (KeyError) key :id not found in: nil. If you are using the dot syntax, such as map.field, make sure the left-hand side of the dot is a map
(marketplace 0.1.0) lib/marketplace/markets/property/offer.ex:38: Marketplace.Markets.Property.Offer.change_0_generated_BEB099BF14879F468430A35E2742B5A0/2
(ash 2.6.10) lib/ash/changeset/changeset.ex:1049: anonymous fn/6 in Ash.Changeset.run_action_changes/6
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.6.10) lib/ash/changeset/changeset.ex:711: Ash.Changeset.do_for_action/4
(ash 2.6.10) lib/ash/actions/create.ex:209: anonymous fn/11 in Ash.Actions.Create.as_requests/5
(ash 2.6.10) lib/ash/engine/request.ex:1036: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.6.10) lib/ash/engine/request.ex:917: Ash.Engine.Request.do_try_resolve/5
(elixir 1.14.0) lib/enum.ex:4751: Enumerable.List.reduce/3
(elixir 1.14.0) lib/enum.ex:2514: Enum.reduce_while/3
(ash 2.6.10) lib/ash/engine/request.ex:631: Ash.Engine.Request.do_strict_check/3
(ash 2.6.10) lib/ash/engine/request.ex:522: anonymous fn/2 in Ash.Engine.Request.strict_check/2
(elixir 1.14.0) lib/enum.ex:4751: Enumerable.List.reduce/3
(elixir 1.14.0) lib/enum.ex:2514: Enum.reduce_while/3
(ash 2.6.10) lib/ash/engine/request.ex:255: Ash.Engine.Request.do_next/1
(ash 2.6.10) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.6.10) lib/ash/engine/engine.ex:661: Ash.Engine.advance_request/2
(ash 2.6.10) lib/ash/engine/engine.ex:567: Ash.Engine.fully_advance_request/2
(ash 2.6.10) lib/ash/engine/engine.ex:508: Ash.Engine.do_run_iteration/2
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.6.10) lib/ash/engine/engine.ex:268: Ash.Engine.run_to_completion/1
[error] c7d26166-e1aa-4af8-be04-4ed209423bde: Exception raised while resolving query.

** (KeyError) key :id not found in: nil. If you are using the dot syntax, such as map.field, make sure the left-hand side of the dot is a map
(marketplace 0.1.0) lib/marketplace/markets/property/offer.ex:38: Marketplace.Markets.Property.Offer.change_0_generated_BEB099BF14879F468430A35E2742B5A0/2
(ash 2.6.10) lib/ash/changeset/changeset.ex:1049: anonymous fn/6 in Ash.Changeset.run_action_changes/6
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.6.10) lib/ash/changeset/changeset.ex:711: Ash.Changeset.do_for_action/4
(ash 2.6.10) lib/ash/actions/create.ex:209: anonymous fn/11 in Ash.Actions.Create.as_requests/5
(ash 2.6.10) lib/ash/engine/request.ex:1036: Ash.Engine.Request.do_try_resolve_local/4
(ash 2.6.10) lib/ash/engine/request.ex:917: Ash.Engine.Request.do_try_resolve/5
(elixir 1.14.0) lib/enum.ex:4751: Enumerable.List.reduce/3
(elixir 1.14.0) lib/enum.ex:2514: Enum.reduce_while/3
(ash 2.6.10) lib/ash/engine/request.ex:631: Ash.Engine.Request.do_strict_check/3
(ash 2.6.10) lib/ash/engine/request.ex:522: anonymous fn/2 in Ash.Engine.Request.strict_check/2
(elixir 1.14.0) lib/enum.ex:4751: Enumerable.List.reduce/3
(elixir 1.14.0) lib/enum.ex:2514: Enum.reduce_while/3
(ash 2.6.10) lib/ash/engine/request.ex:255: Ash.Engine.Request.do_next/1
(ash 2.6.10) lib/ash/engine/request.ex:211: Ash.Engine.Request.next/1
(ash 2.6.10) lib/ash/engine/engine.ex:661: Ash.Engine.advance_request/2
(ash 2.6.10) lib/ash/engine/engine.ex:567: Ash.Engine.fully_advance_request/2
(ash 2.6.10) lib/ash/engine/engine.ex:508: Ash.Engine.do_run_iteration/2
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(ash 2.6.10) lib/ash/engine/engine.ex:268: Ash.Engine.run_to_completion/1
The KeyError is because the actor is nil btw
ZachDaniel
ZachDanielโ€ข3y ago
can you put this in your start function of your application :erlang.system_flag(:backtrace_depth, 100) so we can see a longer backtrace I just can't think of any way that we would be running a resource action without calling the resolve function thats like...where it all happens
Blibs
BlibsOPโ€ข3y ago
Oh, wow, thanks for that, I didn't know about that system_flag, it will help me a lot for debug from now on ๐Ÿ˜
Blibs
BlibsOPโ€ข3y ago
ZachDaniel
ZachDanielโ€ข3y ago
There it is mutate/2 is what we're calling not resolve/2 ๐Ÿ™‚
Blibs
BlibsOPโ€ข3y ago
Ahรก! let me test that, one sec Yep, that was it hahah def mutate(%Absinthe.Resolution{state: :resolved} = resolution, _), do: resolution Adding this to the resolver.ex file will fix the issue
ZachDaniel
ZachDanielโ€ข3y ago
ohhhhhhh okay good catch
Blibs
BlibsOPโ€ข3y ago
Do you know what function absinthe will use in case of subscriptions? I'm not using it yet, but if it is another function, then probably we need the same change there also
ZachDaniel
ZachDanielโ€ข3y ago
pushed Um...subscriptions don't really work quite the same I'll add it to the other resolvers though, resolve_calc and resolve_assoc
Blibs
BlibsOPโ€ข3y ago
Working like a charm! I don't think we need the action_middlewares btw
ZachDaniel
ZachDanielโ€ข3y ago
Yeah, probably not. It doesn't hurt to leave it there though. Like if you want something specific to happen for all ash actions done via graphql
Blibs
BlibsOPโ€ข3y ago
Do you plan to push a new release to ash_graphql shortly or should I just stay in main branch for now?
ZachDaniel
ZachDanielโ€ข3y ago
new release is going out soon, once CI is done

Did you find this page helpful?