How To set_actor global and access it in the custom policy? Using `plug :set_user, :user` in router gives nil actor in the custom policy

How can I set the actor globally after the user successfully logs in, so that I don’t have to pass actor in every query.

Here 's what I did so far. It works if I manually set actor while querying, but it does not work with the actor set in the plugs.

I added set_actor in the Phoenix router plugs like the following

defmodule KamaroWeb.Router do
  use KamaroWeb, :router
  use AshAuthentication.Phoenix.Router
  import AshAuthentication.Plug.Helpers # <-- ADDED THIS

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {KamaroWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug :load_from_session
    plug :set_actor, :user # <-- ADDED THIS LINE
  end

I inspected and indeed confirmed that the actor is being set in the connection privates.

However when I inspect the actor from the Policy, it is nil.

defmodule Kamaro.Checks.Can do
  use Ash.Policy.SimpleCheck

  def describe(_opts) do
    "Check if a user/ actor has permission on a specific resource"
  end

  def match?(actor, context, opts) do
    dbg(actor) # <-- ACTOR IS NIL HERE
    {:ok, true}
  end
end

But, when I query by passing the actor like the following, the actor is not nil.

Stock.get_categories!(actor: user)

Here is how the resource policy is configured

  policies do
    policy action_type(:read) do
      authorize_if Kamaro.Checks.Can
    end
 end

It is (intentionally) not possible to do this. You have to pass the actor to each call.

1 Like

Thank you for the clarification @zachdaniel.

What is the intent behind that design decision? I can see points on both sides.

Benefits of explicit passing:

  • Leaving this to magic action-at-a-distance is bad
  • Making this assumption at the framework level could be a step too far
  • It complies with functional programming design, with more pure functions

Benefits of a global actor:

  • Leaving this to application code that might be insufficiently tested is bad
  • Its easier to ignore or remove a provided actor than to arrange for one to be present whenever needed
  • Only a few functions would be made impure by the introduction of this global context, most functions of interest already receive a context argument

We had this feature in 2.0, and ultimately removed it due to the multitude of footguns that presented themselves. Two such examples:

Code is no longer portable across processes

def mount(...) do
  Ash.set_actor(current_user)

  socket
  |> assign(:posts, Ash.read!(Post))
  # |> async_assign(:posts, fn -> Ash.read!(Post) end) # can't refactor to this
end

Given that many things that exist in the elixir ecosystem take a callback which will (or may or may not at the discretion of whats being called) run in another process, design that depends on the process context for security can be very problematic. Even something simple like using some Task.async to parallelize a few read actions risks introducing very non-obvious security bugs.

Actions calling other actions

Given this example:

# in a resource called Post
update :add_comment do
  change after_action(fn changeset, result, context -> 
    Comment
    |> Ash.Changeset.for_create(:create, %{}) # context is *not* passed through here
    |> Ash.create!()
  end)
end

If you do this:

Domain.add_comment!(post, actor: actor)

the call to Comment’s :create action does not receive an actor.

But if you do

Ash.set_actor(actor)

Domain.add_comment(post)

now the call to create a comment sees the process context actor and uses it. This is especially problematic because when you are writing logic in hooks, you are encapsulating logic, and the ability for some external source to make changes to how changes/hooks in your action run is very dangerous.

When we talked about removing it, we brought the above two things up, and got conflicting feedback that was especially illustrative of why this had to be removed.

Some people were upset to discover that Ash.set_actor set the actor in hooks as well, and realized there were likely bugs in their code.

But also, some people were relying on that behaviour, and saw it as a feature.

Leading up to Ash 3.0, the major design epic was missing major pieces in Ash core, specifically bulk actions and atomics. But the theme of changes made post 3.0 is less surprises/sharp edges and better developer experience, and the removal of the process dictionary features is more in line with those goals.

It may be more annoying, but without the process-dictionary features, your code does exactly what it looks like it does.

1 Like

Well put, thanks.

1 Like