adrian

adrian

Hi,
I’ve written a plug that checks if the user has privileges. If it doesn’t it returns:

defp allow_superusers(_, conn) do
  conn
  |> put_status(403) 
end

Is there any way phoenix can catch those status and render a 403.html or a
text? Is a good idea to use the render view inside the plug?

I’ve also tried:

defp allow_superusers(_, conn) do
  conn
  |> send_resp(403, "Not allowed")
  |> halt
end

And my final choice was using custom errors so I created a custom exception and the function was like:

defp allow_superusers(_, _conn) do
  raise App.UnauthorizedError
end

where the exception is deffined like:

defmodule App.UnauthorizedError do
  defexception message: "Unauthorized", accepts: [], plug_status: 403
end

But I have some concerns. Reading the docs and some books (specially Programing Elixir 1.2), It’s said that in elixir exceptions should be used for things that should never happen, so just for very exceptional cases.

As an example, it says that mix has no exception handlers in the code, and elixir has a total of five. I’ve read some code for Ecto and Phoenix and both define a few exceptions:

I’m totally new to elixir and phoenix, but I think 403 statuses don’t fall under the “exceptional event” that takes a process down like if the database goes down.

Maybe some experience developers could give us some advise here.

Showing Posts 1 to 9

OvermindDL1

OvermindDL1

I do my permission testing, which if that fails it throws match error of a special tagged tuple, which I catch in my generic error catcher here:

# ... snip others
  def handle_error({:perm, false}, conn)              ,do: do_unauthorized(conn)
# ... snip others

Where do_unauthorized/1 is:

  def do_unauthorized(conn) do
    ControllerCallbacks.unauthorized(conn)
  end

Where unauthorized/1 is:

  def unauthorized(conn, _params \\ %{}) do
    last_path = case conn.method do
      "GET" -> true
      _ -> false
    end |> case do
      false -> nil
      true -> conn.request_path <> if byte_size(conn.query_string) > 0 do "?" <> conn.query_string else "" end
    end

    conn
    |> put_session(:last_path, last_path)
    |> put_flash(:error, gettext("The current account does not have acces to that location, please log in with an account that does or contact an admin to add access if it is required."))
    |> redirect(to: auth_path(conn, :ldap_request))
  end

It just gets the location of where they were, the query, stories it, and redirects to the auth controller to have them log in with a better account (which immediately clears the stored path from the conn but stores it in another place internally and will redirect to it if they successfully log in with another account, among some other stuff). This unauthorized/1 function could easily just set a 403 status and render a template though.

adrian

adrian OP

Is it a good practice to call ControllerCallbacks.unauthorized(conn) from the plug? Or maybe unauthorized/1 could be inside the plug?

This is the plug:

defmodule App.Auth do
  import Plug.Conn

  def init(opts) do
    opts
  end

  def call(conn, _opts) do
    conn.assigns[:current_user]
    |> is_admin
    |> allow_superusers(conn)
  end

  defp is_admin(%{is_superuser: true}) do
    true
  end

  defp is_admin(_) do
    false
  end

  defp allow_superusers(true, conn) do
    conn
  end

  defp allow_superusers(_, _conn) do
    raise App.UnauthorizedError

    # This is an alternative
    # conn
    # |> put_status(403) 
    # |> send_resp(403, "Unauthorized")
    # |> halt
  end

end
OvermindDL1

OvermindDL1

I like to have everything that mutates a connection in a controller somewhere, or the helpers for my shared bits, absolutely unnecessary though, just an organizational thing for me. :slight_smile:

EDIT: I would not really raise the error there, I have a wrapper around all my leaf-node controllers to catch my custom errors.

adrian

adrian OP

So, If I understand correctly, you propose to alter the connection in a controller module calling the function in the plug like:

defp allow_superusers(_, _conn) do
  OtherModule.unauthorized(conn)
end

what do you mean with having a wrapper around all the leaf-node controllers? I want to avoid using custom errors, so I really like the idea you proposed of using a function like this:

def unauthorized(conn, _params \\ %{}) do
    last_path = case conn.method do
      "GET" -> true
      _ -> false
    end |> case do
      false -> nil
      true -> conn.request_path <> if byte_size(conn.query_string) > 0 do "?" <> conn.query_string else "" end
    end

    conn
    |> put_session(:last_path, last_path)
    |> put_flash(:error, gettext("The current account does not have acces to that location, please log in with an account that does or contact an admin to add access if it is required."))
    |> redirect(to: auth_path(conn, :ldap_request))
  end
OvermindDL1

