159 lines
4.1 KiB
Elixir
159 lines
4.1 KiB
Elixir
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 %>
|
|
<button phx-click="delete_backup">Delete</button>
|
|
<% 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
|
|
|
|
require Logger
|
|
|
|
@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.
|
|
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
|