Unverified Commit 45619cb1 authored by cmrd Senya's avatar cmrd Senya
Browse files

Account migration model and message support

This commit introduces support for AccountMigration federation message
receive. It covers the cases when the new home pod for a user is remote
respective to the recepient pod of the message. It also allows to initiate
migration locally by a podmin from the rails console. This will give the
pods a possibility to understand the account migration event on the
federation level and thus future version which will implement migration
will be backward compatible with the pods starting from this commit.
parent e2979df6
Loading
Loading
Loading
Loading
+165 −0
Original line number Diff line number Diff line
class AccountMigration < ApplicationRecord
  include Diaspora::Federated::Base

  belongs_to :old_person, class_name: "Person"
  belongs_to :new_person, class_name: "Person"

  validates :old_person, uniqueness: true
  validates :new_person, uniqueness: true

  after_create :lock_old_user!

  attr_accessor :old_private_key

  def receive(*)
    perform!
  end

  def public?
    true
  end

  def sender
    @sender ||= old_user || ephemeral_sender
  end

  # executes a migration plan according to this AccountMigration object
  def perform!
    raise "already performed" if performed?

    ActiveRecord::Base.transaction do
      account_deleter.tombstone_person_and_profile
      account_deleter.close_user if user_left_our_pod?
      account_deleter.tombstone_user if user_changed_id_locally?

      update_all_references
    end

    dispatch if locally_initiated?
    dispatch_contacts if remotely_initiated?
  end

  def performed?
    old_person.closed_account?
  end

  # We assume that migration message subscribers are people that are subscribed to a new user profile updates.
  # Since during the migration we update contact references, this includes all the contacts of the old person.
  # In case when a user migrated to our pod from a remote one, we include remote person to subscribers so that
  # the new pod is informed about the migration as well.
  def subscribers
    new_user.profile.subscribers.remote.to_a.tap do |subscribers|
      subscribers.push(old_person) if old_person.remote?
    end
  end

  private

  # Normally pod initiates migration locally when the new user is local. Then the pod creates AccountMigration object
  # itself. If new user is remote, then AccountMigration object is normally received via the federation and this is
  # remote initiation then.
  def remotely_initiated?
    new_person.remote?
  end

  def locally_initiated?
    !remotely_initiated?
  end

  def old_user
    old_person.owner
  end

  def new_user
    new_person.owner
  end

  def lock_old_user!
    old_user&.lock_access!
  end

  def user_left_our_pod?
    old_user && !new_user
  end

  def user_changed_id_locally?
    old_user && new_user
  end

  # We need to resend contacts of users of our pod for the remote new person so that the remote pod received this
  # contact information from the authoritative source.
  def dispatch_contacts
    new_person.contacts.sharing.each do |contact|
      Diaspora::Federation::Dispatcher.defer_dispatch(contact.user, contact)
    end
  end

  def dispatch
    Diaspora::Federation::Dispatcher.build(sender, self).dispatch
  end

  EphemeralUser = Struct.new(:diaspora_handle, :serialized_private_key) do
    def id
      diaspora_handle
    end

    def encryption_key
      OpenSSL::PKey::RSA.new(serialized_private_key)
    end
  end

  def ephemeral_sender
    raise "can't build sender without old private key defined" if old_private_key.nil?
    EphemeralUser.new(old_person.diaspora_handle, old_private_key)
  end

  def update_all_references
    update_person_references
    update_user_references if user_changed_id_locally?
  end

  def person_references
    references = Person.reflections.reject {|key, _|
      %w[profile owner notifications pod].include?(key)
    }

    references.map {|key, value|
      {value.foreign_key => key}
    }
  end

  def user_references
    references = User.reflections.reject {|key, _|
      %w[
        person profile auto_follow_back_aspect invited_by aspect_memberships contact_people followed_tags
        ignored_people conversation_visibilities pairwise_pseudonymous_identifiers conversations o_auth_applications
      ].include?(key)
    }

    references.map {|key, value|
      {value.foreign_key => key}
    }
  end

  def update_person_references
    logger.debug "Updating references from person id=#{old_person.id} to person id=#{new_person.id}"
    update_references(person_references, old_person, new_person.id)
  end

  def update_user_references
    logger.debug "Updating references from user id=#{old_user.id} to user id=#{new_user.id}"
    update_references(user_references, old_user, new_user.id)
  end

  def update_references(references, object, new_id)
    references.each do |pair|
      key_id = pair.flatten[0]
      association = pair.flatten[1]
      object.send(association).update_all(key_id => new_id)
    end
  end

  def account_deleter
    @account_deleter ||= AccountDeleter.new(old_person)
  end
end
+4 −6
Original line number Diff line number Diff line
@@ -40,7 +40,10 @@ class Person < ApplicationRecord
  has_many :likes, foreign_key: :author_id, dependent: :destroy # This person's own likes
  has_many :participations, :foreign_key => :author_id, :dependent => :destroy
  has_many :poll_participations, foreign_key: :author_id, dependent: :destroy
  has_many :conversation_visibilities
  has_many :conversation_visibilities, dependent: :destroy
  has_many :messages, foreign_key: :author_id, dependent: :destroy
  has_many :conversations, foreign_key: :author_id, dependent: :destroy
  has_many :blocks, dependent: :destroy

  has_many :roles

@@ -307,11 +310,6 @@ class Person < ApplicationRecord
    serialized_public_key
  end

  def exported_key= new_key
    raise "Don't change a key" if serialized_public_key
    serialized_public_key = new_key
  end

  # discovery (webfinger)
  def self.find_or_fetch_by_identifier(diaspora_id)
    # exiting person?
+1 −0
Original line number Diff line number Diff line
@@ -126,6 +126,7 @@ class Profile < ApplicationRecord
  end

  def tombstone!
    @tag_string = nil
    self.taggings.delete_all
    clearable_fields.each do |field|
      self[field] = nil
+2 −0
Original line number Diff line number Diff line
@@ -54,6 +54,8 @@ class User < ApplicationRecord
  belongs_to :auto_follow_back_aspect, class_name: "Aspect", optional: true
  belongs_to :invited_by, class_name: "User", optional: true

  has_many :invited_users, class_name: "User", inverse_of: :invited_by, foreign_key: :invited_by_id

  has_many :aspect_memberships, :through => :aspects

  has_many :contacts
+14 −0
Original line number Diff line number Diff line
class CreateAccountMigrations < ActiveRecord::Migration[5.1]
  def change
    create_table :account_migrations do |t|
      t.integer :old_person_id, null: false
      t.integer :new_person_id, null: false
    end

    add_foreign_key :account_migrations, :people, column: :old_person_id
    add_foreign_key :account_migrations, :people, column: :new_person_id

    add_index :account_migrations, %i[old_person_id new_person_id], unique: true
    add_index :account_migrations, :old_person_id, unique: true
  end
end
Loading