Loading relationships from session

Alternative title: A yearning for mackerel

I have a specific problem (I want to load a :team resource on the current_user that AshAuthentication puts into conn assigns. I could do this as a follow-on query, but I’m trying to do it in one).

But really, I want to learn to fish solve this myself.

I tried adding a preparation, at first to the global preparations, and later to get_by_subject which seems to be used by the password strategy.

Either way, I get errorless redirects back to the sign-in page as if I wasn’t logged in. I can’t see anything with logging set to :debug, I can’t see anything relevant with log_successful_policy_breakdowns or policy_breakdowns turned on.

What are my next steps for finding out why ash_auth is failing here? I’m assuming there must be another way to get to an error code, though I accept it may be something that I simply need to understand and avoid. (I also assume there may be a better way of doing this team load).

Any tips are much appreciated.

Not seeing any logs seems a bit strange.

I assume you have some policies for your team, because AshAuth calls the read action with a private context set that uses the bypass policy set up by AshAuth

    bypass AshAuthentication.Checks.AshAuthenticationInteraction do
      authorize_if always()
    end

# in the policy  
def match?(_, %{subject: %{context: %{private: %{ash_authentication?: true}}}}, _), do: true

So, there is not really an actor at that point, which could lead to the load failing. You could use the accessing_from policy to allow a user to read their team if they are loading through a relationship

policy action_type(:read) do
  authorize_if accessing_from(User, :team)
end

And than the LiveUserAuth module would redirect because there is no user.

What did you set log_successful_policy_breakdowns / policy_breakdown to? I think it has to be a log level.

Otherwise, using a preparation for this is a valid approach.

After going through the source a bit more and experimenting, I found the issue. Our Team resource has multi-tenancy turned on. I’ve fixed the issue now (setting tenant in our router) but I’m still keen to learn how I could have debugged this better.

As it stands, I got zero warnings about the multitenancy issue (even with all debug stuff on), so maybe ash_auth is incorrectly swallowing a warning somewhere? (If I can work out where, I’m happy to PR something)

I’ll paste the configs and logs to demonstrate.

I have a user (99% setup by the ash_auth igniter)

  # user.ex
...
  policies do
    bypass AshAuthentication.Checks.AshAuthenticationInteraction do
      authorize_if always()
    end
    
    # I have temporarily set this policy to always allow to rule it out of debugging
    policy always() do
      authorize_if always()
    end
  end
...
actions do
  ...
  read :get_by_subject do
    description "Get a user by the subject claim in a JWT"
    argument :subject, :string, allow_nil?: false
    get? true
    prepare AshAuthentication.Preparations.FilterBySubject
    prepare build(load: :team) # This is the problematic line
  end
  ...
# dev.exs
...
config :logger, level: :debug
config :ash, :policies, log_policy_breakdowns: :debug
config :ash, :policies, log_successful_policy_breakdowns: :debug
config :ash, :policies, show_policy_breakdowns?: true
...

Here are the full logs if I don’t have the prepare statement active.

[info] GET /
[debug] Processing with WinkWeb.PageController.home/2
  Parameters: %{}
  Pipelines: [:browser, :require_session, :require_team]
[debug] Successful authorization: Wink.Accounts.User.get_by_subject


Policy Breakdown
unknown actor

  Bypass: Policy | 🌟:

    condition: AshAuthentication is performing this interaction

    authorize if: always true | ✓ | 🌟

  Policy | 🌟:

    condition: always true

    authorize if: always true | ✓ | 🌟


[debug] QUERY OK source="users" db=0.3ms idle=1002.1ms
SELECT u0."id", u0."role", u0."email", u0."team_id", u0."hashed_password" FROM "users" AS u0 WHERE (u0."id"::uuid = $1::uuid) ["8e59877b-82d6-4379-ac11-deeb88c8f99a"]
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:767
[info] Sent 200 in 6ms

Here are the logs if I do have the prepare statement active

[info] GET /
[debug] Processing with WinkWeb.PageController.home/2
  Parameters: %{}
  Pipelines: [:browser, :require_session, :require_team]
[debug] Successful authorization: Wink.Accounts.User.get_by_subject


Policy Breakdown
unknown actor

  Bypass: Policy | 🌟:

    condition: AshAuthentication is performing this interaction

    authorize if: always true | ✓ | 🌟

  Policy | 🌟:

    condition: always true

    authorize if: always true | ✓ | 🌟


[debug] QUERY OK source="users" db=0.2ms idle=1292.0ms
SELECT u0."id", u0."role", u0."email", u0."team_id", u0."hashed_password" FROM "users" AS u0 WHERE (u0."id"::uuid = $1::uuid) ["8e59877b-82d6-4379-ac11-deeb88c8f99a"]
↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:767
[info] Sent 302 in 2ms
[debug] Phoenix.Router halted in :require_session/2
# At this point, the conn has no `current_user` and so gets bounced by our checks for current user 

What I can see is that the same SQL is run, the same policy is run (and passes), but I mysteriously don’t have a current_user being set on the conn. As I said above, I assume an Ash.Error.Invalid.TenantRequired is being thrown somewhere but caught by AshAuth.

I think this happens here:

This calls the action on the user resource internally, and if that returns an error, nil is set as the user. It seems the tenant error is detected before any policies are run, so you don’t get a log for the team.

Not sure about how you could debug this better. But here is the approach I took.

I looked at it more like a regular bug than anything Ash specific. From the little LiveView experience I had, I knew that the user would be most likely set in a live_session. So i looked at the router setup and saw the ash_authentication_live_session setup by the ash_auth igniter and went to it’s definition. There I saw that the Module was added to the list of on_mount hooks. That lead me to look at the on_mount function in the module.
I saw the call to AshAuthentication.subject_to_user and looked at that function. There I saw the Ash.Query being constructed and the read being called.

I saw that there was this special context that would allow to bypass the policies and that there is no real actor yet. This is what I put in my first answer.

As a next step I would add a debug statement there. To see what is actually returned from the read.

I’m not sure how you would integrate the tenant here, though. Maybe instead of having just a preparation that adds the load, you create your own that looks at the subject to get the tenant and adds the load with an initial query that sets the tenant.

Thanks Barnaby, I may PR a logger statement there or something so that failure errors are at least logged.

No next step needed on the issue itself, I solved the tenancy issue with Ash.PlugHelpers.set_tenant in the router, this is picked up by AshAuth.

Thanks for your help :heart:

Yeah, that was my bad. I missed the tenant being set here ash_authentication_phoenix/lib/ash_authentication_phoenix/live_session.ex at main · team-alembic/ash_authentication_phoenix · GitHub

1 Like