Packages
Plug based authorization and resource loading. Aimed at Phoenix, but depends only on Plug. MIT license Updated to compile clean with Elixir 1.4
Current section
Files
Jump to
Current section
Files
lib/resource.ex
defmodule PolicyWonk.Resource do
@moduledoc """
# 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
"""
@doc """
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
"""
@callback resource(conn :: Plug.Conn.t(), resource :: any, params :: Map.t()) ::
{:ok, atom, any} | {:error, any}
@doc """
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.
"""
@callback resource_error(conn :: Plug.Conn.t(), message :: any) :: Plug.Conn.t()
@format_error "Loaders must return either {:ok, key, resource} or an {:error, message}"
# ===========================================================================
# define a policy error here - not found or something like that
defmodule Error do
@moduledoc false
defexception message: "#{IO.ANSI.red()}Load Resource Failure\n", module: nil, resource: nil
end
# ===========================================================================
defmacro __using__(_use_opts) do
quote do
@behaviour PolicyWonk.Resource
# ----------------------------------------------------
@doc """
Callable as a local plug. Loads one or more resources.
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 load a resource 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` flag indicating if a list of resources should be loaded asynchronously.
"""
def load(conn, resources, async \\ false) do
PolicyWonk.Resource.load(conn, __MODULE__, resources, async)
end
# ----------------------------------------------------
@doc """
Evaluates one or more resource loaders and returns the results.
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
If multiple resources are requested, they will be returned in a list of tuples.
MyAppWeb.Resources.load!(conn, [:thing_a, :thing_b])
# would return something like
[{:thing_a, thing_a}, {:thing_b, thing_b}]
## 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` flag indicating if a list of resources should be loaded asynchronously.
"""
def load!(conn, resources, async \\ false),
do: PolicyWonk.Resource.load!(conn, __MODULE__, resources, async)
end
end
# ----------------------------------------------------
# Enforce called as a (internal) plug
@doc false
def load(conn, module, resources, async \\ false)
# don't do anything if the conn is already halted
def load(%Plug.Conn{halted: true} = conn, _, _, _), do: conn
# load a list of resources, synchronously
def load(%Plug.Conn{} = conn, module, resources, false) when is_list(resources) do
Enum.reduce(resources, conn, &load(&2, module, &1, false))
end
# load a list of resources, asynchronously
def load(%Plug.Conn{} = conn, module, resources, true) when is_list(resources) do
# spin up tasks for all the loads
# wait for the async tasks to complete - assigning each into the conn
resources
|> Enum.map(fn resource ->
Task.async(fn -> do_load_resource(conn, module, resource) end)
end)
|> Enum.reduce_while(conn, fn task, acc_conn ->
case Task.await(task) do
{:ok, key, resource} ->
{:cont, Plug.Conn.assign(acc_conn, key, resource)}
{:error, message} ->
# handle the error
acc_conn = module.resource_error(acc_conn, message)
{:cont, acc_conn}
end
end)
end
# load a single resource
def load(%Plug.Conn{} = conn, module, resource, _) do
case module.resource(conn, resource, conn.params) do
{:ok, key, resource} ->
Plug.Conn.assign(conn, key, resource)
{:error, message} ->
# handle the error
module.resource_error(conn, message)
end
end
# ----------------------------------------------------
# load that returns the resource or raises an error
@doc false
def load!(conn, module, resources, async \\ false)
# load! a list of resources, synchronously
def load!(%Plug.Conn{} = conn, module, resources, false) when is_list(resources) do
resources
|> Enum.reduce([], fn resource, acc ->
[{resource, load!(conn, module, resource)} | acc]
end)
|> Enum.reverse()
end
# load! a list of resources, asynchronously
def load!(%Plug.Conn{} = conn, module, resources, true) when is_list(resources) do
# spin up tasks for all the resources
# wait for the async tasks to complete - assigning each into the conn
resources
|> Enum.map(fn resource ->
Task.async(fn -> do_load_resource(conn, module, resource) end)
end)
|> Enum.reduce_while([], fn task, acc ->
case Task.await(task) do
{:ok, key, resource} ->
{:cont, [{key, resource} | acc]}
{:error, resource, message} ->
raise_error(message, module, resource)
end
end)
end
# load! a single resource
def load!(%Plug.Conn{} = conn, module, resource, _) do
case module.resource(conn, resource, conn.params) do
{:ok, _, resource} ->
resource
{:error, message} ->
raise_error(message, module, resource)
_ ->
raise_error(@format_error, module, resource)
end
end
# ============================================================================
# --------------------------------------------------------
defp do_load_resource(conn, module, resource) do
case module.resource(conn, resource, conn.params) do
{:ok, key, resource} ->
{:ok, key, resource}
{:error, message} ->
{:error, message}
_ ->
raise_error(@format_error, module, resource)
end
end
# --------------------------------------------------------
defp raise_error(message, module, resource) do
message =
message <>
"\n" <>
"#{IO.ANSI.green()}module: #{IO.ANSI.yellow()}#{inspect(module)}\n" <>
"#{IO.ANSI.green()}resource: #{IO.ANSI.yellow()}#{inspect(resource)}\n" <>
IO.ANSI.default_color()
raise Error, message: message, module: module, resource: resource
end
end