adrian
How to properly send and manage 403 responses
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:
- phoenix/lib/phoenix/exceptions.ex at main · phoenixframework/phoenix · GitHub
- ecto/lib/ecto/exceptions.ex at master · elixir-ecto/ecto · GitHub
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.
Most Liked
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.
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.
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?
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









