AccountMigration entity

A new entity to use when diaspora ID of a person has changed for some
reason (e.g. account migration, pod hostname change, etc)
This commit is contained in:
cmrd Senya 2017-01-31 20:19:01 +02:00
parent f23f4c0ea4
commit 626e59c68d
8 changed files with 293 additions and 0 deletions

View file

@ -0,0 +1,59 @@
---
title: AccountMigration
---
This entity is sent when a person changes their diaspora* ID (e.g. when a user migration from one to another pod happens).
## Properties
| Property | Type | Description |
| ----------- | ---------------------------- | ------------------------------------------------------------------------------------ |
| `author` | [diaspora\* ID][diaspora-id] | The diaspora\* ID of the closed account. |
| `person` | [Profile][profile] | New profile of a person |
| `signature` | [Signature][signature] | Signature that validates original and target diaspora* IDs with the new key of person |
### Signature
The signature base string is produced by concatenating the following substrings together, separated by semicolon (`:`):
1) The entity name specifier: `AccountMigration`.
2) diaspora\* ID of the closed account (old diaspora\* ID).
3) diaspora\* ID of the replacement account (new diaspora\* ID).
Example of a string:
~~~
AccountMigration:old-diaspora-id@example.org:new-diaspora-id@example.com
~~~
## Example
~~~xml
<account_migration>
<author>alice@example.org</author>
<profile>
<author>alice@newpod.example.net</author>
<first_name>my name</first_name>
<last_name/>
<image_url>/assets/user/default.png</image_url>
<image_url_medium>/assets/user/default.png</image_url_medium>
<image_url_small>/assets/user/default.png</image_url_small>
<birthday>1988-07-15</birthday>
<gender>Female</gender>
<bio>some text about me</bio>
<location>github</location>
<searchable>true</searchable>
<nsfw>false</nsfw>
<tag_string>#i #love #tags</tag_string>
</profile>
<signature>
07b1OIY6sTUQwV5pbpgFK0uz6W4cu+oQnlg410Q4uISUOdNOlBdYqhZJm62VFhgvzt4TZXfiJgoupFkRjP0BsaVaZuP2zKMNvO3ngWOeJRf2oRK4Ub5cEA/g7yijkRc+7y8r1iLJ31MFb1czyeCsLxw9Ol8SvAJddogGiLHDhjE=
</signature>
</account_migration>
~~~
[diaspora-id]: {{ site.baseurl }}/federation/types.html#diaspora-id
[profile]: {{ site.baseurl }}/entities/profile.html
[signature]: {{ site.baseurl }}/federation/types.html#signature

View file

@ -20,6 +20,7 @@ require "diaspora_federation/entities/profile"
require "diaspora_federation/entities/person"
require "diaspora_federation/entities/contact"
require "diaspora_federation/entities/account_deletion"
require "diaspora_federation/entities/account_migration"
require "diaspora_federation/entities/participation"
require "diaspora_federation/entities/like"

View file

@ -0,0 +1,74 @@
module DiasporaFederation
module Entities
# This entity is sent when a person changes their diaspora* ID (e.g. when a user migration
# from one to another pod happens).
#
# @see Validators::AccountMigrationValidator
class AccountMigration < Entity
include Signable
# @!attribute [r] author
# The old diaspora* ID of the person who changes their ID
# @see Person#author
# @return [String] author diaspora* ID
property :author, :string
# @!attribute [r] profile
# Holds new updated profile of a person, including diaspora* ID
# @return [Person] person new data
entity :profile, Entities::Profile
# @!attribute [r] signature
# Signature that validates original and target diaspora* IDs with the new key of person
# @return [String] signature
property :signature, :string, default: nil
# @return [String] string representation of this object
def to_s
"AccountMigration:#{author}:#{profile.author}"
end
# Shortcut for calling super method with sensible arguments
#
# @see DiasporaFederation::Entities::Signable#verify_signature
def verify_signature
super(profile.author, :signature)
end
# Calls super and additionally does signature verification for the instantiated entity.
#
# @see DiasporaFederation::Entity.from_hash
def self.from_hash(*args)
super.tap(&:verify_signature)
end
private
# @see DiasporaFederation::Entities::Signable#signature_data
def signature_data
to_s
end
def enriched_properties
super.tap do |hash|
hash[:signature] = signature || sign_with_new_key
end
end
# Sign with new user's key
# @raise [NewPrivateKeyNotFound] if the new user's private key is not found
# @return [String] A Base64 encoded signature of #signature_data with key
def sign_with_new_key
privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key, profile.author)
raise NewPrivateKeyNotFound, "author=#{profile.author} obj=#{self}" if privkey.nil?
sign_with_key(privkey).tap do
logger.info "event=sign status=complete signature=signature author=#{profile.author} obj=#{self}"
end
end
# Raised, if creating the signature fails, because the new private key of a user was not found
class NewPrivateKeyNotFound < RuntimeError
end
end
end
end

View file

@ -41,6 +41,13 @@ module DiasporaFederation
searchable true
end
factory :account_migration_entity, class: DiasporaFederation::Entities::AccountMigration do
author { generate(:diaspora_id) }
profile {
FactoryGirl.build(:profile_entity)
}
end
factory :person_entity, class: DiasporaFederation::Entities::Person do
guid
author { generate(:diaspora_id) }

View file

@ -41,6 +41,7 @@ require "diaspora_federation/validators/relayable_validator"
# types
require "diaspora_federation/validators/account_deletion_validator"
require "diaspora_federation/validators/account_migration_validator"
require "diaspora_federation/validators/comment_validator"
require "diaspora_federation/validators/contact_validator"
require "diaspora_federation/validators/conversation_validator"

View file

@ -0,0 +1,12 @@
module DiasporaFederation
module Validators
# This validates a {Entities::AccountMigration}.
class AccountMigrationValidator < Validation::Validator
include Validation
rule :author, %i(not_empty diaspora_id)
rule :profile, :not_nil
end
end
end

View file

@ -0,0 +1,119 @@
module DiasporaFederation
describe Entities::AccountMigration do
let(:new_diaspora_id) { alice.diaspora_id }
let(:new_author_pkey) { alice.private_key }
let(:hash) {
FactoryGirl.attributes_for(
:account_migration_entity,
profile: FactoryGirl.build(:profile_entity, author: new_diaspora_id)
)
}
let(:data) {
hash.tap {|hash|
properties = described_class.new(hash).send(:enriched_properties)
hash[:signature] = properties[:signature]
}
}
let(:signature_data) { "AccountMigration:#{hash[:author]}:#{new_diaspora_id}" }
let(:xml) { <<-XML }
<account_migration>
<author>#{data[:author]}</author>
<profile>
<diaspora_handle>#{data[:profile].author}</diaspora_handle>
<first_name>#{data[:profile].first_name}</first_name>
<last_name/>
<image_url>#{data[:profile].image_url}</image_url>
<image_url_medium>#{data[:profile].image_url}</image_url_medium>
<image_url_small>#{data[:profile].image_url}</image_url_small>
<birthday>#{data[:profile].birthday}</birthday>
<gender>#{data[:profile].gender}</gender>
<bio>#{data[:profile].bio}</bio>
<location>#{data[:profile].location}</location>
<searchable>#{data[:profile].searchable}</searchable>
<nsfw>#{data[:profile].nsfw}</nsfw>
<tag_string>#{data[:profile].tag_string}</tag_string>
</profile>
<signature>#{data[:signature]}</signature>
</account_migration>
XML
let(:string) { "AccountMigration:#{data[:author]}:#{data[:profile].author}" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity"
describe "#to_xml" do
it "computes signature when no signature was provided" do
expect_callback(:fetch_private_key, new_diaspora_id).and_return(new_author_pkey)
entity = Entities::AccountMigration.new(hash)
xml = entity.to_xml
signature = xml.at_xpath("signature").text
expect(verify_signature(new_author_pkey, signature, entity.to_s)).to be_truthy
end
it "doesn't change signature if it is already set" do
hash[:signature] = "aa"
xml = Entities::AccountMigration.new(hash).to_xml
expect(xml.at_xpath("signature").text).to eq("aa")
end
it "raises when signature isn't set and key isn't supplied" do
expect_callback(:fetch_private_key, new_diaspora_id).and_return(nil)
expect {
Entities::AccountMigration.new(hash).to_xml
}.to raise_error Entities::AccountMigration::NewPrivateKeyNotFound
end
end
describe "#verify_signature" do
it "doesn't raise anything if correct signatures were passed" do
hash[:signature] = sign_with_key(new_author_pkey, signature_data)
expect_callback(:fetch_public_key, new_diaspora_id).and_return(new_author_pkey)
expect { Entities::AccountMigration.new(hash).verify_signature }.not_to raise_error
end
it "raises when no public key for author was fetched" do
expect_callback(:fetch_public_key, anything).and_return(nil)
expect {
Entities::AccountMigration.new(hash).verify_signature
}.to raise_error Entities::AccountMigration::PublicKeyNotFound
end
it "raises when bad author signature was passed" do
hash[:signature] = "abcdef"
expect_callback(:fetch_public_key, new_diaspora_id).and_return(new_author_pkey.public_key)
expect {
Entities::AccountMigration.new(hash).verify_signature
}.to raise_error Entities::AccountMigration::SignatureVerificationFailed
end
end
describe ".from_hash" do
it "calls #verify_signature" do
expect_any_instance_of(Entities::AccountMigration).to receive(:freeze)
expect_any_instance_of(Entities::AccountMigration).to receive(:verify_signature)
Entities::AccountMigration.from_hash(hash)
end
it "raises when bad author signature was passed" do
hash[:signature] = "abcdef"
expect_callback(:fetch_public_key, new_diaspora_id).and_return(new_author_pkey.public_key)
expect {
Entities::AccountMigration.from_hash(hash)
}.to raise_error Entities::AccountMigration::SignatureVerificationFailed
end
end
end
end

View file

@ -0,0 +1,20 @@
module DiasporaFederation
describe Validators::AccountMigrationValidator do
let(:entity) { :account_migration_entity }
it_behaves_like "a common validator"
it_behaves_like "a diaspora* ID validator" do
let(:property) { :author }
let(:mandatory) { true }
end
describe "#person" do
it_behaves_like "a property with a value validation/restriction" do
let(:property) { :profile }
let(:wrong_values) { [nil] }
let(:correct_values) { [] }
end
end
end
end