I have an app where users have different permissions. Those permissions are used to determine whether a user can see a page. Each route has one or more permissions associated with it, for dead views it’s easy enough to define a plug that checks for the authorisation and use the private conn fields to pass along the permission info, eg:
# router.ex
scope "/", MyWeb do
pipe_through([:browser, :authorize])
...
get("/some-page", SomePage :index, private: %{required_permissions: [:admin]})
...
end
# user_auth.ex
def authorize(conn, _opts) do
required_permissions = Map.get(conn.private, :required_permissions)
user = conn.assigns[:current_user]
if user && user.confirmed_at && required_permissions do
if Users.has_all_permissions?(user, required_permissions) do
conn
else
conn
|> Phoenix.Controller.put_flash(:error, "Unauthorized.")
|> Phoenix.Controller.redirect(to: ~p"/")
|> Plug.Conn.halt()
end
else
...
end
end
For liveview though we want to check the permissions on mount. Mount does not get conn it gets params
, session
and socket
, so even though you can do this:
# router.ex
live("the-page", Pages.ThePage, :new, private: %{required_permissions: [:admin]})
The required_permissions
are not available to you. You can instead do this:
live_session :admin_user,
session: %{"required_permissions" => [:admin]},
on_mount: [{UserAuth, :ensure_authenticated}] do
live("/the-page", ThePage, :new)
end
and you will see required_permissions
in session. But this means you have to make a new live_session for each unqiue set of permissions. Eg if I have one route that requires :view_users
and one route that requires :admin
they now can’t be in the same live_session, which means you miss out on the benefit of live_redirects.
What are some strategies you’ve tried to avoid this?
The only other thing I’ve tried is inverting the control by specifying the route in a live_view and using that in an on_mount
def live_view(route) do
quote do
...
unquote(html_helpers())
on_mount({AuthorizeRoute, unquote(route)})
end
end
defmacro __using__(:live_view) do
raise_message()
end
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
defp raise_message() do
raise """
When defining a live view you should provide the type and route options like so:
use MyAppWeb, type: :live_view, route: :the_route
The route should have a matching function head in AuthorizeRoute.on_mount/4
so that the live_view can be authorized.
"""
end
# in a live view:
defmodule MyAppWeb.Pages.ThePage do
use MyAppWeb, type: :live_view, route: :the_page
...
end
# authorixe_route.ex
defmodule AuthorizeRoute do
@permissions %{
the_page [:admin],
some_other_route: [:view_users]
}
def on_mount(route, params, session, socket) do
if permissions = Map.get(@permissions, route, nil) do
user = socket.assigns.current_user
if Users.has_all_permissions?(user, permission) do
...
else
....
end
else
# Could flash
{:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
end
end
end
But that feels kinda nasty.