Skip to content

Commit

Permalink
Admin permission (#63)
Browse files Browse the repository at this point in the history
* feat: make User struct the "resource" for Guardian

Also saves the groups as part of the claims

* feat: add admin? flag to User and fill in when fetching resource

* fixup! feat: make User struct the "resource" for Guardian
  • Loading branch information
lemald authored Jul 29, 2024
1 parent 1486b76 commit b7b0225
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 19 deletions.
2 changes: 2 additions & 0 deletions lib/orbit/authentication/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule Orbit.Authentication.User do
@type t :: %__MODULE__{
id: integer(),
email: String.t(),
admin?: boolean(),
permissions: [UserPermission.t()],
inserted_at: DateTime.t(),
updated_at: DateTime.t()
Expand All @@ -14,6 +15,7 @@ defmodule Orbit.Authentication.User do
schema "users" do
field(:email, :string)
field(:permissions, {:array, UserPermission})
field(:admin?, :boolean, virtual: true)
timestamps()
end

Expand Down
13 changes: 4 additions & 9 deletions lib/orbit_web/auth/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule OrbitWeb.Auth.Auth do

@spec login(Conn.t(), String.t(), integer(), [String.t()], String.t() | nil) ::
Conn.t()
def login(conn, username, ttl_seconds, _groups, logout_url) do
def login(conn, username, ttl_seconds, groups, logout_url) do
email = String.downcase(username)

case Repo.get_by(User, email: email) do
Expand All @@ -19,9 +19,9 @@ defmodule OrbitWeb.Auth.Auth do
# We use username (email) as the Guardian resource
conn
|> OrbitWeb.Auth.Guardian.Plug.sign_in(
username,
%User{email: email},
# claims
%{},
%{groups: groups},
ttl: {ttl_seconds, :seconds}
)
|> Plug.Conn.put_session(:username, username)
Expand All @@ -33,12 +33,7 @@ defmodule OrbitWeb.Auth.Auth do
if Map.has_key?(conn.assigns, :logged_in_user) do
conn.assigns[:logged_in_user]
else
if email = OrbitWeb.Auth.Guardian.Plug.current_resource(conn) do
email = String.downcase(email)
Repo.get_by(User, email: email)
else
nil
end
OrbitWeb.Auth.Guardian.Plug.current_resource(conn)
end
end

Expand Down
17 changes: 11 additions & 6 deletions lib/orbit_web/auth/guardian.ex
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
defmodule OrbitWeb.Auth.Guardian do
use Guardian, otp_app: :orbit
alias Orbit.Authentication.User
alias Orbit.Repo

@admin_group "orbit-admin"

@spec subject_for_token(Guardian.Token.resource(), Guardian.Token.claims()) ::
{:ok, String.t()} | {:error, atom()}
def subject_for_token(resource, _claims) do
sub = resource
{:ok, sub}
def subject_for_token(%User{email: email}, _claims) do
{:ok, email}
end

@spec resource_from_claims(Guardian.Token.claims()) ::
{:ok, Guardian.Token.resource()} | {:error, atom()}
def resource_from_claims(%{"sub" => id}) do
resource = id
{:ok, resource}
def resource_from_claims(%{"sub" => id, "groups" => groups}) do
case Repo.get_by(User, email: id) do
nil -> {:error, :user_not_found}
user -> {:ok, %User{user | admin?: @admin_group in groups}}
end
end
end

Expand Down
3 changes: 1 addition & 2 deletions lib/orbit_web/controllers/auth_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ defmodule OrbitWeb.AuthController do
end

def callback(%{assigns: %{ueberauth_auth: %{provider: :keycloak} = auth}} = conn, _params) do
username = String.replace(auth.info.email, "MBTA.com", "mbta.com")
# credentials = auth.credentials

# Ignore auth provider's TTL, set ours to 30 days so users don't have to log back in
Expand All @@ -58,7 +57,7 @@ defmodule OrbitWeb.AuthController do

conn
|> Auth.login(
username,
auth.info.email,
ttl_seconds,
groups,
logout_url
Expand Down
30 changes: 30 additions & 0 deletions test/orbit_web/auth/guardian_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule OrbitWeb.Auth.GuardianTest do
use Orbit.DataCase
alias Orbit.Authentication.User
alias OrbitWeb.Auth.Guardian
import Orbit.Factory

describe "subject_for_token/2" do
test "returns email address field" do
user = build(:user)

assert {:ok, user.email} == Guardian.subject_for_token(user, %{})
end
end

describe "resource_for_claims/1" do
test "pulls user from database" do
user = insert(:user)

assert {:ok, %User{user | admin?: false}} ==
Guardian.resource_from_claims(%{"sub" => user.email, "groups" => []})
end

test "sets the admin flag if user is in appropriate group" do
user = insert(:user)

assert {:ok, %User{user | admin?: true}} ==
Guardian.resource_from_claims(%{"sub" => user.email, "groups" => ["orbit-admin"]})
end
end
end
11 changes: 9 additions & 2 deletions test/orbit_web/controllers/auth_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ defmodule OrbitWeb.AuthControllerTest do
use OrbitWeb.ConnCase
alias OrbitWeb.Auth.Auth

defp login_with_groups(conn, _groups) do
defp login_with_groups(conn, groups) do
conn
|> get(~p"/auth/keycloak/callback?#{%{"email" => "[email protected]"}}")
|> get(~p"/auth/keycloak/callback?#{%{"email" => "[email protected]", "groups" => groups}}")
end

describe "/login" do
Expand All @@ -20,6 +20,13 @@ defmodule OrbitWeb.AuthControllerTest do
assert redirected_to(conn) == "/"
end

test "group information is saved in the claims", %{conn: conn} do
conn = login_with_groups(conn, ["some-group"])
assert redirected_to(conn) == "/"

assert %{"groups" => ["some-group"]} = Guardian.Plug.current_claims(conn)
end

test "remembers target URL", %{conn: conn} do
conn = get(conn, "/help")
assert redirected_to(conn) == ~p"/login"
Expand Down

0 comments on commit b7b0225

Please sign in to comment.