From 6843432c547ec88378d73a377dda6ce49a2fdfc6 Mon Sep 17 00:00:00 2001 From: Tobias Hungwe Date: Sat, 9 May 2026 14:53:27 +0200 Subject: [PATCH 01/12] update role management by adding member actions and abilities --- lib/mailgun_logger/roles/roles.ex | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/mailgun_logger/roles/roles.ex b/lib/mailgun_logger/roles/roles.ex index 8a873bb..0088be8 100644 --- a/lib/mailgun_logger/roles/roles.ex +++ b/lib/mailgun_logger/roles/roles.ex @@ -7,15 +7,18 @@ defmodule MailgunLogger.Roles do @superuser_role "superuser" @admin_role "admin" + @member_role "member" ######################################################### @default_actions ~w() - @admin_actions ~w(do_stuff) ++ @default_actions + @admin_actions ~w(do_stuff) ++ @default_actions ++ @member_actions @superuser_actions ~w() ++ @admin_actions + @member_actions ~w() ++ @default_actions + ######################################################### @doc false @@ -55,6 +58,12 @@ defmodule MailgunLogger.Roles do Enum.any?(roles, &can?(&1.name, action)) end + + for action <- @member_actions do + action = String.to_atom(action) + def can?(@member_role, unquote(action)), do: true + end + for action <- @admin_actions do action = String.to_atom(action) def can?(@admin_role, unquote(action)), do: true @@ -69,12 +78,14 @@ defmodule MailgunLogger.Roles do def is?(%User{roles: roles}, :superuser), do: is(roles, "superuser") def is?(%User{roles: roles}, :admin), do: is(roles, "admin") + def is?(%User{roles: roles}, :member), do: is(roles, "member") def is?(_, _), do: raise("Roles.is/2 requires roles to be preloaded") defp is(roles, role) when is_binary(role), do: Enum.map(roles, & &1.name) |> Enum.member?(role) def abilities(%User{roles: []}), do: [] def abilities(%User{roles: roles}), do: hd(roles) |> abilities() + def abilities(%Role{name: "member"}), do: @member_actions def abilities(%Role{name: "admin"}), do: @admin_actions def abilities(%Role{name: "superuser"}), do: @superuser_actions From 162a6b6cccab3c83e2479db1cc93a062b5417816 Mon Sep 17 00:00:00 2001 From: Tobias Hungwe Date: Sat, 9 May 2026 14:54:34 +0200 Subject: [PATCH 02/12] add new member role to seeding data --- lib/mailgun_logger/seeder.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mailgun_logger/seeder.ex b/lib/mailgun_logger/seeder.ex index 6fe0588..d2d25f4 100644 --- a/lib/mailgun_logger/seeder.ex +++ b/lib/mailgun_logger/seeder.ex @@ -6,7 +6,7 @@ defmodule MailgunLogger.Seeder do # alias MailgunLogger.UserRole - @roles [%Role{name: "superuser"}, %Role{name: "admin"}] + @roles [%Role{name: "superuser"}, %Role{name: "admin"}, %Role{name: "member"}] def run do Enum.each(@roles, &insert_if_new(&1)) From aa7f8e72c534796adf2c18fe49c35e5d93a06547 Mon Sep 17 00:00:00 2001 From: Tobias Hungwe Date: Sat, 9 May 2026 15:16:50 +0200 Subject: [PATCH 03/12] add RequirePermission plug to enforce user permissions --- .../plugs/require_permission.ex | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 lib/mailgun_logger_web/plugs/require_permission.ex diff --git a/lib/mailgun_logger_web/plugs/require_permission.ex b/lib/mailgun_logger_web/plugs/require_permission.ex new file mode 100644 index 0000000..fd86409 --- /dev/null +++ b/lib/mailgun_logger_web/plugs/require_permission.ex @@ -0,0 +1,29 @@ +defmodule MailgunLoggerWeb.Plugs.RequirePermission do + import Plug.Conn + import Phoenix.Controller + + alias MailgunLogger.Roles + alias MailgunLoggerWeb.Router.Helpers, as: Routes + + @moduledoc """ + Enforces that the authenticated user has the required permission. + This plug expects MailgunLoggerWeb.Plugs.Auth to have assigned :current_user. + """ + + @spec init(atom()) :: atom() + def init(permission), do: permission + + @spec call(Plug.Conn.t(), atom()) :: Plug.Conn.t() + def call(conn, permission) do + user = conn.assigns[:current_user] + + if user && Roles.can?(user, permission) do + conn + else + conn + |> put_flash(:error, "You do not have permission to access this page.") + |> redirect(to: Routes.event_path(conn, :index)) + |> halt() + end + end +end \ No newline at end of file From dbffa6addf1af968fb53bc441d697b85e870736b Mon Sep 17 00:00:00 2001 From: Tobias Hungwe Date: Sat, 9 May 2026 15:33:31 +0200 Subject: [PATCH 04/12] add role-based access pipelines for events and admin actions --- lib/mailgun_logger_web/router.ex | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/mailgun_logger_web/router.ex b/lib/mailgun_logger_web/router.ex index 7297982..df8f706 100644 --- a/lib/mailgun_logger_web/router.ex +++ b/lib/mailgun_logger_web/router.ex @@ -23,6 +23,16 @@ defmodule MailgunLoggerWeb.Router do plug(MailgunLoggerWeb.Plugs.Auth) end + pipeline :require_events_access do + plug(MailgunLoggerWeb.Plugs.RequirePermission, :view_events) + end + + pipeline :require_admin_access do + plug(MailgunLoggerWeb.Plugs.RequirePermission, :do_stuff) + end + + + # Always except in prod if Application.compile_env(:mailgun_logger, :env) == :dev do forward("/sent_emails", Bamboo.SentEmailViewerPlug) @@ -73,18 +83,28 @@ defmodule MailgunLoggerWeb.Router do pipe_through([:browser, :auth]) get("/logout", AuthController, :logout) + get("/profile", ProfileController, :edit) + put("/profile", ProfileController, :update) + end + + scope "/", MailgunLoggerWeb do + pipe_through([:browser, :auth, :require_events_access]) + + get("/", PageController, :index) + get("/non-affiliation", PageController, :non_affiliation) resources("/events", EventController, only: [:index, :show]) get("/events/:id/stored_message", EventController, :stored_message) + end + + scope "/", MailgunLoggerWeb do + pipe_through([:browser, :auth, :require_admin_access]) + resources("/accounts", AccountController, except: [:show]) - get("/profile", ProfileController, :edit) - put("/profile", ProfileController, :update) resources("/users", UserController, except: [:show]) - get("/", PageController, :index) post("/trigger-run", PageController, :trigger_run) get("/stats", PageController, :stats) get("/graphs", PageController, :graphs) - get("/non-affiliation", PageController, :non_affiliation) end end From 8c4309563330c32af27bc8732da5561bf626af19 Mon Sep 17 00:00:00 2001 From: Tobias Hungwe Date: Sat, 9 May 2026 15:48:03 +0200 Subject: [PATCH 05/12] implement role-based visibility for navigation links --- .../templates/layout/app.html.heex | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/mailgun_logger_web/templates/layout/app.html.heex b/lib/mailgun_logger_web/templates/layout/app.html.heex index 6a78307..5d0cd52 100644 --- a/lib/mailgun_logger_web/templates/layout/app.html.heex +++ b/lib/mailgun_logger_web/templates/layout/app.html.heex @@ -38,10 +38,16 @@
    <%= if assigns[:current_user] do %> -
  • <.link href={Routes.event_path(@conn, :index)} class={active_link_class(@conn, MailgunLoggerWeb.EventController)}>Events
  • -
  • <.link href={Routes.page_path(@conn, :stats)} class={active_link_class(@conn, MailgunLoggerWeb.PageController, :index)}>Stats
  • -
  • <.link href={Routes.account_path(@conn, :index)} class={active_link_class(@conn, MailgunLoggerWeb.AccountController, :index)}>Accounts
  • -
  • <.link href={Routes.user_path(@conn, :index)} class={active_link_class(@conn, MailgunLoggerWeb.UserController, :index)}>Users
  • + <%= if MailgunLogger.Roles.can?(@current_user, :view_events) do %> +
  • <.link href={Routes.event_path(@conn, :index)} class={active_link_class(@conn, MailgunLoggerWeb.EventController)}>Events
  • + <% end %> + + <%= if MailgunLogger.Roles.can?(@current_user, :do_stuff) do %> +
  • <.link href={Routes.page_path(@conn, :stats)} class={active_link_class(@conn, MailgunLoggerWeb.PageController, :stats)}>Stats
  • +
  • <.link href={Routes.account_path(@conn, :index)} class={active_link_class(@conn, MailgunLoggerWeb.AccountController, :index)}>Accounts
  • +
  • <.link href={Routes.user_path(@conn, :index)} class={active_link_class(@conn, MailgunLoggerWeb.UserController, :index)}>Users
  • + <% end %> +