policy_wonk v1.0.0-rc.0 PolicyWonk View Source

A lightweight authorization and resource loading tool for use with any Plug or Phoenix application.

About version 1.0

Policy Wonk is almost completely re-written for version 1.0. After living with it for well over a year, I realized there were a set of issues that warranted re-opening the underlying architecture.

  • It wasn’t compatible with Phoenix 1.3 umbrella apps. Or rather, you couldn’t have separate policies for different apps in an umbrella.
  • It had a whole mess of complexity that simply wasn’t needed. I never used most of the “shortcut” options since the more explicit versions (with slightly more typing) were always clearer.
  • Returning errors from Policies was too vague. I want to know errors are being processed!
  • The config file data isn’t necessary in a more explicit model.
  • Naming was inconsistent between policies and resources.

Version 1.0 takes these observations (and more), fixes them, and simplifies the configuration dramatically. It has less code and is overall simpler and faster.

Please see the section Upgrading to version 1.0 below for instructions on how to migrate existing policies to version 1.0. There is a small amount of work to do, but it is worth it.

Authentication vs. Authorization

Authentication (Auth-N) is the process of proving that a user or other entity is who/what it claims to be. Tools such as comeonin or guardian are mostly about authentication. Any time you are checking hashes or passwords, you are doing Auth-N.

Authorization (Auth-Z) is the process of deciding what a user/entity is allowed to do after they’ve been authenticated.

Authorization ranges from simple (ensuring somebody is logged in), to very rich (making sure the user has specific permissions to see a resource or that one resource is correctly related to the other resources being manipulated).

Examples

Load and enforce a current user in a router:

  pipeline :browser_session do
    plug MyAppWeb.Resources,  :current_user
    plug MyAppWeb.Policies,   :current_user
  end

  pipeline :admin do
    plug MyAppWeb.Policies, {:admin_permission, "dashboard"}
  end

In a controller:

  plug MyAppWeb.Policies, {:admin_permission, "dashboard"}

Policies

With PolicyWonk, you create policies and loaders for your application. They can be used as plugs in your router or controller or called for yes/no decisions in a template or controller.

This lets you enforce things like “a user is signed in” or “the admin has this permission” in the router. Or you could use a policy to determine if you should render a set of UI.

If a policy fails, it halts your plug chain and lets you decide what to do with the error.

Example policy:

  defmodule MyAppWeb.Policies do
    use PolicyWonk.Policy         # set up support for policies
    use PolicyWonk.Enforce        # turn this module into an enforcement plug

    def policy( assigns, :current_user ) do
      case assigns[:current_user] do
        %MyApp.Account.User{} ->
          :ok
        _ ->
          {:error, :current_user}
      end
    end

    def policy_error(conn, :current_user) do
      MyAppWeb.ErrorHandlers.unauthenticated(conn, "Must be logged in")
    end
  end

See the the PolicyWonk.Policy documentation for details.

Loaders

Loaders are similar to policies in that you define functions that can be used in the plug chain. Instead of making a yes/no enforcement decision, a loader will load a resource and insert it into the conn’s assigns map.

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

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

    def load_error(conn, _resource_id) do
      MyAppWeb.ErrorHandlers.resource_not_found( conn )
    end
  end

See the the PolicyWonk.Resource documentation for details.

Behaviors

PolicyWonk defines two behaviors for creating policies and resource loaders.

Policies outside plugs

In addition to evaluating policies in a plug chain, you will often want to test a policy when rendering ui, processing an action in a controller, or somewhere else.

The use PolicyWonk.Policy call in your policy module adds the enforce!/2 and authorized?/2 functions, which you can use in templates or controllers to decide what UI to show or to raise an error under certain conditions.

In a template:

  <%= if MyAppWeb.Policies.authorized?(@conn, {:admin_permission, "dashboard"}) do %>
    <%= link "Admin Dashboard", to: admin_dashboard_path(@conn, :index) %>
  <% end %>

In an action in a controller:

  def settings(conn, params) do
    ...
    # raise an error if the current user is not the user specified in the url.
    MyAppWeb.Policies.enforce!(conn, :user_is_self)
    ...
  end

Configuration

You no longer need to set up anything in your config files.

Just create the appropriate policy or loader modules and use them directly.

Upgrading to version 1.0

Module Names

The two resource loading modules have been renamed to make them more consistent with policies.

PolicyWonk.LoadResource -> PolicyWonk.Load PolicyWonk.Loader -> PolicyWonk.Resource

Likewise, within a resource module the following callback names have changed.

load_resource -> resource load_error -> resource_error

Using policies and Loaders

You no longer directly call PolicyWonk.Policy and PolicyWonk.Load as plugs from your router.

After using them in your policy modules, call your module in the router. This lets you be explicit about which policy modules are used where without anything in config.exs.

You can have different policy modules for different apps in an umbrella project, or simply build up a library policy modules that you can re-use as appropriate.

  # Old. Don't do this
  # pipeline :browser_session do
  #   plug PolicyWonk.Load, :current_user
  #   plug PolicyWonk.Enforce, :current_user
  # end

  # New. Do this
  pipeline :browser_session do
    plug MyAppWeb.Resources, :current_user
    plug MyAppWeb.Policies, :current_user
  end

Policies

Policies now require you to be more specific about when the policy fails. Previously, :ok was success and anything else was a failure. This lead to code that wasn’t obvious about what the failure cases were. Now the only accepted return values are :ok and {:error, message}.

The message part of {:error, message} can be any term you want and will be passed, unchanged, into your policy_error function.

  # Old. Don't do this
  # def policy( assigns, :current_user ) do
  #   case assigns[:current_user] do
  #     %MyApp.Account.User{} ->
  #       :ok
  #     _ ->
  #       :current_user
  #   end
  # end

  # New. Do this
  def policy( assigns, :current_user ) do
    case assigns[:current_user] do
      %MyApp.Account.User{} ->
        :ok
      _ ->
        {:error, :current_user}
    end
  end

Loaders

Loaders also now require you to be more specific about when loading a resource fails. Previously, {:ok, key, resource} was success and anything else was a failure. This lead to code that wasn’t obvious about what the failure cases were. Now the only accepted return values are {:ok, key, resource} and {:error, message}.

The message part of {:error, message} can be any term you want and will be passed, unchanged, into your load_error function.

  # Old. Don't do this
  # case Repo.get(User, user_id) do
  #   nil ->  :user
  #   user -> {:ok, :user, nil}
  # end

  # New. Do this
  case MyApp.Account.get_user(user_id) do
    nil ->  {:error, :user}
    user -> {:ok, :user, user}
  end

Local Policies and Loaders in a Controller

Previously, you could simply define a policy in a controller and it would override whatever was in your policy module. You can still have a policy or loader specific to a controller, but you need to call it as a plug in a more explicit fashion. This is more functional in nature.

To use a policy that is local to a controller, call use PolicyWonk.Policy at the top of your controller. This adds a small set of functions to your controller including enforce/2, which allows you to call local policies as a plug.

  # Old. Don't do this
  # defmodule MyAppWeb.Controller.AdminController do
  #   use MyAppWeb, :controller
  # 
  #   plug PolicyWonk.Enforce :user
  #
  #   policy(conn, :user) do
  #     ...
  #   end
  # end

  # New. Do this
  defmodule MyAppWeb.Controller.AdminController do
    use MyAppWeb, :controller
    use PolicyWonk.Policy

    plug :enforce, :user

    policy(conn, :user) do
      ...
    end
  end

Note that you do not need to call use PolicyWonk.Enforce to use a local policy in a controller. PolicyWonk.Enforce is only used to turn a module into a plug that can be called from a router.