View Source LiveGuard

A simple package to protect the LiveView lifecycle stages such as :mount, :handle_params, :handle_event, :handle_info and :handle_async.

Installation

For the latest master:

def deps do
  [
    {:live_guard, github: "FabianDaniel00/live_guard"}
  ]
end

For the latest release:

def deps do
  [
    {:live_guard, "~> 0.1.8"}
  ]
end

Then run mix deps.get to fetch the dependencies.

Config

  • :current_user

    You need to assign the current user to the socket before LiveGuard on_mount/4 callback is called.. The default assign name for the current user is :current_user. If you assign the current user as another than :current_user you can set in the config:

    config :live_guard, :current_user, :user
  • :unauthorized_handler

    This function handles unauthorized LiveView lifecycle stages. It's called when the allowed?/4 function returns false.

    By default it will put an error flash message with text "You don't have permission to do that!".

    :mount and :handle_params LiveView lifecycle stages needs redirect after it detected as unauthorized. In this case by default it will redirect to the home page (/).

    You can set a custom handler in the config:

    config :live_guard, :unauthorized_handler, {MyModule, :my_handle_unauthorized}

    It's called with 2 inputs, first is a socket, second is is_redirect (boolean).

Usage

LiveGuard provide an on_mount/4 callback which can be used in Phoenix LiveViews. Read the docs.

Since this is an on_mount/1 callback you can use it three ways:

  • with live_session/3 to protect a couple of LiveViews lifecycle stages at once:
    # lib/my_app_web/router.ex
    
    defmodule MyAppWeb.Router do
      use MyAppWeb, :router
    
      live_session :default, on_mount: LiveGuard do
        # routes...
      end
    end
  • if you want to protect LiveView lifecycle stages in every LiveViews, you can achive that with the following code:
    # lib/my_app_web.ex
    
    defmodule MyAppWeb do
      # some code...
    
      def live_view do
        quote do
          use Phoenix.LiveView, layout: {MyAppWeb.Layouts, :app}
    
          on_mount LiveGuard
    
          unquote(html_helpers())
        end
      end
    
      # some code...
    end
  • and if you want to protect an individual LiveView lifecycle stages you can achive that with the following code:
    # lib/my_app_web/live/my_module_live.ex
    
    defmodule MyAppWeb.MyModuleLive do
      use MyAppWeb, :live_view
    
      on_mount LiveGuard
    
      def mount(_params, _session, socket) do
        {:ok, socket}
      end
    
      # some code...
    end

Implementation

For now you should ask, okay but how it will know how to protect the LiveView lifecycle stages?

You need to implement allowed?/4 protocol functions. The first input of allowed?/4 function is the user, the second is the LiveView module, the third is the LiveView lifecycle stage and the last is LiveView lifecycle stage inputs. In this way you can pattern match to your needings. You can put this file anywhere but /lib/my_app_web/live/abilities.ex is recommended.

It must return a boolean.

# /lib/my_app_web/live/abilities.ex

defimpl LiveGuard.Allowed, for: User do
  @before_compile {LiveGuard, :before_compile_allowed}

  def allowed?(
        %User{role: role},
        MyModuleLive,
        :handle_event,
        {"delete_item", _unsigned_params, _socket}
      )
      when role in [:viewer, :customer],
      do: false

  # other `allowed?/4` functions...
end

Note: As you can see, you don't have to define catch-all allowed?/4 function because we used @before_compile {LiveGuard, :before_compile_allowed} hook. It returns true. This is optional.

If the user is not authenticated you can add the following implementation as below:

defimpl LiveGuard.Allowed, for: Atom do
  @before_compile {LiveGuard, :before_compile_allowed}

  def allowed?(nil, MyModuleLive, :handle_event, {"delete_item", _unsigned_params, _socket}),
    do: false

  # other `allowed?/4` functions...
end

Optimization (optional)

By default if you use the on_mount/4 callback of LiveGuard, it will attach hooks to attachable LiveView lifecycle stages (:handle_params, :handle_event, :handle_info and :handle_async). If you need to protect for example only the :handle_event LiveView lifecycle stage for an individual LiveView module you can use this function. You can put this file anywhere but /lib/my_app_web/live/guarded_stages.ex is recommended.

It must return a list of valid attachable LiveView lifecycle stages (unless :after_render).

Example

# /lib/my_app_web/live/guarded_stages.ex

defimpl LiveGuard.GuardedStages, for: Atom do
  @before_compile {LiveGuard, :before_compile_guarded_stages}

  def guarded_stages(MyModuleLive), do: [:handle_event]

  # other `guarded_stages?/1` functions...
end

In this case it will only attach hook to :handle_event LiveView lifecycle stage.

Note: As you can see, you don't have to define catch-all guarded_stages/1 function because we used @before_compile {LiveGuard, :before_compile_guarded_stages} hook. It returns the valid attachable LiveView lifecycle stages (:handle_params, :handle_event, :handle_info and :handle_async). This is optional.

License

MIT License. Copyright 2023 Daniel Fabian.

Few words from the author

GitHub repository

This package is inspired by canary.