Executing a plug *after* a route was matched?


How can I execute a plug after a route has matched, and know the controller and action that are to be executed?

Example is authorization:

# admin is allowed access to /users/edit
def authorized?(:admin, {Admin.UserController, :edit}), do: :ok

# all other users are unauthorized
def authorized?(_, {Admin.UserController, :edit}), do: {:error, :unauthorized}

I could enforce authorization rules inside each controller that needs authorization like:

plug :authorize_user_update when action in [:edit, :update, :delete]

But instead, I would like to have all the authorization rules (authorized? functions) in a centralized single file (Authorizor.ex) without needing to add anything to the controller.

In order to achieve this I think I just need the conn that I am still able to halt/redirect, the controller, the action, and pass these to a plug function that is called after the router has found the match (controller/action). That plug would call the function clauses above and the plug will halt/redirect in case of an authorization error.

Note: the reason for not having these in the controller, is

  1. to have all these rules in a single centralized place;
  2. if someone forgets to protect a controller, it’s not easily detected; dev errors happen and that would compromise security; missing authorization rules are easier to detect if those are centralized.

When a route is matched MyApp.SomeController.call(conn, action) is called. The router doesn’t know about controllers at all. It’s just another plug and the action is the option. So you could wrap the controller dispatch in a custom plug, but I guess you’d loose at least a few of the conveniencies of the router macros.

You can also look at putting the plug in my_app_web.ex and enforce it as part of use MyAppWeb, :controller.

1 Like

Do you mean add the following:

def controller do
  quote do
    # some code
    plug MyAppWeb.Authorization

And then use use :phoenix_action and :phoenix_controller from conn.private?

And about your first idea wrapping the controller dispatch, I don’t know where is that code to wrap.