Where is the best place to put the permissions checking logic in LiveView?

Hi,
What is the best place to put the permissions checking logic in LiveView?
Is there a central place where I can put the permission checking logic similar to Plug for controller actions?

1 Like

You can maybe use hooks, but given LV is long running / stateful this is going to be more complex and you’ll need to do permission checks in more places.

https://hexdocs.pm/phoenix_live_view/0.18.16/Phoenix.LiveView.html#attach_hook/4

https://hexdocs.pm/phoenix_live_view/0.18.16/security-model.html

2 Likes

I’ve put two hooks to catch all handle_params and handle_event activities, those hooks are fired when clicking a button from within a live_view but not live_component ( form_component.ex for example )!, how can intercept the activity on live_component?

Generally speaking, LiveComponent events go to its parent LiveView by default. The reason events from the generated form components do not is because they explicitly set phx-target={@myself}.

One approach would be removing the phx-target={@myself} to send the event directly to the parent LiveView and handle events there.

Another approach would be forwarding/relaying the event from the child LiveComponent to its parent LiveView.

# child LiveComponent
defmodule CardComponent do
  ...
  def handle_event("update_title", %{"title" => title}, socket) do
    send self(), {:updated_card, %{socket.assigns.card | title: title}}
    {:noreply, socket}
  end
end
# parent LiveView
defmodule BoardView do
  ...
  def handle_info({:updated_card, card}, socket) do
    # update the list of cards in the socket
    {:noreply, updated_socket}
  end
end

Regardless, I recommend reading through the Managing State section of the LiveComponent module docs.

It may also be possible to attach_hook directly onto the handle_event callback of the LiveComponent and do the permissions checking logic in the LiveComponent itself.

1 Like

Could you tell me exactly in which place to put attach_hook in the LiveComponent? I am not able to figure it out!

I haven’t tried attaching a hook to a LiveComponent before, but LiveComponents do have a mount lifecycle callback so maybe try putting it there. See these docs for an example of attaching a hook to a LiveView’s mount.

I did not know that LiveComponent has mount lifecycle callback! I will try it, thank you.

I can give you a heads up here: you can’t attach hooks in LiveComponents. That approach won’t work unfortunately

1 Like

I would recommend putting the authorization logic outside of LiveView. For example you could create module(s) that implement your “business logic”. LiveView can call these modules when an action needs to be performed and the modules can take care of checking whether the user is allowed to perform the action, maybe relying on an authorization library.

I am already using bodyguard library, but was searching for a central place in which I can put the permission checking logic to handle all use cases in one place.

Also remember to recheck permissions when user action happens. If user’s permissions can change middle of session then you should handle that gracefully. Security considerations of the LiveView model — Phoenix LiveView v0.18.17

1 Like

@trisolaran you are right,
I can’t attach hooks in LiveComponent.
Phoenix will throw an exception

** (ArgumentError) lifecycle hooks are not supported on stateful components.

Unfortunately you can not do it, Phoenix will complain

** (ArgumentError) lifecycle hooks are not supported on stateful components.

You won’t find this central place in liveview. But you can write for example a module with functions that perform your actions and take as parameter the user. These functions can then query bodyguard to do the auth check, and can be called from anywhere, LV or not. All the caller has to supply is the user.

I am thinking of putting the checking logic in the Context or in LiveView handle_event callback. which approach can you advice?

Ahh, that is unfortunate… Funnily enough, your profile picture just reminded where I likely got the idea from in the first place.

So I’m guessing you’re speaking from hard earned experience!

Agreed, this is how it was addressed in the security considerations docs @wanton7 linked above.

Project.delete!(socket.assigns.current_user, project_id)

It could be nice to use function arity to support actions with or without a user parameter for permission checking since traditional controllers can be protected by plugs. Maybe something like this assuming there’s a Blog Context:

defmodule MyApp.Blog do
  def update_post(%User{} = user, post, post_params) do
    with :ok <- Bodyguard.permit(MyApp.Blog, :update_post, user, post),
      do: MyApp.Blog.update_post(post, post_params)
  end

  def update_post(%Post{} = post, attrs) do
    post
    |> Post.changeset(attrs)
    |> Repo.update()
  end
end

One consideration when relying solely on the LiveView handle_event callback would be that all operations would need to go through the LiveView. That would mean the LiveView can potentially get very cluttered with event callbacks. Take the out of the box form components generated by mix phx.gen.live for example, you would need to move all the calls to Context functions in the event callbacks to the parent LiveView.

1 Like

Yea man, as always! Was really stunned when I realized that it wasn’t possible

1 Like

By the way, sorry for misleading you with wrong informations @codeanpeace. I tried to update that old comment but I can’t :person_shrugging:

1 Like

While discussing LiveView in another topic, this design pattern came to mind that consolidates the authorization checking logic into a catchall handle_event clause for LiveComponents where attach_hook is unavailable. Unlike the example in the docs that add a user param to the

defmodule MyAppWeb.ProjectLiveComponent do
  ...
  # specify events that do not require authorization like normal
  def handle_event("open to all", message, socket), do: ...

  # catchall `handle_event` function for events that require authorization
  def handle_event(event, project, socket) do

    # user defined authorizaton check in context
    if Project.is_authorized?(project, event, socket.assigns.current_user) do
      handle_authorized_event(event, message, socket)
      # or alternatively `handle_event/4`
      handle_event(:authorized, event, message, socket)
      # but don't overload `handle_event/3` e.g. `handle_event({:authorized, event}, message, socket)`
      # as that will still leave the LiveView open to malicious client events/requests
    end

    # or alternatively library defined authorization check e.g. Bodyguard
    with :ok <- Bodyguard.permit(MyApp.Project, event, user, project) do
      handle_authorized_event(event, message, socket)
    end
  end

  # specify events that do require authorization like this
  def handle_authorized_event("delete_project", %{"project_id" => project_id}, socket) do
    # only reached after passing authorization check in the catchall `handle_event/3` clause
    Project.delete!(project_id) # now there's no need to pass the current user as an argument
    {:noreply, update(socket, :projects, &Enum.reject(&1, fn p -> p.id == project_id end)}
  end

  # or like this when using `handle_event/4` rather than `handle_authorized_event/3`
  def handle_event(:authorized, "delete_project", %{"project_id" => project_id}, socket) do
    # only reached after passing authorization check in the catchall `handle_event/3` clause
    Project.delete!(project_id) # now there's no need to pass the current user as an argument
    {:noreply, update(socket, :projects, &Enum.reject(&1, fn p -> p.id == project_id end)}
  end
  ...
end
1 Like

Maybe I’m paranoid, but a standard I’ve stablished in my projects is to pass User as the first argument to every function in my contexts. Then when the user has no permission, respond with {:error, :unauthorized}.

Most of those context cases will never reach in practice, given how to UI is built, but it’s nice to know that the data layer knows nothing about web/api, and it’s a relentless bouncer.

It also makes sense from a system perspective: the data layer doesn’t respond to Give me all books, but it does to User A wants all books.

5 Likes