neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
Secrets: changing a secret or variable leaves a line in the log
77def2d · Gabriel Morell · 2026-10-20 18:32
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud_web/live/secrets_live/org.ex
+28
−4
@@ -10,6 +10,8 @@ defmodule GitGudWeb.SecretsLive.Org do
| 10 | 10 | |
| 11 | 11 | use GitGudWeb, :live_view |
| 12 | 12 | |
| 13 | + require Logger | |
| 14 | + | |
| 13 | 15 | alias GitGud.Organizations |
| 14 | 16 | alias GitGud.Secrets |
| 15 | 17 |
@@ -58,8 +60,12 @@ defmodule GitGudWeb.SecretsLive.Org do
| 58 | 60 | defp handle_admin_event("put_secret", %{"name" => name, "value" => value}, socket) |
| 59 | 61 | when is_binary(name) and is_binary(value) and value != "" do |
| 60 | 62 | case Secrets.put_org_secret(socket.assigns.org, name, value) do |
| 61 | − {:ok, _} -> {:noreply, socket |> load() |> put_flash(:info, "Secret saved.")} | |
| 62 | − {:error, cs} -> {:noreply, put_flash(socket, :error, error_message(cs))} | |
| 63 | + {:ok, _} -> | |
| 64 | + audit(socket, "set", "secret", name) | |
| 65 | + {:noreply, socket |> load() |> put_flash(:info, "Secret saved.")} | |
| 66 | + | |
| 67 | + {:error, cs} -> | |
| 68 | + {:noreply, put_flash(socket, :error, error_message(cs))} | |
| 63 | 69 | end |
| 64 | 70 | end |
| 65 | 71 |
@@ -68,24 +74,42 @@ defmodule GitGudWeb.SecretsLive.Org do
| 68 | 74 | |
| 69 | 75 | defp handle_admin_event("delete_secret", %{"name" => name}, socket) when is_binary(name) do |
| 70 | 76 | :ok = Secrets.delete_org_secret(socket.assigns.org, name) |
| 77 | + audit(socket, "deleted", "secret", name) | |
| 71 | 78 | {:noreply, socket |> load() |> put_flash(:info, "Removed.")} |
| 72 | 79 | end |
| 73 | 80 | |
| 74 | 81 | defp handle_admin_event("put_var", %{"name" => name, "value" => value}, socket) |
| 75 | 82 | when is_binary(name) and is_binary(value) do |
| 76 | 83 | case Secrets.put_org_variable(socket.assigns.org, name, value) do |
| 77 | − {:ok, _} -> {:noreply, socket |> load() |> put_flash(:info, "Variable saved.")} | |
| 78 | − {:error, cs} -> {:noreply, put_flash(socket, :error, error_message(cs))} | |
| 84 | + {:ok, _} -> | |
| 85 | + audit(socket, "set", "variable", name) | |
| 86 | + {:noreply, socket |> load() |> put_flash(:info, "Variable saved.")} | |
| 87 | + | |
| 88 | + {:error, cs} -> | |
| 89 | + {:noreply, put_flash(socket, :error, error_message(cs))} | |
| 79 | 90 | end |
| 80 | 91 | end |
| 81 | 92 | |
| 82 | 93 | defp handle_admin_event("delete_var", %{"name" => name}, socket) when is_binary(name) do |
| 83 | 94 | :ok = Secrets.delete_org_variable(socket.assigns.org, name) |
| 95 | + audit(socket, "deleted", "variable", name) | |
| 84 | 96 | {:noreply, socket |> load()} |
| 85 | 97 | end |
| 86 | 98 | |
| 87 | 99 | defp handle_admin_event(_event, _params, socket), do: {:noreply, socket} |
| 88 | 100 | |
| 101 | + # There is no audit table to put this in, so it goes to the log, which | |
| 102 | + # an operator keeps: who changed which name where. Never the value. | |
| 103 | + defp audit(socket, verb, what, name) do | |
| 104 | + user = socket.assigns.current_scope.user | |
| 105 | + | |
| 106 | + Logger.notice( | |
| 107 | + "audit: user #{user.id} (#{user.handle}) #{verb} #{what} #{inspect(name)} on " <> | |
| 108 | + "organization #{socket.assigns.org.handle}", | |
| 109 | + audit: true | |
| 110 | + ) | |
| 111 | + end | |
| 112 | + | |
| 89 | 113 | defp error_message(%Ecto.Changeset{} = cs) do |
| 90 | 114 | cs |
| 91 | 115 | |> Ecto.Changeset.traverse_errors(fn {m, _} -> m end) |
modified
lib/git_gud_web/live/secrets_live/repo.ex
+28
−4
@@ -1,6 +1,8 @@
| 1 | 1 | defmodule GitGudWeb.SecretsLive.Repo do |
| 2 | 2 | use GitGudWeb, :live_view |
| 3 | 3 | |
| 4 | + require Logger | |
| 5 | + | |
| 4 | 6 | alias GitGud.Repositories |
| 5 | 7 | alias GitGud.Repositories.Storage |
| 6 | 8 | alias GitGud.Secrets |
@@ -41,8 +43,12 @@ defmodule GitGudWeb.SecretsLive.Repo do
| 41 | 43 | defp handle_admin_event("put_secret", %{"name" => name, "value" => value}, socket) |
| 42 | 44 | when is_binary(name) and is_binary(value) and value != "" do |
| 43 | 45 | case Secrets.put_repo_secret(socket.assigns.repo, name, value) do |
| 44 | − {:ok, _} -> {:noreply, socket |> load() |> put_flash(:info, "Secret saved.")} | |
| 45 | − {:error, cs} -> {:noreply, put_flash(socket, :error, error_message(cs))} | |
| 46 | + {:ok, _} -> | |
| 47 | + audit(socket, "set", "secret", name) | |
| 48 | + {:noreply, socket |> load() |> put_flash(:info, "Secret saved.")} | |
| 49 | + | |
| 50 | + {:error, cs} -> | |
| 51 | + {:noreply, put_flash(socket, :error, error_message(cs))} | |
| 46 | 52 | end |
| 47 | 53 | end |
| 48 | 54 |
@@ -51,24 +57,42 @@ defmodule GitGudWeb.SecretsLive.Repo do
| 51 | 57 | |
| 52 | 58 | defp handle_admin_event("delete_secret", %{"name" => name}, socket) when is_binary(name) do |
| 53 | 59 | :ok = Secrets.delete_repo_secret(socket.assigns.repo, name) |
| 60 | + audit(socket, "deleted", "secret", name) | |
| 54 | 61 | {:noreply, socket |> load() |> put_flash(:info, "Removed.")} |
| 55 | 62 | end |
| 56 | 63 | |
| 57 | 64 | defp handle_admin_event("put_var", %{"name" => name, "value" => value}, socket) |
| 58 | 65 | when is_binary(name) and is_binary(value) do |
| 59 | 66 | case Secrets.put_repo_variable(socket.assigns.repo, name, value) do |
| 60 | − {:ok, _} -> {:noreply, socket |> load() |> put_flash(:info, "Variable saved.")} | |
| 61 | − {:error, cs} -> {:noreply, put_flash(socket, :error, error_message(cs))} | |
| 67 | + {:ok, _} -> | |
| 68 | + audit(socket, "set", "variable", name) | |
| 69 | + {:noreply, socket |> load() |> put_flash(:info, "Variable saved.")} | |
| 70 | + | |
| 71 | + {:error, cs} -> | |
| 72 | + {:noreply, put_flash(socket, :error, error_message(cs))} | |
| 62 | 73 | end |
| 63 | 74 | end |
| 64 | 75 | |
| 65 | 76 | defp handle_admin_event("delete_var", %{"name" => name}, socket) when is_binary(name) do |
| 66 | 77 | :ok = Secrets.delete_repo_variable(socket.assigns.repo, name) |
| 78 | + audit(socket, "deleted", "variable", name) | |
| 67 | 79 | {:noreply, socket |> load()} |
| 68 | 80 | end |
| 69 | 81 | |
| 70 | 82 | defp handle_admin_event(_event, _params, socket), do: {:noreply, socket} |
| 71 | 83 | |
| 84 | + # There is no audit table to put this in, so it goes to the log, which | |
| 85 | + # an operator keeps: who changed which name where. Never the value. | |
| 86 | + defp audit(socket, verb, what, name) do | |
| 87 | + user = socket.assigns.current_scope.user | |
| 88 | + | |
| 89 | + Logger.notice( | |
| 90 | + "audit: user #{user.id} (#{user.handle}) #{verb} #{what} #{inspect(name)} on " <> | |
| 91 | + "repository #{Storage.repo_handle(socket.assigns.repo)}/#{socket.assigns.repo.name}", | |
| 92 | + audit: true | |
| 93 | + ) | |
| 94 | + end | |
| 95 | + | |
| 72 | 96 | defp error_message(%Ecto.Changeset{} = cs) do |
| 73 | 97 | cs |
| 74 | 98 | |> Ecto.Changeset.traverse_errors(fn {m, _} -> m end) |
added
test/git_gud_web/live/secrets_audit_log_test.exs
+54
−0
@@ -0,0 +1,54 @@
| 1 | +defmodule GitGudWeb.SecretsAuditLogTest do | |
| 2 | + @moduledoc """ | |
| 3 | + Changing a secret or variable leaves a line in the log naming who, | |
| 4 | + what and where — and never the value. | |
| 5 | + """ | |
| 6 | + use GitGudWeb.ConnCase, async: false | |
| 7 | + | |
| 8 | + import ExUnit.CaptureLog | |
| 9 | + import Phoenix.LiveViewTest | |
| 10 | + import GitGud.ForgeFixtures | |
| 11 | + | |
| 12 | + alias GitGud.Repositories.Storage | |
| 13 | + | |
| 14 | + setup do | |
| 15 | + # The suite logs at :warning; the audit line is a :notice. | |
| 16 | + for mod <- [GitGudWeb.SecretsLive.Repo, GitGudWeb.SecretsLive.Org] do | |
| 17 | + Logger.put_module_level(mod, :notice) | |
| 18 | + end | |
| 19 | + | |
| 20 | + on_exit(fn -> | |
| 21 | + for mod <- [GitGudWeb.SecretsLive.Repo, GitGudWeb.SecretsLive.Org] do | |
| 22 | + Logger.delete_module_level(mod) | |
| 23 | + end | |
| 24 | + end) | |
| 25 | + | |
| 26 | + :ok | |
| 27 | + end | |
| 28 | + | |
| 29 | + test "a repository's secrets and variables", %{conn: conn} do | |
| 30 | + {user, repo} = repository_fixture() | |
| 31 | + conn = log_in_user(conn, user) | |
| 32 | + {:ok, lv, _} = live(conn, "/r/#{Storage.owner_handle(user)}/#{repo.name}/settings/secrets") | |
| 33 | + | |
| 34 | + log = | |
| 35 | + capture_log([level: :notice], fn -> | |
| 36 | + lv | |
| 37 | + |> form("#secret-form", %{"name" => "DEPLOY_KEY", "value" => "hunter2-value"}) | |
| 38 | + |> render_submit() | |
| 39 | + | |
| 40 | + lv | |
| 41 | + |> form("#variable-form", %{"name" => "REGION", "value" => "us-east-plain"}) | |
| 42 | + |> render_submit() | |
| 43 | + | |
| 44 | + render_click(lv, "delete_secret", %{"name" => "DEPLOY_KEY"}) | |
| 45 | + end) | |
| 46 | + | |
| 47 | + assert log =~ ~s(user #{user.id}) | |
| 48 | + assert log =~ ~s(set secret "DEPLOY_KEY" on repository) | |
| 49 | + assert log =~ ~s(set variable "REGION") | |
| 50 | + assert log =~ ~s(deleted secret "DEPLOY_KEY") | |
| 51 | + refute log =~ "hunter2-value" | |
| 52 | + refute log =~ "us-east-plain" | |
| 53 | + end | |
| 54 | +end |
Parents: e999b5e