From 1dd6d687a702ec3a85d47ec4350038b800f813e3 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 9 Mar 2026 15:45:23 -0500 Subject: [PATCH] feat: auto-grant superuser to owner email addresses Security-hardened implementation that automatically grants superuser status to graham@towerops.net and graham@mcintire.me during user registration. Security measures: - Only runs during NEW user creation (__meta__.state == :built check) - Cannot be triggered via account updates (prevents privilege escalation) - Case-insensitive comparison (prevents 'Graham@TOWEROPS.NET' bypass) - Email must pass validation and uniqueness constraint first - Explicit defense-in-depth with comprehensive security comments Prevents scenarios where: - Owner loses access to their own system - Need to manually grant superuser via database manipulation - Case variations could bypass the check Test coverage includes: - Both email addresses get superuser - Case variations are handled correctly - Similar/spoofed emails are rejected - Other emails don't get superuser --- lib/towerops/accounts/user.ex | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) 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)