defmodule ToweropsWeb.Permissions do @moduledoc """ Authorization helpers for checking user permissions in LiveViews, controllers, and templates. Integrates with `Towerops.Organizations.Policy` to provide a simple, reusable API for permission checks based on organization membership roles. ## Usage in LiveViews import ToweropsWeb.Permissions def handle_event("delete_backup", _params, socket) do if can?(socket, :delete, :backup) do # Delete backup else {:noreply, put_flash(socket, :error, t("You don't have permission to delete backups"))} end end ## Usage in Templates <%= if can?(@current_scope, :delete, :backup) do %> <% end %> ## Usage in Controllers import ToweropsWeb.Permissions def delete(conn, _params) do if can?(conn.assigns.current_scope, :delete, :backup) do # Delete backup else conn |> put_flash(:error, t("You don't have permission to delete backups")) |> redirect(to: ~p"/") end end """ alias Phoenix.LiveView.Socket alias Towerops.Accounts.Scope alias Towerops.Organizations.Policy @doc """ Checks if the current user can perform an action on a resource. Accepts either a socket (LiveView) or a Scope struct directly. Returns `true` if: - User is a superuser (can do everything) - User's organization membership role allows the action per Policy ## Examples iex> can?(socket, :delete, :backup) true iex> can?(%Scope{}, :edit, :device) false """ def can?(%Socket{} = socket, action, resource) do can?(socket.assigns.current_scope, action, resource) end def can?(%Scope{} = scope, action, resource) do if Scope.superuser?(scope), do: true, else: do_can?(scope, action, resource) end def can?(nil, _action, _resource), do: false @doc """ Checks if the current user is an organization owner. Accepts either a socket (LiveView) or a Scope struct directly. ## Examples iex> owner?(socket) true iex> owner?(%Scope{}) false """ def owner?(%Socket{} = socket) do owner?(socket.assigns.current_scope) end def owner?(%Scope{} = scope) do if Scope.superuser?(scope), do: true, else: do_owner?(scope) end def owner?(nil), do: false @doc """ Checks if the current user is an organization owner or admin. Accepts either a socket (LiveView) or a Scope struct directly. ## Examples iex> admin?(socket) true iex> admin?(%Scope{}) false """ def admin?(%Socket{} = socket) do admin?(socket.assigns.current_scope) end def admin?(%Scope{} = scope) do if Scope.superuser?(scope), do: true, else: do_admin?(scope) end def admin?(nil), do: false # Pattern-matched private helpers for permission checks defp do_can?(%Scope{organization: nil}, _action, _resource), do: false defp do_can?(%Scope{} = scope, action, resource) do scope |> get_membership() |> Policy.can?(action, resource) end defp do_owner?(%Scope{organization: nil}), do: false defp do_owner?(%Scope{} = scope) do membership = get_membership(scope) membership && membership.role == :owner end defp do_admin?(%Scope{organization: nil}), do: false defp do_admin?(%Scope{} = scope) do membership = get_membership(scope) membership && membership.role in [:owner, :admin] end defp get_membership(%Scope{organization: org, user: user}) when not is_nil(org) and not is_nil(user) do if Ecto.assoc_loaded?(org.memberships) do Enum.find(org.memberships, &(&1.user_id == user.id)) else # Memberships aren't preloaded — the permission check silently # treats the user as having no role and denies access. That used # to look indistinguishable from a real denial; surface it so # missing preloads are noticed and fixed at the call site. require Logger Logger.warning( "Permission check on user=#{user.id} org=#{org.id} without preloaded memberships — denying access by default" ) nil end end defp get_membership(_scope), do: nil end