From ea2de1f28a1c783c553289637e6d54b6009a501e Mon Sep 17 00:00:00 2001 From: Oneric Date: Sat, 11 Jan 2025 21:25:00 +0100 Subject: [PATCH] signing_key: ensure only one key per user exists Fixes: AkkomaGang/akkoma issue 858 --- lib/pleroma/user/signing_key.ex | 13 +++++-- ...50112000001_signing_key_unique_user_id.exs | 36 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 priv/repo/migrations/20250112000001_signing_key_unique_user_id.exs diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex index 69e13355a..705302013 100644 --- a/lib/pleroma/user/signing_key.ex +++ b/lib/pleroma/user/signing_key.ex @@ -201,13 +201,22 @@ def fetch_remote_key(key_id) do {:ok, user} <- User.get_or_fetch_by_ap_id(ap_id) do Logger.debug("Fetched remote key: #{ap_id}") # store the key - key = %__MODULE__{ + key = %{ user_id: user.id, public_key: public_key_pem, key_id: key_id } - Repo.insert(key, on_conflict: :replace_all, conflict_target: :key_id) + key_cs = + cast(%__MODULE__{}, key, [:user_id, :public_key, :key_id]) + |> unique_constraint(:user_id) + + Repo.insert(key_cs, + # while this should never run for local users anyway, etc make sure we really never loose privkey info! + on_conflict: {:replace_all_except, [:inserted_at, :private_key]}, + # if the key owner overlaps with a distinct existing key entry, this intetionally still errros + conflict_target: :key_id + ) else e -> Logger.debug("Failed to fetch remote key: #{inspect(e)}") diff --git a/priv/repo/migrations/20250112000001_signing_key_unique_user_id.exs b/priv/repo/migrations/20250112000001_signing_key_unique_user_id.exs new file mode 100644 index 000000000..00a716db8 --- /dev/null +++ b/priv/repo/migrations/20250112000001_signing_key_unique_user_id.exs @@ -0,0 +1,36 @@ +defmodule Pleroma.Repo.Migrations.SigningKeyUniqueUserId do + use Ecto.Migration + + import Ecto.Query + + def up() do + # If dupes exists for any local user we do NOT want to delete the genuine privkey alongside the fake. + # Instead just filter out anything pertaining to local users, if dupes exists manual intervention + # is required anyway and index creation will just fail later (check against legacy field in users table) + dupes = + Pleroma.User.SigningKey + |> join(:inner, [s], u in Pleroma.User, on: s.user_id == u.id) + |> group_by([s], s.user_id) + |> having([], count() > 1) + |> having([_s, u], not fragment("bool_or(?)", u.local)) + |> select([s], s.user_id) + + # Delete existing remote duplicates + # they’ll be reinserted on the next user update + # or proactively fetched when receiving a signature from it + Pleroma.User.SigningKey + |> where([s], s.user_id in subquery(dupes)) + |> Pleroma.Repo.delete_all() + + drop_if_exists(index(:signing_keys, [:user_id])) + + create_if_not_exists( + index(:signing_keys, [:user_id], name: :signing_keys_user_id_index, unique: true) + ) + end + + def down() do + drop_if_exists(index(:signing_keys, [:user_id])) + create_if_not_exists(index(:signing_keys, [:user_id], name: :signing_keys_user_id_index)) + end +end