Compare commits
1 commit
5998fb643d
...
bcd3871245
| Author | SHA1 | Date | |
|---|---|---|---|
| bcd3871245 |
8 changed files with 34 additions and 59 deletions
|
|
@ -106,18 +106,14 @@ defmodule MvWeb.Authorization do
|
||||||
iex> can_access_page?(mitglied, "/members")
|
iex> can_access_page?(mitglied, "/members")
|
||||||
false
|
false
|
||||||
"""
|
"""
|
||||||
@spec can_access_page?(map() | nil, String.t() | Phoenix.VerifiedRoutes.unverified_path()) ::
|
@spec can_access_page?(map() | nil, String.t()) :: boolean()
|
||||||
boolean()
|
|
||||||
def can_access_page?(nil, _page_path), do: false
|
def can_access_page?(nil, _page_path), do: false
|
||||||
|
|
||||||
def can_access_page?(user, page_path) do
|
def can_access_page?(user, page_path) do
|
||||||
# Convert verified route to string if needed
|
|
||||||
page_path_str = if is_binary(page_path), do: page_path, else: to_string(page_path)
|
|
||||||
|
|
||||||
with %{role: %{permission_set_name: ps_name}} when not is_nil(ps_name) <- user,
|
with %{role: %{permission_set_name: ps_name}} when not is_nil(ps_name) <- user,
|
||||||
{:ok, ps_atom} <- PermissionSets.permission_set_name_to_atom(ps_name),
|
{:ok, ps_atom} <- PermissionSets.permission_set_name_to_atom(ps_name),
|
||||||
permissions <- PermissionSets.get_permissions(ps_atom) do
|
permissions <- PermissionSets.get_permissions(ps_atom) do
|
||||||
page_matches?(permissions.pages, page_path_str)
|
page_matches?(permissions.pages, page_path)
|
||||||
else
|
else
|
||||||
_ -> false
|
_ -> false
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,9 @@ defmodule MvWeb.Layouts.Navbar do
|
||||||
<li>
|
<li>
|
||||||
<.link navigate="/settings">{gettext("Global Settings")}</.link>
|
<.link navigate="/settings">{gettext("Global Settings")}</.link>
|
||||||
</li>
|
</li>
|
||||||
<%= if can_access_page?(@current_user, ~p"/admin/roles") do %>
|
<%= if can_access_page?(@current_user, "/admin/roles") do %>
|
||||||
<li>
|
<li>
|
||||||
<.link navigate={~p"/admin/roles"}>{gettext("Roles")}</.link>
|
<.link navigate="/admin/roles">{gettext("Roles")}</.link>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ defmodule MvWeb.RoleLive.Form do
|
||||||
|
|
||||||
alias Mv.Authorization.PermissionSets
|
alias Mv.Authorization.PermissionSets
|
||||||
|
|
||||||
import MvWeb.RoleLive.Helpers, only: [format_error: 1]
|
import MvWeb.RoleLive.Helpers
|
||||||
|
|
||||||
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ defmodule MvWeb.RoleLive.Helpers do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Formats an error for display to the user.
|
Formats an error for display to the user.
|
||||||
Extracts error messages from Ash.Error.Invalid and joins them.
|
|
||||||
"""
|
"""
|
||||||
@spec format_error(Ash.Error.Invalid.t() | String.t() | any()) :: String.t()
|
@spec format_error(Ash.Error.Invalid.t() | String.t() | any()) :: String.t()
|
||||||
def format_error(%Ash.Error.Invalid{} = error) do
|
def format_error(%Ash.Error.Invalid{} = error) do
|
||||||
|
|
@ -25,20 +24,4 @@ defmodule MvWeb.RoleLive.Helpers do
|
||||||
def permission_set_badge_class("normal_user"), do: "badge badge-success badge-sm"
|
def permission_set_badge_class("normal_user"), do: "badge badge-success badge-sm"
|
||||||
def permission_set_badge_class("admin"), do: "badge badge-error badge-sm"
|
def permission_set_badge_class("admin"), do: "badge badge-error badge-sm"
|
||||||
def permission_set_badge_class(_), do: "badge badge-ghost badge-sm"
|
def permission_set_badge_class(_), do: "badge badge-ghost badge-sm"
|
||||||
|
|
||||||
@doc """
|
|
||||||
Builds Ash options with actor and domain, ensuring actor is never nil in real paths.
|
|
||||||
"""
|
|
||||||
@spec opts_with_actor(keyword(), map() | nil, atom()) :: keyword()
|
|
||||||
def opts_with_actor(base_opts \\ [], actor, domain) do
|
|
||||||
opts = Keyword.put(base_opts, :domain, domain)
|
|
||||||
|
|
||||||
if actor do
|
|
||||||
Keyword.put(opts, :actor, actor)
|
|
||||||
else
|
|
||||||
require Logger
|
|
||||||
Logger.warning("opts_with_actor called with nil actor - this may bypass policies")
|
|
||||||
opts
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,7 @@ defmodule MvWeb.RoleLive.Index do
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
import MvWeb.RoleLive.Helpers,
|
import MvWeb.RoleLive.Helpers
|
||||||
only: [format_error: 1, permission_set_badge_class: 1, opts_with_actor: 3]
|
|
||||||
|
|
||||||
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
||||||
|
|
||||||
|
|
@ -126,28 +125,33 @@ defmodule MvWeb.RoleLive.Index do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Loads all user counts for roles using DB-side aggregation for better performance
|
# Loads all user counts for roles in a single query to avoid N+1 queries
|
||||||
@spec load_user_counts([Mv.Authorization.Role.t()], map() | nil) :: %{
|
@spec load_user_counts([Mv.Authorization.Role.t()], map() | nil) :: %{
|
||||||
Ecto.UUID.t() => non_neg_integer()
|
Ecto.UUID.t() => non_neg_integer()
|
||||||
}
|
}
|
||||||
defp load_user_counts(roles, _actor) do
|
defp load_user_counts(roles, actor) do
|
||||||
role_ids = Enum.map(roles, & &1.id)
|
role_ids = Enum.map(roles, & &1.id)
|
||||||
|
|
||||||
# Use Ecto directly for efficient GROUP BY COUNT query
|
# Load all users with role_id in a single query
|
||||||
# This is much more performant than loading all users and counting in Elixir
|
opts = [domain: Mv.Accounts]
|
||||||
# Note: We bypass Ash here for performance, but this is a simple read-only query
|
opts = if actor, do: Keyword.put(opts, :actor, actor), else: opts
|
||||||
import Ecto.Query
|
|
||||||
|
|
||||||
query =
|
users =
|
||||||
from u in Accounts.User,
|
case Ash.read(
|
||||||
where: u.role_id in ^role_ids,
|
Accounts.User
|
||||||
group_by: u.role_id,
|
|> Ash.Query.filter(role_id in ^role_ids)
|
||||||
select: {u.role_id, count(u.id)}
|
|> Ash.Query.select([:role_id]),
|
||||||
|
opts
|
||||||
|
) do
|
||||||
|
{:ok, users_list} -> users_list
|
||||||
|
{:error, _} -> []
|
||||||
|
end
|
||||||
|
|
||||||
results = Mv.Repo.all(query)
|
# Group by role_id and count
|
||||||
|
users
|
||||||
results
|
|> Enum.group_by(& &1.role_id)
|
||||||
|> Enum.into(%{}, fn {role_id, count} -> {role_id, count} end)
|
|> Enum.map(fn {role_id, users_list} -> {role_id, length(users_list)} end)
|
||||||
|
|> Map.new()
|
||||||
end
|
end
|
||||||
|
|
||||||
# Gets user count from preloaded assigns map
|
# Gets user count from preloaded assigns map
|
||||||
|
|
@ -160,7 +164,8 @@ defmodule MvWeb.RoleLive.Index do
|
||||||
# Recalculates user count for a specific role (used before deletion)
|
# Recalculates user count for a specific role (used before deletion)
|
||||||
@spec recalculate_user_count(Mv.Authorization.Role.t(), map() | nil) :: non_neg_integer()
|
@spec recalculate_user_count(Mv.Authorization.Role.t(), map() | nil) :: non_neg_integer()
|
||||||
defp recalculate_user_count(role, actor) do
|
defp recalculate_user_count(role, actor) do
|
||||||
opts = opts_with_actor([], actor, Mv.Accounts)
|
opts = [domain: Mv.Accounts]
|
||||||
|
opts = if actor, do: Keyword.put(opts, :actor, actor), else: opts
|
||||||
|
|
||||||
case Ash.count(Accounts.User |> Ash.Query.filter(role_id == ^role.id), opts) do
|
case Ash.count(Accounts.User |> Ash.Query.filter(role_id == ^role.id), opts) do
|
||||||
{:ok, count} -> count
|
{:ok, count} -> count
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,7 @@ defmodule MvWeb.RoleLive.Show do
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
import MvWeb.RoleLive.Helpers,
|
import MvWeb.RoleLive.Helpers
|
||||||
only: [format_error: 1, permission_set_badge_class: 1, opts_with_actor: 3]
|
|
||||||
|
|
||||||
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
||||||
|
|
||||||
|
|
@ -144,7 +143,8 @@ defmodule MvWeb.RoleLive.Show do
|
||||||
# Recalculates user count for a specific role (used before deletion)
|
# Recalculates user count for a specific role (used before deletion)
|
||||||
@spec recalculate_user_count(Mv.Authorization.Role.t(), map() | nil) :: non_neg_integer()
|
@spec recalculate_user_count(Mv.Authorization.Role.t(), map() | nil) :: non_neg_integer()
|
||||||
defp recalculate_user_count(role, actor) do
|
defp recalculate_user_count(role, actor) do
|
||||||
opts = opts_with_actor([], actor, Mv.Accounts)
|
opts = [domain: Mv.Accounts]
|
||||||
|
opts = if actor, do: Keyword.put(opts, :actor, actor), else: opts
|
||||||
|
|
||||||
case Ash.count(Accounts.User |> Ash.Query.filter(role_id == ^role.id), opts) do
|
case Ash.count(Accounts.User |> Ash.Query.filter(role_id == ^role.id), opts) do
|
||||||
{:ok, count} -> count
|
{:ok, count} -> count
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,8 @@ defmodule MvWeb.LiveHelpers do
|
||||||
opts = [domain: Mv.Accounts, actor: user]
|
opts = [domain: Mv.Accounts, actor: user]
|
||||||
|
|
||||||
case Ash.load(user, :role, opts) do
|
case Ash.load(user, :role, opts) do
|
||||||
{:ok, loaded_user} ->
|
{:ok, loaded_user} -> loaded_user
|
||||||
loaded_user
|
{:error, _} -> user
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
# Log warning if role loading fails - this can cause authorization issues
|
|
||||||
require Logger
|
|
||||||
Logger.warning("Failed to load role for user #{user.id}: #{inspect(error)}")
|
|
||||||
user
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,7 @@ defmodule MvWeb.Router do
|
||||||
AshAuthentication-specific: We define that all routes can only be accessed when the user is signed in.
|
AshAuthentication-specific: We define that all routes can only be accessed when the user is signed in.
|
||||||
"""
|
"""
|
||||||
ash_authentication_live_session :authentication_required,
|
ash_authentication_live_session :authentication_required,
|
||||||
on_mount: [
|
on_mount: {MvWeb.LiveUserAuth, :live_user_required} do
|
||||||
{MvWeb.LiveUserAuth, :live_user_required},
|
|
||||||
{MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
|
||||||
] do
|
|
||||||
live "/", MemberLive.Index, :index
|
live "/", MemberLive.Index, :index
|
||||||
|
|
||||||
live "/members", MemberLive.Index, :index
|
live "/members", MemberLive.Index, :index
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue