3.1 KiB · text History b868907
defmodule GitGudWeb.AuditLive.Org do
  @moduledoc """
  `/orgs/:handle/settings/audit[/:category[/before|after/:id]]` — who
  changed what in an organization and its repositories
  (`GitGud.Audit`).

  For the org's admins and instance admins, in sudo mode: the log is
  read-only, but it maps out the org's security posture. Both are
  checked again on every `handle_params`, so a tab left open by someone
  who has since been demoted, or whose sudo window lapsed, stops paging.
  The client address and user agent recorded with each entry are not
  shown here; only the instance view shows them.
  """
  use GitGudWeb, :live_view

  alias GitGud.Organizations
  alias GitGudWeb.AuditLive.Page

  # What an org's log can contain; instance and account entries never
  # belong to an org.
  @categories ~w(secrets members settings repositories branch_protection deploy_keys
                 webhooks credentials runners ci moderation)

  @impl true
  def mount(%{"handle" => handle}, _session, socket) do
    org = Organizations.get_organization_by_handle!(handle)

    if allowed?(org, socket.assigns.current_scope.user) do
      {:ok,
       socket
       |> assign(:org, org)
       |> assign(:page_title, "Audit log — #{handle}")
       |> Page.setup(@categories, refines: [:actor, :repo], repos: repo_names(org))}
    else
      {:ok, deny(socket, org)}
    end
  end

  @impl true
  def handle_params(params, uri, socket) do
    %{org: org, current_scope: %{user: user}} = socket.assigns

    if allowed?(org, user),
      do: Page.params(socket, params, uri, ~p"/orgs/#{org.handle}/settings/audit", {:org, org}),
      else: {:noreply, deny(socket, org)}
  end

  @impl true
  def handle_event("filter", %{"filter" => params}, socket), do: Page.filter(socket, params)
  def handle_event(_event, _params, socket), do: {:noreply, socket}

  @impl true
  def handle_info({:audit, id}, socket) do
    %{org: org, current_scope: %{user: user}} = socket.assigns
    Page.live(socket, id, allowed?(org, user))
  end

  # Every repository the org has, including any gone since: a deleted
  # one's entries are still in the log, under its old name.
  defp repo_names(org) do
    live = GitGud.Repositories.list_repositories_for(org) |> Enum.map(& &1.name)

    (live ++ GitGud.Audit.deleted_repository_names(org)) |> Enum.uniq() |> Enum.sort()
  end

  defp allowed?(org, user), do: GitGud.Audit.readable?(user, {:org, org})

  defp deny(socket, org) do
    socket
    |> put_flash(:error, "Org admin only.")
    |> redirect(to: ~p"/orgs/#{org.handle}")
  end

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash} current_scope={@current_scope}>
      <div class="space-y-6">
        <.org_settings_header org={@org} current={:audit} />
        <Page.body
          base={@base}
          filters={@filters}
          filter_form={@filter_form}
          categories={@categories}
          refines={@refines}
          repo_options={@repo_options}
          entries={@streams.entries}
          older={@older}
          newer={@newer}
        />
      </div>
    </Layouts.app>
    """
  end
end