diff --git a/lib/towerops/accounts/user.ex b/lib/towerops/accounts/user.ex index 7656f9e0..b86329b0 100644 --- a/lib/towerops/accounts/user.ex +++ b/lib/towerops/accounts/user.ex @@ -150,6 +150,7 @@ defmodule Towerops.Accounts.User do |> validate_timezone() |> validate_length(:first_name, max: 100) |> validate_length(:last_name, max: 100) + |> maybe_grant_superuser() end defp validate_consent(changeset) do @@ -162,6 +163,32 @@ defmodule Towerops.Accounts.User do ) end + # Automatically grant superuser status to specific trusted email addresses. + # + # SECURITY MEASURES: + # 1. Only runs during NEW user creation (checks __meta__.state == :built) + # 2. Email must pass validation and uniqueness constraint before reaching here + # 3. Case-insensitive comparison prevents "Graham@towerops.net" bypass attempts + # 4. Cannot be triggered via account updates (prevents privilege escalation) + # + # This ensures only legitimate registrations with exact email addresses gain superuser access. + defp maybe_grant_superuser(changeset) do + # Only grant superuser on NEW user creation, never on updates + if changeset.data.__meta__.state == :built do + email = get_field(changeset, :email) + + # Case-insensitive comparison - defense against "Graham@TOWEROPS.net" variants + if email && String.downcase(email) in ["graham@towerops.net", "graham@mcintire.me"] do + put_change(changeset, :is_superuser, true) + else + changeset + end + else + # This is an update to an existing user - never grant superuser via update + changeset + end + end + defp validate_timezone(changeset) do validate_change(changeset, :timezone, fn :timezone, timezone -> validate_timezone_value(timezone)