From 92350173b09ce98a90c97a9f4aad0e607216dc26 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 1 Feb 2026 14:09:40 -0600 Subject: [PATCH] feat: add audit logging to grant_sudo_mode/1 --- lib/towerops/accounts.ex | 17 +++++++++--- lib/towerops/admin/audit_log.ex | 3 ++- .../accounts/grant_sudo_mode_test.exs | 26 +++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/lib/towerops/accounts.ex b/lib/towerops/accounts.ex index d8cbbce5..0131e350 100644 --- a/lib/towerops/accounts.ex +++ b/lib/towerops/accounts.ex @@ -15,6 +15,7 @@ defmodule Towerops.Accounts do alias Towerops.Accounts.UserRecoveryCode alias Towerops.Accounts.UserToken alias Towerops.Accounts.UserTotpDevice + alias Towerops.Admin.AuditLogger alias Towerops.GeoIP alias Towerops.Repo @@ -633,9 +634,19 @@ defmodule Towerops.Accounts do def grant_sudo_mode(%User{} = user) do now = DateTime.utc_now() - user - |> User.sudo_changeset(%{last_sudo_at: now}) - |> Repo.update() + with {:ok, _updated_user} = result <- + user + |> User.sudo_changeset(%{last_sudo_at: now}) + |> Repo.update() do + # Log sudo mode grant for security auditing + AuditLogger.log_event("sudo_mode_granted", nil, + actor_id: user.id, + target_user_id: user.id, + metadata: %{granted_at: now} + ) + + result + end end @doc """ diff --git a/lib/towerops/admin/audit_log.ex b/lib/towerops/admin/audit_log.ex index ee5169e3..c6cfb0a5 100644 --- a/lib/towerops/admin/audit_log.ex +++ b/lib/towerops/admin/audit_log.ex @@ -61,7 +61,8 @@ defmodule Towerops.Admin.AuditLog do "monitoring_data_queried", # Security events "failed_access_attempt", - "privilege_escalation" + "privilege_escalation", + "sudo_mode_granted" ]) end end diff --git a/test/towerops/accounts/grant_sudo_mode_test.exs b/test/towerops/accounts/grant_sudo_mode_test.exs index e3451a27..b265b1b4 100644 --- a/test/towerops/accounts/grant_sudo_mode_test.exs +++ b/test/towerops/accounts/grant_sudo_mode_test.exs @@ -5,6 +5,7 @@ defmodule Towerops.Accounts.GrantSudoModeTest do alias Towerops.Accounts alias Towerops.Accounts.User + alias Towerops.Admin describe "grant_sudo_mode/1" do test "updates user's last_sudo_at timestamp" do @@ -40,5 +41,30 @@ defmodule Towerops.Accounts.GrantSudoModeTest do refute changeset.valid? assert "can't be blank" in errors_on(changeset).last_sudo_at end + + test "creates audit log entry when sudo mode is granted" do + user = user_fixture() + + # Verify no audit logs exist before + logs_before = Admin.list_audit_logs_for_user(user.id) + assert Enum.empty?(logs_before) + + # Grant sudo mode + {:ok, updated_user} = Accounts.grant_sudo_mode(user) + + # Verify audit log was created + logs_after = Admin.list_audit_logs_for_user(user.id) + assert length(logs_after) == 1 + + [log] = logs_after + assert log.action == "sudo_mode_granted" + assert log.superuser_id == user.id + assert log.target_user_id == user.id + assert log.metadata["granted_at"] + + # Verify the granted_at timestamp matches the user's last_sudo_at + granted_at = log.metadata["granted_at"] |> DateTime.from_iso8601() |> elem(1) + assert DateTime.diff(granted_at, updated_user.last_sudo_at, :second) == 0 + end end end