defmodule Permit.Phoenix do @moduledoc ~S""" Phoenix, Plug and LiveView integrations integration for [Permit](https://github.com/curiosum-dev/permit). In Phoenix controller actions as well as LiveView modules, based on the resource configured for the current controller or LiveView (e.g. an `Article`), the current user, and the action (determined by the controller action or `:live_action`), it typically performs the following operations:application 1. Based on current params, typically an ID parameter, preload a record which is the subject of the current action. This is done either from the DB when `Permit.Ecto` is used or in a different manner. 2. Call to the application's authorization module (the one that has `use Permit` - refer to [docs for `Permit`](https://hexdocs.pm/permit/Permit.html#module-check-a-user-s-authorization-to-perform-an-action-on-a-resource) for examples) to ask whether the preloaded record is authorized to perform the current controller action. 3. Expose the loaded record into `assigns` if authorized, or perform a defined action (e.g. redirect) otherwise. ## Setup In your `mix.exs`, add `:permit_phoenix` to your dependencies. Also, add `:permit_ecto` unless you don't use Ecto for managing relationships between users and data and sourcing attributes important for authorization. ``` def deps do [ {:permit_phoenix, "~> 0.4.0"}, {:permit_ecto, "~> 0.3.1"} ] end ``` For convenience defining permissions using action names corresponding to controller and live actions, create an actions module that extends `Permit.Phoenix.Actions` and merges the actions from your router. This module also declares which actions are considered singular (operating on a single resource) or plural (operating on a list of resources). After configuring action names, configure and define your permissions [(see Permit docs for details)] (https://hexdocs.pm/permit/Permit.html#module-configure-define-your-permissions). With `Permit.Ecto`, use `Permit.Ecto.Permissions` in your permissions module. Once you've got a configured `MyApp.Authorization` module, use it in your controller and live view modules. ## Controller usage To setup a controller to use `Permit.Phoenix`, use `Permit.Phoenix.Controller`: ```elixir defmodule MyAppWeb.ArticleController do use Permit.Phoenix.Controller, authorization_module: MyApp.Authorization, resource_module: MyApp.Article def index(conn, params) do # @loaded_resources is assigned if authorized, containing the list of records render(conn, :index) end def show(conn, params) do # @loaded_resource is assigned if authorized, containing the loaded record render(conn, :show) end end ``` By default, in `:show` and `:index`, Permit will check for the `:read` permission on the `MyApp.Article` resource by the current user (`@current_scope.user`). `Permit.Ecto` will generate a query filtered by permission conditions and to fetch the record (or records) by the `"id"` param, by default. If authorized, the record will be assigned to `@loaded_resource`. Otherwise, the `handle_unauthorized/2` callback will be called, which defaults to redirecting to `/`. You can customize the authorization check by overriding the `handle_unauthorized/2` callback. ```elixir @impl true def handle_unauthorized(action, conn) do redirect(conn, to: "/") end ``` You can put the configuration in your main web module and override specific options in individual controllers. ```elixir defmodule MyAppWeb do def controller do quote do use Permit.Phoenix.Controller, authorization_module: MyApp.Authorization end end end defmodule MyAppWeb.ArticleController do use MyAppWeb, :controller @impl true def resource_module, do: MyApp.Article # Controller actions... end ``` See `Permit.Phoenix.Controller` for more information on how to handle authorization errors, customize generated queries, and other available options and callbacks. That's it - the preload, the current user fetching, the assignment of loaded record and the handling of authorization errors is automatic. ## LiveView usage On mount, `Permit.Phoenix.LiveView` attaches hooks to the `handle_params/3` and `handle_event/3` callbacks to preload records and perform authorization: - at mount, hooked before the `mount/3` callback - during navigation, when `:live_action` changes - before `handle_params/3` is called - in event handling, when an event is triggered - before `handle_event/3` is called Action name is either taken from the `:live_action` defined for the current route, or - in event authorization - from an explicit mapping between event names and Permit action names using the `@permit_action` module attribute. Record ID is taken from the `"id"` param, by default - both in navigation and in event handling. In events that do not carry an ID param, the record currently assigned to `@loaded_resource` is used and reloaded to ensure permissions are evaluated against the latest data. To setup a LiveView to use `Permit.Phoenix`, use `Permit.Phoenix.LiveView`: ```elixir defmodule MyApp.ArticleLive.Show do use MyAppWeb, :live_view use Permit.Phoenix.LiveView, authorization_module: MyApp.Authorization, resource_module: MyApp.Article end ``` Just like with controllers, you can put the configuration in your main web module and override specific options in individual LiveViews. ```elixir defmodule MyAppWeb do def live_view do quote do use Permit.Phoenix.LiveView, authorization_module: MyApp.Authorization end end end defmodule MyApp.ArticleLive.Show do use MyAppWeb, :live_view @impl true def resource_module, do: MyApp.Article end ``` Param and event handlers put preloaded and authorized record in `@loaded_resource` or `@loaded_resources` assigns, respectively. For plural actions like `:index`, the records can be streamed to `@streams.loaded_resources` instead, if `:use_stream` option is set to `true`. ```elixir defmodule MyApp.ArticleLive.Index do use MyAppWeb, :live_view use Permit.Phoenix.LiveView, authorization_module: MyApp.Authorization, resource_module: MyApp.Article, use_stream?: true @impl true def handle_params(params, _url, socket) do # @streams.loaded_resources is assigned if authorized, containing the list of records {:noreply, socket} end @impl true @permit_action :delete def handle_event("delete", params, socket) do # @loaded_resource is assigned if authorized, containing the loaded record # Delete the record and stream the deletion to the client end end ``` If authorization fails, `handle_unauthorized/2` is called, which can either `:cont` or `:halt` the processing. By default, it stays on the same page and displays a flash error message unless it's in the mounting phase or cannot determine the live referer. In events that do not carry an ID param (e.g. form updating with `"save"` and form data in the params), authorization to perform the current action in general is still performed - the record currently assigned to `@loaded_resource` is used and reloaded to ensure authorization correctness, unless `reload_on_event?/2` is overridden to `false`. See `Permit.Phoenix.LiveView` for more information on how to handle authorization errors, customize generated queries, and other available options and callbacks. """ end