Protecting Admin Pages
I have a few LiveView pages used to inspect games in progress which are currently available to all users. I want to protect these routes so only users designated as admin have access. If an unauthenticated or unauthorized user visits an admin route, I want to return a 404 to not acknowledge the route even exists.
I first write a test that I know will fail since I haven’t written the code for the desired behavior.
defmodule MinotaurWeb.Admin.GameListLiveTest do
@moduledoc false
use MinotaurWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Minotaur.AccountsFixtures
describe "when user is unauthenticated" do
test "responds with 404", %{conn: conn} do
assert_error_sent :not_found, fn ->
live(conn, ~p"/admin/games")
end
end
end
end
To make the test pass, I create a custom exception module that I can raise from the router pipeline for unauthorized requests to admin routes.
defmodule MinotaurWeb.UnauthorizedAdminAccessError do
defexception [:message, plug_status: 404]
end
I create a new plug in the UserAuth
module which is already imported in the application router module.
def require_admin_user(conn, _opts) do
user = conn.assigns[:current_user]
if user && user.is_admin do
conn
else
raise MinotaurWeb.UnauthorizedAdminAccessError, "User is not admin"
end
end
There is no is_admin
key on a User
struct so I add a virtual field to the User
schema to allow the test to compile.
I set the default value for is_admin
to false which is enough to get my initial test to pass.
To test for users where is_admin
is true, I will need to add the column to the database table.
I create a new migration file for this:
defmodule Minotaur.Repo.Migrations.AddIsAdminToUsersTable do
use Ecto.Migration
def change do
alter(table("users")) do
add :is_admin, :boolean, default: false, null: false
end
end
end
I remove the :virtual
key from the User
schema, but keep the default value since calls to Repo.insert
will only return the values that are sent to the insert query.
If I set the default value only at the database level, this field would always be nil
anytime the key was omitted on insert.
I also add a function to the Accounts
module which promotes a user to an admin.
I create additional test cases for a logged in non-admin user and a happy path for an admin user.
Once these tests are passing, I deploy the changes to production.
The admin routes are now protected!