OvermindDL1

My controller commands are like this (this is before a refactor in rearranging things, but still):

  def index_section(conn, %{"slug" => slug_param}) do
    happy_path!(else: handle_error(conn)) do
      @perm true = conn |> can?(show(%Perms.CheckInOut{section: true}))
      {:ok, section} = verify_single_record get_section_by_slug(slug_param)
      @perm true = conn |> can?(show(%Perms.CheckInOut{section: section.slug}))
      section = preload_section_with_checked_out_values(section)
      changeset = %CheckInOut.Value{section_id: section.id}
      |> CheckInOut.Value.changeset()
      render(conn, :index_section, section: section, changeset: changeset)
    end
  end

The happy_path!(else: handle_error(conn)) do handles the errors. The lines @perm true = conn |> can?(show(%Perms.CheckInOut{section: true})) and @perm true = conn |> can?(show(%Perms.CheckInOut{section: section.slug})) test permissions and return true if successful, so if that fails then it fails the match to true, and thus gets tagged with a :perm tuple via the @perm tag, which gets tossed into the handle_error(conn) call, which enters the path you saw before. I have very fine-grained permissions that are built up based on data that gets accessed (with a lot of early tests, like here it tests if they have access to ‘anything’ in the section, if true then it gets the section information and tests if they have access to this specific section, if true then it continues). This is why a Plug style permission tests would not even remotely work for me, they are far too coarse-grained, at least without multiple DB lookups, which…why, or stuffing the conn assign with lots of stuff?

adrian

adrian OP

Very useful, thank you!!

christopheradams

christopheradams

As an aside: for the benefit of fellow programmers and your future self, you should consider renaming your errors and functions from “Unauthorized” to “Forbidden”, as the former suggests (at least to me) a 401 error rather than 403.

bobbypriambodo

bobbypriambodo

Let me join the fun :slight_smile:

I was the one who suggested @adrian to use custom errors on the #phoenix-talk google group (as a fellow beginner). I personally used the method when implementing parameter validation for my routes (using GitHub - vic/params: Easy parameters validation/casting with Ecto.Schema, akin to Rails' strong parameters. · GitHub, raising InvalidParamsError on not ch.valid?). My justification is that I wanted to treat it as 422, and the Phoenix docs seem to suggest using custom errors for that (as they have been doing with Ecto.NoResultsError and others).

That way, my controller can be as simple as

defparams media_index_params %{
  limit: [field: :integer, default: 10],
  offset: [field: :integer, default: 0],
}

def index(conn, params) do
  validated_params = ParamsValidator.validate params, &media_index_params/1
  media = Media.fetch(validated_params)
  render(conn, "index.json", media: media)
end

The ParamsValidator.validate/1 function is the one that will raise the error. This is just a small API project and I don’t handle authentication and permission at all. I see @OvermindDL1’s solution as kind of convoluted but maybe it’s because I just haven’t worked on something of that scale yet (or because I haven’t seen the bigger picture :slight_smile:).

I do want feedbacks if there is a more recommended approach than using custom errors, or if there are drawbacks by using it.

OvermindDL1

OvermindDL1

True true, unauthorized should be 401 if you are returning codes.[quote=“bobbypriambodo, post:9, topic:2191”]
The ParamsValidator.validate/1 function is the one that will raise the error. This is just a small API project and I don’t handle authentication and permission at all. I see @OvermindDL1’s solution as kind of convoluted but maybe it’s because I just haven’t worked on something of that scale yet (or because I haven’t seen the bigger picture :slight_smile:).
[/quote]

It could very well be convoluted! ^.^
It grew this way because the error handling has become massive and can come from a variety of parts, like on my example above the verify_single_record call will cause a Match Error on the {:ok, section} if there is not a single record (I.E., it is not found), the special error tag it returns it matched for in my error handlers to return an appropriate NotFound code or so:

  def handle_error({:error, :no_record}, conn)        ,do: do_not_found(conn)
  def handle_error({:error, :too_many_records}, conn) ,do: do_unprocessable_entity(conn)

I have a long list here of various generic things that can be returned from controllers or functions they call. The not found and unprocessable entities, like most, are more generic than my unauthorized (which redirects to let them login better):

  def do_not_found(conn) do
    conn
    |> put_status(:not_found)
    |> render(MyServer.ErrorView, :not_found)
  end

  def do_unprocessable_entity(conn) do
    conn
    |> put_status(:unprocessable_entity)
    |> render(MyServer.ErrorView, :unprocessable_entity)
  end

And is probably more of what the OP wants to do.

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews