10
votes

I have a small Phoenix application allowing users to login and consult their profile. I used the following simple route:

resources "/users", MyApp.UserController

But this allows every user to see the list of users via the :index action, as well as delete or update any user.

What is the easiest way to restrict access to admins only? Should I add a check in front of every action? Or should I create a "/admin" resource which would handle those operations? What is the recommended way?

3

3 Answers

14
votes

You would use a plug in the UserController. 0.4.x has no ability for conditionally plug s, but you could achieve what you want with something like:

defmodule MyApp.UserController do
  use Phoenix.Controller

  plug :authenticate, :admin
  plug :action

  def index(conn, _) do
    render conn, "index"
  end

  def create(conn, params) do
    # do the creating
  end
  ...

  defp authenticate(conn, :admin) do
    do_auth(conn, action_name(conn))
  end
  defp do_auth(conn, action) when action in [:create, :update, :destroy] do
    if AdminAuth.authenticated?(conn) do
      conn
    else
      halt conn
    end
  end
  defp do_auth(conn, _action), do: conn
end

The changes coming soon in 0.5 will allow easier conditional plugs, i.e.:

defmodule MyApp.UserController do
  use Phoenix.Controller

  plug :authenticate, :admin when action in [:create, :update, :destroy]

  def index(conn, _) do
    render conn, "index"
  end

  def create(conn, params) do
    # do the creating
  end
  ...

  defp authenticate(conn, :admin) do
    if AdminAuth.authenticated?(conn) do
      conn
    else
      halt conn
    end
  end
end

It's a good idea to keep your controllers for public/restricted access separate, so I would add an Admin.UserController like you made reference to for the restricted functionality.

2
votes

You could also define a separate pipeline for the authenticated endpoints:

defmodule MyApp.Router do
  use MyApp.Web, :router

  pipeline :admin do 
    plug :accepts, ["html"]

    plug Authentication # this represents some plug that provides authentication
  end

  scope "/", MyApp do
    pipe_through :browser

    resources "/things", ThingController
  end

  scope "/admin", MyApp do
    pipe_through :admin

    resources "/admin/things", Admin.ThingsController
  end
end

The admin scope is just an example, it can be anything you like, but the pipeline idea remains consistent.

This technique will keep your controllers cleaner, but is not always possible. It depends on your exact requirements.

0
votes

Building off of Chris Mccord you can actually do this without taking up a lot of space in the controller. Like this:

defmodule MyAppWeb.CategoryController do
  use MyAppWeb, :controller
  use MyApp.Helpers.Connection

  plug :authorize_crud, %{action: :read, permissions: [:can_access_categories]} when action in [:index, :show]
  plug :authorize_crud, %{action: :create, permissions: [:can_create_categories]} when action in [:new, :create]
  plug :authorize_crud, %{action: :update, permissions: [:can_update_categories]} when action in [:edit, :update]

  def index(conn, _params) do
    conn
  end

  def new(conn, _params) do
   conn
  end

  def create(conn, %{"category" => category_params}) do
    conn
  end

  def update(conn, %{"id" => id, "category" => category_params}) do
    conn
  end

end

defmodule MyApp.Helpers.Connection do

  @spec authorize(%Plug.Conn{}, list(), list()) :: %Plug.Conn{}
  def authorize(conn, user_permissions \\ [], required_permissions \\ []) do
    import Plug.Conn

     up = user_permissions |> MapSet.new()
     rp = required_permissions |> MapSet.new()

    case MapSet.subset?(rp, up) do
      true ->
        conn
      false ->
        conn
        |> put_status(404)
        |> Phoenix.Controller.render(MyAppWeb.ErrorView, "404.html", %{layout: false})
        |> halt()
    end
  end

  defmacro __using__(_) do
    quote do
      def authorize_crud(conn, opts = %{action: :read, permissions: permissions}) do
        check(conn, permissions)
      end

      def authorize_crud(conn, opts = %{action: :create, permissions: permissions}) do
        check(conn, permissions)
      end

      def authorize_crud(conn, opts = %{action: :update, permissions: permissions}) do
        check(conn, permissions)
      end

      def authorize_crud(conn, opts = %{action: :destroy, permissions: permissions}) do
        check(conn, permissions)
      end

      def check(conn, permissions) do
        user = conn.assigns.current_user |> Repo.preload(:role)
        MyApp.Helpers.Connection.authorize(conn, Accounts.list_permissions(user), permissions)
      end
    end
  end

end

Notes:

  1. When we use use Myapp.Helpers.Connection we get to use the imported function without a module name, this allows us to use it in the plug.
  2. The code defmacro __using__(_) is required because You have to include the using macro and put all the code that should be compiled into the using module in there
  3. The usage in my case is authorization functionality to restrict user access to certain controller actions. The authorize/3 function serves to return a true or false if the user has the required requirements.
  4. Inside of the defmacro we are overriding authorize_crud with pattern matching from what comes in from the plug call.
  5. An ideal list of data for Accounts.list_permissions(user) is
[:can_access_categories,:can_create_categories]
  1. The permissions coming in to match are whatever come in as permissions in the plug. So
[:can_access_categories]