ash-project / ash_admin

A super-admin UI dashboard for Ash Framework applications, built with Phoenix LiveView.
https://hexdocs.pm/ash_admin
MIT License
106 stars 48 forks source link

Setting user in schema based multitenancy throws error #100

Closed carlgleisner closed 8 months ago

carlgleisner commented 8 months ago

Describe the bug

Trying to set the current user with the key-button int he UI in a schema based multitenancy setup throws an error.

Queries against the MyApp.Accounts.User resource require a tenant to be specified

To Reproduce

defmodule MyApp.Accounts do
  use Ash.Api,
    extensions: [AshAdmin.Api]

  resources do
    resource MyApp.Accounts.User
    resource MyApp.Accounts.Token
    resource MyApp.Accounts.Organization
  end

  admin do
    show? true
  end
end
defmodule MyApp.Accounts.Organization do
  use Ash.Resource,
    data_layer: AshPostgres.DataLayer,
    extensions: [AshAdmin.Resource]

  attributes do
    uuid_primary_key :id

    attribute :subdomain, :ci_string, allow_nil?: false

  end

  actions do
    defaults [:create, :read, :update, :destroy]
  end

  postgres do
    table "organizations"
    repo MyApp.Repo

    manage_tenant do
      template ["org_", :id]
    end
  end
end
defmodule MyApp.Accounts.User do
  use Ash.Resource,
    data_layer: AshPostgres.DataLayer,
    extensions: [AshAuthentication, AshAdmin.Resource],
    authorizers: [Ash.Policy.Authorizer]

  attributes do
    uuid_primary_key :id

    attribute :email, :ci_string, allow_nil?: false
    attribute :hashed_password, :string, allow_nil?: false, sensitive?: true, private?: true
  end

  authentication do
    api MyApp.Accounts

    strategies do
      password :password do
        identity_field :email
      end
    end

    tokens do
      enabled? true
      token_resource MyApp.Accounts.Token

      signing_secret fn _, _ ->
        Application.fetch_env(:my_app, :token_signing_secret)
      end
    end
  end

  actions do
    defaults [:read, :update, :destroy]
  end

  identities do
    identity :unique_email, [:email]
  end

  # A safe default that only allows user data to be interacted with via AshAuthentication.
  policies do
    bypass AshAuthentication.Checks.AshAuthenticationInteraction do
      authorize_if always()
    end

    policy always() do
      forbid_if always()
    end
  end

  multitenancy do
    strategy :context
  end

  postgres do
    table "users"
    repo MyApp.Repo
  end

  admin do
    actor? true
  end
end

Set tenant and confirm that this is working fine in other respects than setting the user.

screenshot-set-user-tenant-id-missing

Then try to set the user with the key button.

screenshot-set-user-tenant-id-missing2

That crashes the LiveView process with the following error.

[debug] HANDLE EVENT "set_actor" in AshAdmin.PageLive
  Parameters: %{"api" => "Elixir.MyApp.Accounts", "pkey" => "7795ffa9-4d0d-463d-b095-98651bff887f", "resource" => "Elixir.MyApp.Accounts.User", "value" => ""}
[error] GenServer #PID<0.856.0> terminating
** (Ash.Error.Invalid) Input Invalid

* Queries against the MyApp.Accounts.User resource require a tenant to be specified
  (elixir 1.16.1) lib/process.ex:860: Process.info/2
  (ash 2.21.8) lib/ash/error/exception.ex:59: Ash.Error.Invalid.TenantRequired.exception/1
  (ash 2.21.8) lib/ash/actions/read/read.ex:1163: Ash.Actions.Read.validate_multitenancy/1
  (ash 2.21.8) lib/ash/actions/read/read.ex:304: anonymous fn/5 in Ash.Actions.Read.do_read/4
  (ash 2.21.8) lib/ash/engine/engine.ex:514: anonymous fn/4 in Ash.Engine.async/2
  (elixir 1.16.1) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2
  (elixir 1.16.1) lib/task/supervised.ex:36: Task.Supervised.reply/4
    (elixir 1.16.1) lib/process.ex:860: Process.info/2
    (ash 2.21.8) lib/ash/error/exception.ex:59: Ash.Error.Invalid.exception/1
    (ash 2.21.8) lib/ash/error/error.ex:602: Ash.Error.choose_error/2
    (ash 2.21.8) lib/ash/error/error.ex:260: Ash.Error.to_error_class/2
    (ash 2.21.8) lib/ash/actions/read/read.ex:292: Ash.Actions.Read.do_run/3
    (ash 2.21.8) lib/ash/actions/read/read.ex:50: anonymous fn/3 in Ash.Actions.Read.run/3
    (ash 2.21.8) lib/ash/actions/read/read.ex:49: Ash.Actions.Read.run/3
    (ash 2.21.8) lib/ash/api/api.ex:2612: Ash.Api.read_one/3
    (ash 2.21.8) lib/ash/api/api.ex:2599: Ash.Api.read_one!/3
    (ash_admin 0.10.9) lib/ash_admin/pages/page_live.ex:381: AshAdmin.PageLive.handle_event/3
    (phoenix_live_view 0.20.14) lib/phoenix_live_view/channel.ex:507: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
    (telemetry 1.2.1) /Users/carlgleisner/Developer/my_app/deps/telemetry/src/telemetry.erl:321: :telemetry.span/3
    (phoenix_live_view 0.20.14) lib/phoenix_live_view/channel.ex:260: Phoenix.LiveView.Channel.handle_info/2
    (stdlib 5.1.1) gen_server.erl:1077: :gen_server.try_handle_info/3
    (stdlib 5.1.1) gen_server.erl:1165: :gen_server.handle_msg/6
    (stdlib 5.1.1) proc_lib.erl:241: :proc_lib.init_p_do_apply/3
