defmodule GitGudWeb.Api.Mobile.AuditController do
@moduledoc """
An organization's or a repository's audit log (`GitGud.Audit`), for
`gg audit`.
Takes the `audit:read` scope, which no token bundle includes: it has
to be ticked by hand. The reader is whoever may read the log on the
web (`Audit.readable?/2`): an org's admins, a personal repository's
owner, instance admins. The web's sudo mode has no API counterpart —
the token, made for this, is the second factor's stand-in.
An org or repository the caller can't see is a 404, as anywhere in
this API; one they can see but not administer is a 403. Entries
never carry the client address or user agent: the instance view on
the web is the only place those show.
Query params: `category`, `actor` (a handle), `repo` (a repository
name, org log only), `before` (an entry id) and `limit` (at most
100). The response's `next_before` pages on, null at the end.
"""
use GitGudWeb, :controller
import GitGudWeb.Api.Mobile.Helpers
alias GitGud.Audit
alias GitGud.Audit.Catalog
alias GitGud.Organizations
alias GitGudWeb.Api.Mobile.Auth
alias GitGudWeb.Api.Mobile.JSON
def org(conn, %{"org" => handle} = params) do
with {:ok, user} <- Auth.require_scope(conn, "audit:read"),
{:ok, org} <- fetch_org(conn, user, handle),
:ok <- authorize(conn, user, {:org, org}, "@#{org.handle}"),
{:ok, filters} <- filters(conn, params) do
filters = Map.merge(filters, Audit.repository_filter(org, present(params["repo"])))
page(conn, {:org, org}, filters, params)
else
{:error, conn} -> conn
end
end
def repo(conn, %{"owner" => owner, "name" => name} = params) do
with {:ok, user} <- Auth.require_scope(conn, "audit:read"),
{:ok, repo} <- fetch_repo(conn, user, owner, name),
:ok <- authorize(conn, user, {:repo, repo}, "#{owner}/#{name}"),
{:ok, filters} <- filters(conn, params) do
page(conn, {:repo, repo}, filters, params)
else
{:error, conn} -> conn
end
end
defp page(conn, scope, filters, params) do
limit = int_param(params, "limit", 50, 100)
cursor =
case Integer.parse(to_string(params["before"] || "")) do
{id, ""} when id > 0 -> {:before, id}
_ -> nil
end
{entries, cursors} = Audit.list(scope, filters, cursor, limit)
json(conn, %{
"entries" => Enum.map(entries, &JSON.audit_entry/1),
"next_before" => cursors.older
})
end
defp filters(conn, params) do
category = present(params["category"])
if category && category not in Catalog.categories() do
{:error, send_error(conn, 422, "unknown category #{category}")}
else
{:ok,
%{category: category}
|> Map.merge(Audit.actor_filter(present(params["actor"]) |> strip_at()))}
end
end
# Private orgs are their members' to know about.
defp fetch_org(conn, user, handle) do
case Organizations.get_organization_by_handle(handle) do
nil ->
{:error, send_error(conn, 404, "organization not found")}
org ->
if org.visibility != "private" or user.is_admin or Organizations.member?(org, user),
do: {:ok, org},
else: {:error, send_error(conn, 404, "organization not found")}
end
end
defp authorize(conn, user, scope, name) do
if Audit.readable?(user, scope),
do: :ok,
else: {:error, send_error(conn, 403, "only #{name}'s admins can read its audit log")}
end
defp present(v) when is_binary(v) do
case String.trim(v) do
"" -> nil
trimmed -> trimmed
end
end
defp present(_), do: nil
defp strip_at(nil), do: nil
defp strip_at(handle), do: String.trim_leading(handle, "@")
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
3.7 KiB · text
History
b868907