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 chainresource
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 chainresource
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
Link to this section Callbacks
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 plugparams
, theparams
field from the currentconn
. 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
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
, themessage
returned from yourresource
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.