Last message: %Phoenix.Socket.Message{topic: "lv:phx-F8M_oBHxitqXUyTB", event: "event", payload: %{"event" => "set_actor", "type" => "click", "value" => %{"api" => "Elixir.MyApp.Accounts", "pkey" => "7795ffa9-4d0d-463d-b095-98651bff887f", "resource" => "Elixir.MyApp.Accounts.User", "value" => ""}}, ref: "202", join_ref: "201"}
State: %{socket: #Phoenix.LiveView.Socket<id: "phx-F8M_oBHxitqXUyTB", endpoint: MyAppWeb.Endpoint, view: AshAdmin.PageLive, parent_pid: nil, root_pid: #PID<0.856.0>, router: MyAppWeb.Router, assigns: %{id: nil, table: nil, record: nil, prefix: "/admin", params: %{"action" => "read", "action_type" => "read", "api" => "Accounts", "resource" => "User", "route" => [], "table" => ""}, resource: MyApp.Accounts.User, action: %Ash.Resource.Actions.Read{arguments: [], description: nil, filter: nil, get_by: [], get?: false, manual: nil, metadata: [], modify_query: nil, name: :read, pagination: false, preparations: [], primary?: true, touches_resources: [], timeout: nil, transaction?: false, type: :read}, __changed__: %{}, flash: %{}, tables: [], api: MyApp.Accounts, primary_key: nil, tenant: "org_cdbd5349-26ac-4a6e-b9e2-956092bc780e", actor: nil, live_action: :page, apis: [MyApp.Accounts], action_type: :read, polymorphic_actions: nil, authorizing: false, actor_paused: true, editing_tenant: false, actor_api: nil, url_path: "/admin", tab: nil, actor_resources: [{MyApp.Accounts, MyApp.Accounts.User}]}, transport_pid: #PID<0.847.0>, ...>, components: {%{1 => {AshAdmin.Components.TopNav, "top_nav", %{id: "top_nav", open: false, prefix: "/admin", resource: MyApp.Accounts.User, __changed__: %{}, flash: %{}, api: MyApp.Accounts, tenant: "org_cdbd5349-26ac-4a6e-b9e2-956092bc780e", actor: nil, set_tenant: "set_tenant", myself: %Phoenix.LiveComponent.CID{cid: 1}, apis: [MyApp.Accounts], authorizing: false, actor_paused: true, editing_tenant: false, actor_api: nil, actor_resources: [{MyApp.Accounts, MyApp.Accounts.User}], clear_actor: "clear_actor", clear_tenant: "clear_tenant", toggle_actor_paused: "toggle_actor_paused", toggle_authorizing: "toggle_authorizing", nav_collapsed: false}, %{lifecycle: %Phoenix.LiveView.Lifecycle{after_render: [], handle_async: [], handle_event: [], handle_info: [], handle_params: [], mount: []}, live_temp: %{}, root_view: AshAdmin.PageLive, children_cids: [6, 5]}, {148060397520760006710221863373862312843, %{1 => {131016450657116420037473116728389064313, %{3 => {193117531274676238663499316904078091610, %{}}}}, 2 => 323659859598785054574994542540331106742, 3 => {288055939197927762304529307924563788880, %{0 => {51478131608043983236170183579808818613, %{2 => {145606693040105998035254632725030243907, %{}}, 6 => {138038591087602664711848583458530382947, %{0 => {10907819869706596814376658420644338192, %{0 => {131016450657116420037473116728389064313, %{3 => {178003258052215265257989775076931059304, %{}}}}}}}}}}}}, 4 => {52452721994246833135358052505681706306, %{0 => {180493659858002504575080165245409507118, %{1 => {177309640539645176365905376703197149830, %{}}, 2 => {141661402952870611029419311691675797224, %{}}}}}}}}}, 2 => {AshAdmin.Components.Resource, MyApp.Accounts.User, %{id: MyApp.Accounts.User, table: nil, record: nil, prefix: "/admin", params: %{"action" => "read", "action_type" => "read", "api" => "Accounts", "resource" => "User", "route" => [], "table" => ""}, resource: MyApp.Accounts.User, action: %Ash.Resource.Actions.Read{arguments: [], description: nil, filter: nil, get_by: [], get?: false, manual: nil, metadata: [], modify_query: nil, name: :read, pagination: false, preparations: [], primary?: true, touches_resources: [], timeout: nil, transaction?: false, type: :read}, __changed__: %{}, flash: %{}, tables: [], api: MyApp.Accounts, primary_key: nil, tenant: "org_cdbd5349-26ac-4a6e-b9e2-956092bc780e", actor: nil, set_actor: "set_actor", myself: %Phoenix.LiveComponent.CID{cid: 2}, action_type: :read, polymorphic_actions: nil, authorizing: false, url_path: "/admin", tab: nil, filter_open: false}, %{lifecycle: %Phoenix.LiveView.Lifecycle{after_render: [], handle_async: [], handle_event: [], handle_info: [], handle_params: [], mount: []}, live_temp: %{}, root_view: AshAdmin.PageLive, children_cids: [4, 3]}, {116549564983629548503950293970224204311, %{0 => {198509498940223282628687766034426527391, %{0 => {131016450657116420037473116728389064313, %{3 => {216715932998474834650148707730297806747, %{}}}}, 1 => {283810406251810921612151390347975945762, %{0 => {131016450657116420037473116728389064313, %{3 => {232574604184611946196318217149198693703, %{}}}}}}}}, 6 => {194783921231291960685782404229296793506, %{}}}}}, 3 => {AshAdmin.Components.TopNav.Dropdown, "Elixir.MyApp.Accounts.User_data_dropdown", %{active: false, id: "Elixir.MyApp.Accounts.User_data_dropdown", name: "Read", open: false, __changed__: %{}, class: "", flash: %{}, myself: %Phoenix.LiveComponent.CID{cid: 3}, group_labels: [], groups: [[%{active: false, text: "Sign In With Password", to: "/admin?api=Accounts&resource=User&table=&action_type=read&action=sign_in_with_password"}, %{active: false, text: "Get By Subject", to: "/admin?api=Accounts&resource=User&table=&action_type=read&action=get_by_subject"}, %{active: true, text: "Read", to: "/admin?api=Accounts&resource=User&table=&action_type=read&action=read"}]]}, %{lifecycle: %Phoenix.LiveView.Lifecycle{after_render: [], handle_async: [], handle_event: [], handle_info: [], handle_params: [], mount: []}, live_temp: %{}, root_view: AshAdmin.PageLive, children_cids: []}, {132048723562976016917943051661893721681, %{}}}, 4 => {AshAdmin.Components.Resource.DataTable, "Elixir.MyApp.Accounts.User_table", %{data: {:ok, [#MyApp.Accounts.User<__meta__: #Ecto.Schema.Metadata<:loaded, "org_cdbd5349-26ac-4a6e-b9e2-956092bc780e", "users">, id: "7795ffa9-4d0d-463d-b095-98651bff887f", email: #Ash.CiString<"firstname@domain.tld">, first_name: "First name", last_name: "Last name", phone: "555-123 456 79", is_a (truncated)

Expected behavior

The current user, fetched from the users table of the set tenant, to be set.

Runtime

Additional context

Comment from @zachdaniel on Discord:

"there are some interesting implications there, we'll need to track the actor_tenant in the session to make sure we can look up the user properly"

zachdaniel commented 8 months ago

This is fixed in main of ash_admin and back ported to 2.0

zachdaniel commented 8 months ago

There is a difficulty here with the way we've set up our releases for this package, releasing a pre-3.0 version is kind of hard. Are you planning on upgrading to 3.0 any time soon? Would make our lives easier if we didn't have to figure it out right now 😆

carlgleisner commented 8 months ago

[…] Are you planning on upgrading to 3.0 any time soon? Would make our lives easier if we didn't have to figure it out right now 😆

I'm a happy camper here, no need for a pre-3.0 on my behalf! 🤗 I trust that you are sufficiently occupied as it is 🫠