Secrets: changing a secret or variable leaves a line in the log

77def2d · Gabriel Morell · 2026-10-20 18:32

10 files +110 -8
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