policy_wonk v1.0.0-rc.0 PolicyWonk.Resource behaviour View Source

Overview

A resource loader is a function that loads (or prepares) a single resource. The result is put into the conn’s assigns field.

A simple resource loader:

def resource( _conn, :user, %{"id" => user_id} ) do
  case MyAppWeb.Account.get_user(user_id) do
    nil ->  {:error, :not_found}
    user -> {:ok, :user, user}
  end
end

The above loader takes the user_id (which is from the params of the current request) and attempts to load a user model from the database. The result is put into the conn’s assigns field under the :user key.

Your loaders don’t need to hit a database. You could generate data or otherwise prepare something else. The point is that the result gets put into the conn’s assigns map.

Usage

The only time you should directly use the PolicyWonk.Resource module is to call use PolicyWonk.Resource when defining your resource loader module.

use PolicyWonk.Resource injects the load/3, load!/3, functions into your loader modules. These run and evaluate your resource functions and act accordingly on the results.

Loading resources during the plug chain has several important benefits:

  • When a resource can’t be loaded, you can halt the plug chain and handle the error before your actions get called. This lets you code your actions for the happy path.
  • Reuse and consistency improves in how resources are loaded. You can write one loader that is accessed by many controllers or router pipelines.
  • It lets you enforce policies on the resources before your actions are called. The best way to avoid security mistakes is to never run that code in the first place.

The only way to indicate success from a resource function is to return a tuple such as {:ok, key, resource}. The first field in the tuple must be :ok to indicate success. The middle field is the name of the resource as you want it assigned into conn.assigns. The last field is the resource itself.

The idea is that you define multiple resource functions and use Elixir’s pattern matching to find the right one. Like policies, this loader name could be an atom, tuple, map or really anything Elixir/Erlang can match against.

If the resource fails to load, return {:error, message}, which will in turn pass the message term to your loader_error callback.

In general, if a requested resource fails to load, it halts the plug and handles the error before the request controller action is ever run. This front-loads the resource loading checks before the controller/actions using router pipelines as a choke point.

Example resource loader module:

  defmodule MyAppWeb.Resources do
    use PolicyWonk.Resource       # set up support for resources
    use PolicyWonk.Load           # turn this module into an resource loading into a plug

    def resource( _conn, :user, %{"id" => user_id} ) do
      case MyAppWeb.Account.get_user(user_id) do
        nil ->  {:error, :not_found}
        user -> {:ok, :user, user}
      end
    end

    def policy_error(conn, :not_found) do
      MyAppWeb.ErrorHandlers.resource_not_found(conn, "Resource Not Found")
    end
  end

Injected functions

When you call use PolicyWonk.Resource, the following functions are injected into your module.

load/3

load(conn, resource, async \ false)

Callable as a local plug. Load accepts the current conn and a resource indicator. It then calls the resource function, evaluates the response and either puts the result into conn.assigns or transforms the conn with a failure.

You will normally only use this function if you want to enforce a policy that is written into a controller. Then the plug call will look like this:

  plug :load, :some_resource

If you want to enforce a policy from your router, please read the PolicyWonk.Load documentation.

parameters:

  • conn The current conn in the plug chain
  • resource The resource or resources you want to load. This can be either a single term representing one resource, or a list of resource terms.
  • ‘async’ a true/false flag indicating if the resources passed in a list should be loaded asynchronously or not.

load!/3

load!(conn, resource, async \ false)

Loads a resource and returns it. Raises when the resource function returns {:error, message}. This is a handy way to use your resource functions from within an action in a controller.

If multiple resources are requested the loaded resources are returned in a list of tuples indicating which resources are which.

  load!(conn, [:user, :thing])
  # returns something like...
  [{:user, user}, {:thing, thing}]

parameters:

  • conn The current conn in the plug chain
  • resource The resource or resources you want to load. This can be either a single term representing one resource, or a list of resource terms.
  • ‘async’ a true/false flag indicating if the resources passed in a list should be loaded asynchronously or not.

Loader Failures

To gracefully handle a load error, return a {:error, message} tuple.

PolicyWonk.Load will then cease attempting to load other resources and call your resource_error(conn, message) function. The message parameter is what you returned from your resource function.

def resource_error(conn, message) do
  conn
  |> put_status(404)
  |> put_view(MyApp.ErrorView)
  |> render("404.html")
  |> halt()
end

The resource_error function works just like a regular plug function. It takes a conn, and whatever was returned from the loader. You can manipulate the conn however you want to respond to that error. Then return the conn.

Unlike handling a policy error, halt(conn) is not called for you. If you want the resource load failure to halt the plug chain, make sure to call halt(conn) in your resource_error function.

Sometimes you want the plug chain to continue with a nil resource…

Use outside the plug chain

Resources are usually loaded through a plug, but can also be used inside of other code, such as an action. If you are just reading from a db, then you should probably call your model context functions instead. But if it does something more complicated to prepare a resource, then this can be pretty handy.

In an action in a controller:

  def settings(conn, params) do
    ...
    # raise an error if the resource fails to load.
    resource = MyAppWeb.Resources.load!(conn, :some_resource)
    ...
  end

Resources in a single controller

Sometimes you want to load a resource just across the actions of a single controller. Instead of building up a separate resource module, you can just add and load the resource in the controller itself.

  defmodule MyAppWeb.Controller.AdminController do
    use PolicyWonk.Resource         # set up support for resources
    # do not need to use PolicyWonk.Load here...

    plug :load, :thing

    def policy( assigns, :thing ) do
      # code that loads a thing...
    end

    def policy_error(conn, :thing) do
      MyAppWeb.ErrorHandlers.resource_not_found(conn)
    end
  end

Link to this section Summary

Callbacks

Handle a resource load error. Only called during the plug chain

Link to this section Callbacks

Link to this callback resource(conn, resource, params) View Source
resource(conn :: Plug.Conn.t(), resource :: any(), params :: Map.t()) ::
  {:ok, atom(), any()} |
  {:error, any()}

Load a resource.

parameters

  • conn, the current conn in the plug chain. For informational purposes.
  • resource, The resource term you requested with invoking the plug
  • params, the params field from the current conn. Passed in as a convenience. Useful for parsing and matching against.

Return values

Must return either {:ok, key, resource} or {:error, message}. If it is an error, the message term will be pass on you your resource_error callback unchanged.

Example:

  def resource( _conn, :user, %{"id" => user_id} ) do
    case Repo.get(Account.User, user_id) do
      nil ->  {:error, "User not found"}
      user -> {:ok, :user, user}
    end
  end
Link to this callback resource_error(conn, message) View Source
resource_error(conn :: Plug.Conn.t(), message :: any()) :: Plug.Conn.t()

Handle a resource load error. Only called during the plug chain.

Must return a conn, which you are free to transform.

parameters

  • conn, the current conn in the plug chain. Transform this to handle the error.
  • message, the message returned from your resource function.

Example:

  def resource_error(conn, message) do
    conn
    |> put_status(404)
    |> put_view(MyApp.ErrorView)
    |> render("404.html")
    |> halt()
  end

Unlike policies, if you want to halt the plug chain on a resource load error, you must call halt() yourself during the resource_error function.