AE
Ash Elixir•2mo ago
Rutgerdj

How to get tenant in LiveView after setting it using `PlugHelpers.set_tenant`

I'm currently setting my tenant in my controller like this:
conn
|> put_session(:tenant, tenant_id)
conn
|> put_session(:tenant, tenant_id)
And this is how I put it in my LiveView socket assigns:
socket
|> Phoenix.Component.assign(current_tenant: session["tenant"])
socket
|> Phoenix.Component.assign(current_tenant: session["tenant"])
I am however getting warnings in my logs saying I should use set_tenant:
[warning] Storing the tenant in conn assigns is deprecated.
deps/ash/lib/ash/plug_helpers.ex:169: Ash.PlugHelpers.get_tenant/1
[warning] Storing the tenant in conn assigns is deprecated.
deps/ash/lib/ash/plug_helpers.ex:169: Ash.PlugHelpers.get_tenant/1
So I updated how I set the tenant:
conn
|> Ash.PlugHelpers.set_tenant(tenant_id)
conn
|> Ash.PlugHelpers.set_tenant(tenant_id)
This then sets the tenant in the private fields as per the docs. But how do i get that tenant_id in my liveview? Its not stored in the session and I dont have access to the connection.
Solution:
ok I think we are going in the right direction, so the authcontroller code is called when you sign in, but then it redirects and there you get a new conn and the browser pipeline only loads the user from the session. You need another plug/controller that you put after load_from_session, where you put this code ```elixir tenant = if user.is_super_admin? do...
Jump to solution
10 Replies
barnabasj
barnabasj•2mo ago
are you using ash_authentication_phoenix as well? it has this macro
@doc """
Generate a live session wherein all subject assigns are copied from the conn
into the socket.

Options:
* `:otp_app` - Set the otp app in which to search for authenticated resources.
* `:on_mount_prepend` - Same as `:on_mount`, but for hooks that need to be
run before AshAuthenticationPhoenix's hooks.

All other options are passed through to `live_session`, but with session and on_mount hooks
added to set assigns for authenticated resources. Unlike `live_session`, this supports
multiple MFAs provided for the `session` option. The produced sessions will be merged.
"""
@spec ash_authentication_live_session(atom, opts :: Keyword.t()) :: Macro.t()
defmacro ash_authentication_live_session(session_name \\ :ash_authentication, opts \\ [],
@doc """
Generate a live session wherein all subject assigns are copied from the conn
into the socket.

Options:
* `:otp_app` - Set the otp app in which to search for authenticated resources.
* `:on_mount_prepend` - Same as `:on_mount`, but for hooks that need to be
run before AshAuthenticationPhoenix's hooks.

All other options are passed through to `live_session`, but with session and on_mount hooks
added to set assigns for authenticated resources. Unlike `live_session`, this supports
multiple MFAs provided for the `session` option. The produced sessions will be merged.
"""
@spec ash_authentication_live_session(atom, opts :: Keyword.t()) :: Macro.t()
defmacro ash_authentication_live_session(session_name \\ :ash_authentication, opts \\ [],
it sets up an mfa for the session option of live_session
@doc """
Supplements the session with any `current_X` assigns which are authenticated
resource records from the conn.
"""
@spec generate_session(Plug.Conn.t(), atom | [atom], additional_hooks :: [mfa]) :: %{
required(String.t()) => String.t()
}
def generate_session(conn, otp_app \\ nil, additional_hooks \\ []) do
otp_app = otp_app || conn.assigns[:otp_app] || conn.private.phoenix_endpoint.config(:otp_app)

acc =
Enum.reduce(additional_hooks, %{}, fn {m, f, a}, acc ->
Map.merge(acc, apply(m, f, [conn | a]) || %{})
end)

otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(&{to_string(Info.authentication_subject_name!(&1)), &1})
|> Enum.reduce(acc, fn {subject_name, resource}, session ->
case Map.fetch(
conn.assigns,
String.to_existing_atom("current_#{subject_name}")
) do
{:ok, user} when is_struct(user, resource) ->
session
|> Map.put(subject_name, AshAuthentication.user_to_subject(user))
|> Map.put("tenant", Ash.PlugHelpers.get_tenant(conn))
|> Map.put("context", Ash.PlugHelpers.get_context(conn))

_ ->
session
|> Map.put("tenant", Ash.PlugHelpers.get_tenant(conn))
|> Map.put("context", Ash.PlugHelpers.get_context(conn))
end
end)
end
@doc """
Supplements the session with any `current_X` assigns which are authenticated
resource records from the conn.
"""
@spec generate_session(Plug.Conn.t(), atom | [atom], additional_hooks :: [mfa]) :: %{
required(String.t()) => String.t()
}
def generate_session(conn, otp_app \\ nil, additional_hooks \\ []) do
otp_app = otp_app || conn.assigns[:otp_app] || conn.private.phoenix_endpoint.config(:otp_app)

acc =
Enum.reduce(additional_hooks, %{}, fn {m, f, a}, acc ->
Map.merge(acc, apply(m, f, [conn | a]) || %{})
end)

otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(&{to_string(Info.authentication_subject_name!(&1)), &1})
|> Enum.reduce(acc, fn {subject_name, resource}, session ->
case Map.fetch(
conn.assigns,
String.to_existing_atom("current_#{subject_name}")
) do
{:ok, user} when is_struct(user, resource) ->
session
|> Map.put(subject_name, AshAuthentication.user_to_subject(user))
|> Map.put("tenant", Ash.PlugHelpers.get_tenant(conn))
|> Map.put("context", Ash.PlugHelpers.get_context(conn))

_ ->
session
|> Map.put("tenant", Ash.PlugHelpers.get_tenant(conn))
|> Map.put("context", Ash.PlugHelpers.get_context(conn))
end
end)
end
and an on mount hook that gets the user from the session and sets in on the socket. so this part
socket
|> Phoenix.Component.assign(current_tenant: session["tenant"])
socket
|> Phoenix.Component.assign(current_tenant: session["tenant"])
should work if the rest is setup correctly
Rutgerdj
RutgerdjOP•2mo ago
Thanks for the insights! But I'm still stuck on what the issue could be I use ash_authentication_phoenix and in my router I do use the ash_authentication_live_session:
live "/password-reset/:token", Live.Auth.Reset
live "/reset", Live.Auth.Request
live "/sign-in", Live.Auth.Index

sign_out_route AuthController
auth_routes_for Cvs.Account.User, to: AuthController

ash_authentication_live_session :dashboard,
on_mount: [
{Cvs.Live.UserAuth, :user_required},
{Cvs.Util.ClearEts, {:wipe_other_fields, :dashboard}}
] do
live "/", Live.Dashboard.Index
end
live "/password-reset/:token", Live.Auth.Reset
live "/reset", Live.Auth.Request
live "/sign-in", Live.Auth.Index

sign_out_route AuthController
auth_routes_for Cvs.Account.User, to: AuthController

ash_authentication_live_session :dashboard,
on_mount: [
{Cvs.Live.UserAuth, :user_required},
{Cvs.Util.ClearEts, {:wipe_other_fields, :dashboard}}
] do
live "/", Live.Dashboard.Index
end
Anything that could be wrong here? The authcontroller is what is calling the set_tenant
barnabasj
barnabasj•2mo ago
🤔 can you double check that your authcontroller is in the pipeline before it hits the live session?
Rutgerdj
RutgerdjOP•2mo ago
Yes, I've added some logging to the auth controller and the live_sessions generate_session:
[(cvs 0.1.0) lib/cvs_web/controllers/auth_controller.ex:21: CvsWeb.AuthController.success/4]
"setting tenant" #=> "setting tenant"

[(cvs 0.1.0) lib/cvs_web/controllers/auth_controller.ex:31: CvsWeb.AuthController.success/4]
conn #=> %Plug.Conn{
...
}
|> Ash.PlugHelpers.get_tenant() #=> "572bff9b-623c-49f0-83aa-5cdfdb10ffee"

[(ash_authentication_phoenix 2.10.1) lib/ash_authentication_phoenix/live_session.ex:225: AshAuthentication.Phoenix.LiveSession.generate_session/3]
session #=> %{}
|> Map.put(subject_name, AshAuthentication.user_to_subject(user)) #=> %{"user" => "user?id=69377c88-a3c7-4cdc-84c2-24d1d8bf4d38"}
|> Map.put("tenant", Ash.PlugHelpers.get_tenant(conn)) #=> %{"tenant" => nil, "user" => "user?id=69377c88-a3c7-4cdc-84c2-24d1d8bf4d38"}
|> Map.put("context", Ash.PlugHelpers.get_context(conn)) #=> %{
"context" => nil,
"tenant" => nil,
"user" => "user?id=69377c88-a3c7-4cdc-84c2-24d1d8bf4d38"
}
[(cvs 0.1.0) lib/cvs_web/controllers/auth_controller.ex:21: CvsWeb.AuthController.success/4]
"setting tenant" #=> "setting tenant"

[(cvs 0.1.0) lib/cvs_web/controllers/auth_controller.ex:31: CvsWeb.AuthController.success/4]
conn #=> %Plug.Conn{
...
}
|> Ash.PlugHelpers.get_tenant() #=> "572bff9b-623c-49f0-83aa-5cdfdb10ffee"

[(ash_authentication_phoenix 2.10.1) lib/ash_authentication_phoenix/live_session.ex:225: AshAuthentication.Phoenix.LiveSession.generate_session/3]
session #=> %{}
|> Map.put(subject_name, AshAuthentication.user_to_subject(user)) #=> %{"user" => "user?id=69377c88-a3c7-4cdc-84c2-24d1d8bf4d38"}
|> Map.put("tenant", Ash.PlugHelpers.get_tenant(conn)) #=> %{"tenant" => nil, "user" => "user?id=69377c88-a3c7-4cdc-84c2-24d1d8bf4d38"}
|> Map.put("context", Ash.PlugHelpers.get_context(conn)) #=> %{
"context" => nil,
"tenant" => nil,
"user" => "user?id=69377c88-a3c7-4cdc-84c2-24d1d8bf4d38"
}
As you can see the auth_controller logs that the tenant is being set, but then in the generate_session the get_tenant returns nil
barnabasj
barnabasj•2mo ago
is there anything between those in the pipeline that could muck with the conn? or are you maybe missing an assignment or something so you are returning the unmodified conn?
Rutgerdj
RutgerdjOP•2mo ago
As far as I can tell Im returning the conn with the tenant set. Here is the full AuthController:
defmodule CvsWeb.AuthController do
use CvsWeb, :controller
use AshAuthentication.Phoenix.Controller

def success(conn, _activity, user, _token) do
# we do not know the tenant yet
user =
Cvs.Account.User.get_by_email!(user.email, authorize?: false)

tenant =
if user.is_super_admin? do
[tenant | _] = Cvs.Tenant.Tenant.read!(actor: user)
tenant.id
else
user.primary_registration.tenant_id
end

default_page = Cvs.Live.UserAuth.get_default_page(user, tenant)
return_to = get_session(conn, :return_to) || default_page

"setting tenant" |> dbg

conn =
conn
|> delete_session(:return_to)
|> store_in_session(user)
|> put_session(:login_time, System.system_time(:second))
|> Ash.PlugHelpers.set_tenant(tenant)
|> assign(:current_user, user)
|> redirect(to: return_to)

conn |> Ash.PlugHelpers.get_tenant() |> dbg

conn
end

def failure(conn, _activity, _reason) do
conn
|> put_status(401)
|> put_layout(html: {CvsWeb.Layouts, :root})
|> render("failure.html")
end

def sign_out(conn, _params) do
return_to = get_session(conn, :return_to) || ~p"/"

conn
|> clear_session(:cvs)
|> put_layout(html: {CvsWeb.Layouts, :root})
|> redirect(to: return_to)
end
end
defmodule CvsWeb.AuthController do
use CvsWeb, :controller
use AshAuthentication.Phoenix.Controller

def success(conn, _activity, user, _token) do
# we do not know the tenant yet
user =
Cvs.Account.User.get_by_email!(user.email, authorize?: false)

tenant =
if user.is_super_admin? do
[tenant | _] = Cvs.Tenant.Tenant.read!(actor: user)
tenant.id
else
user.primary_registration.tenant_id
end

default_page = Cvs.Live.UserAuth.get_default_page(user, tenant)
return_to = get_session(conn, :return_to) || default_page

"setting tenant" |> dbg

conn =
conn
|> delete_session(:return_to)
|> store_in_session(user)
|> put_session(:login_time, System.system_time(:second))
|> Ash.PlugHelpers.set_tenant(tenant)
|> assign(:current_user, user)
|> redirect(to: return_to)

conn |> Ash.PlugHelpers.get_tenant() |> dbg

conn
end

def failure(conn, _activity, _reason) do
conn
|> put_status(401)
|> put_layout(html: {CvsWeb.Layouts, :root})
|> render("failure.html")
end

def sign_out(conn, _params) do
return_to = get_session(conn, :return_to) || ~p"/"

conn
|> clear_session(:cvs)
|> put_layout(html: {CvsWeb.Layouts, :root})
|> redirect(to: return_to)
end
end
And this is the browser pipeline:
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_live_flash)
plug(:put_root_layout, html: {CvsWeb.Layouts, :root})
plug(:protect_from_forgery)

plug(:put_secure_browser_headers, %{
"content-security-policy" =>
"default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'unsafe-inline' 'unsafe-eval'; font-src 'self' https://fonts.gstatic.com"
})

plug(:load_from_session)
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_live_flash)
plug(:put_root_layout, html: {CvsWeb.Layouts, :root})
plug(:protect_from_forgery)

plug(:put_secure_browser_headers, %{
"content-security-policy" =>
"default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'unsafe-inline' 'unsafe-eval'; font-src 'self' https://fonts.gstatic.com"
})

plug(:load_from_session)
Solution
barnabasj
barnabasj•2mo ago
ok I think we are going in the right direction, so the authcontroller code is called when you sign in, but then it redirects and there you get a new conn and the browser pipeline only loads the user from the session. You need another plug/controller that you put after load_from_session, where you put this code
tenant =
if user.is_super_admin? do
[tenant | _] = Cvs.Tenant.Tenant.read!(actor: user)
tenant.id
else
user.primary_registration.tenant_id
end

conn
|> Ash.PlugHelpers.set_tenant(tenant)
tenant =
if user.is_super_admin? do
[tenant | _] = Cvs.Tenant.Tenant.read!(actor: user)
tenant.id
else
user.primary_registration.tenant_id
end

conn
|> Ash.PlugHelpers.set_tenant(tenant)
Rutgerdj
RutgerdjOP•2mo ago
Ah i see, but then that would still require me to set the tenant in the session (so that another plug can get it from the session and set it using set_tenant right? I need to somehow communicate to the plug which tenant to set (for example when switching tenants)
barnabasj
barnabasj•2mo ago
yeah, you could also just store the tenant in the session in your auth controller and get it from there in a plug instead of repeating that logic. Another approach I have seen is using a subdomain per tenant and get the tenant that way
Rutgerdj
RutgerdjOP•2mo ago
I understand. I will figure it out from here. Thanks again for the help!! Really appreciate it! 🧡

Did you find this page helpful?