Introduce alternative form of the account migration message

In the alternative form author can be the new diaspora user
This commit is contained in:
cmrd Senya 2017-10-30 19:52:54 +02:00
parent 69e523abd0
commit 7e2321d6c9
6 changed files with 231 additions and 96 deletions

View file

@ -8,9 +8,16 @@ This entity is sent when a person changes their diaspora* ID (e.g. when a user m
| 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 |
| `author` | [diaspora\* ID][diaspora-id] | The diaspora\* ID of the sender of the entity. The entity may be sent by either old user identity or new user identity. |
| `person` | [Profile][profile] | New profile of a person. |
| `signature` | [Signature][signature] | Signature that validates original and target diaspora* IDs with the private key of the second identity, other than the entity author. So if the author is the old identity then this signature is made with the new identity key, and vice versa. |
## Optional Properties
| Property | Type | Description |
| ----------- | ---------------------------- | ------------------------------------------------------------------------------------ |
| `old_identity` | [diaspora\* ID][diaspora-id] | The diaspora\* ID of the closed account. This field is mandatory if the author of the entity is the new identity. |
### Signature
@ -51,6 +58,7 @@ AccountMigration:old-diaspora-id@example.org:new-diaspora-id@example.com
<signature>
07b1OIY6sTUQwV5pbpgFK0uz6W4cu+oQnlg410Q4uISUOdNOlBdYqhZJm62VFhgvzt4TZXfiJgoupFkRjP0BsaVaZuP2zKMNvO3ngWOeJRf2oRK4Ub5cEA/g7yijkRc+7y8r1iLJ31MFb1czyeCsLxw9Ol8SvAJddogGiLHDhjE=
</signature>
<old_identity>alice@example.org</old_identity>
</account_migration>
~~~

View file

@ -8,7 +8,9 @@ module DiasporaFederation
include AccountMigration::Signable
# @!attribute [r] author
# The old diaspora* ID of the person who changes their ID
# Sender of the AccountMigration message. Usually it is the old diaspora* ID of the person who changes their ID.
# This property is also allowed to be the new diaspora* ID, which is equal to the author of the included
# profile.
# @see Person#author
# @return [String] author diaspora* ID
property :author, :string
@ -19,14 +21,31 @@ module DiasporaFederation
entity :profile, Entities::Profile
# @!attribute [r] signature
# Signature that validates original and target diaspora* IDs with the new key of person
# Signature that validates original and target diaspora* IDs with the private key of the second identity, other
# than the entity author. So if the author is the old identity then this signature is made with the new identity
# key, and vice versa.
# @return [String] signature
property :signature, :string, default: nil
def old_user_id
# @!attribute [r] old_identity
# Optional attribute which keeps old diaspora* ID. Must be present when author attribute contains new diaspora*
# ID.
# @return [String] old identity
property :old_identity, :string, default: nil
# Returns diaspora* ID of the old person identity.
# @return [String] diaspora* ID of the old person identity
def old_identity
return @old_identity if author_is_new_id?
author
end
# Returns diaspora* ID of the new person identity.
# @return [String] diaspora* ID of the new person identity
def new_identity
profile.author
end
# @return [String] string representation of this object
alias to_s unique_migration_descriptor
@ -46,33 +65,33 @@ module DiasporaFederation
private
def new_user_id
profile.author
def author_is_new_id?
author == new_identity
end
def signer_id
new_user_id
author_is_new_id? ? @old_identity : new_identity
end
def enriched_properties
super.tap do |hash|
hash[:signature] = signature || sign_with_new_key
hash[:signature] = signature || sign_with_respective_key
end
end
# Sign with new user's key
# @raise [NewPrivateKeyNotFound] if the new user's private key is not found
# Sign with the key of the #signer_id identity
# @raise [PrivateKeyNotFound] if the signer's private key is not found
# @return [String] A Base64 encoded signature of #signature_data with key
def sign_with_new_key
def sign_with_respective_key
privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key, signer_id)
raise NewPrivateKeyNotFound, "signer=#{signer_id} obj=#{self}" if privkey.nil?
raise PrivateKeyNotFound, "signer=#{signer_id} obj=#{self}" if privkey.nil?
sign_with_key(privkey).tap do
logger.info "event=sign status=complete signature=signature signer=#{signer_id} obj=#{self}"
end
end
# Raised, if creating the signature fails, because the new private key of a user was not found
class NewPrivateKeyNotFound < RuntimeError
class PrivateKeyNotFound < RuntimeError
end
end
end

View file

@ -42,6 +42,7 @@ module DiasporaFederation
Fabricator(:account_migration_entity, class_name: DiasporaFederation::Entities::AccountMigration) do
author { Fabricate.sequence(:diaspora_id) }
profile { Fabricate(:profile_entity) }
old_identity { Fabricate.sequence(:diaspora_id) }
end
Fabricator(:person_entity, class_name: DiasporaFederation::Entities::Person) do

View file

@ -7,6 +7,8 @@ module DiasporaFederation
rule :author, :diaspora_id
rule :profile, :not_nil
rule :old_identity, :diaspora_id
end
end
end

View file

@ -1,19 +1,183 @@
module DiasporaFederation
describe Entities::AccountMigration do
let(:new_diaspora_id) { alice.diaspora_id }
let(:new_author_pkey) { alice.private_key }
let(:hash) {
Fabricate.attributes_for(:account_deletion_entity).merge(
profile: Fabricate(:profile_entity, author: new_diaspora_id)
)
}
let(:old_user) { Fabricate(:user) }
let(:new_user) { Fabricate(:user) }
let(:old_diaspora_id) { old_user.diaspora_id }
let(:new_diaspora_id) { new_user.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(:signature_data) { "AccountMigration:#{old_diaspora_id}:#{new_diaspora_id}" }
let(:string) { signature_data }
shared_examples_for "an account migration entity" do
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, signer_id).and_return(signer_pkey)
entity = Entities::AccountMigration.new(hash)
xml = entity.to_xml
signature = xml.at_xpath("signature").text
expect(verify_signature(signer_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, signer_id).and_return(nil)
expect {
Entities::AccountMigration.new(hash).to_xml
}.to raise_error Entities::AccountMigration::PrivateKeyNotFound
end
end
describe "#verify_signature" do
it "doesn't raise anything if correct signature was passed" do
hash[:signature] = sign_with_key(signer_pkey, signature_data)
expect_callback(:fetch_public_key, signer_id).and_return(signer_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, signer_id).and_return(signer_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, signer_id).and_return(signer_pkey.public_key)
expect {
Entities::AccountMigration.from_hash(hash)
}.to raise_error Entities::AccountMigration::SignatureVerificationFailed
end
end
end
context "with old identity as author" do
let(:signer_id) { new_diaspora_id }
let(:signer_pkey) { new_user.private_key }
let(:hash) {
{
author: old_diaspora_id,
profile: Fabricate(:profile_entity, author: new_diaspora_id),
old_identity: old_diaspora_id
}
}
let(:xml) { <<-XML }
<account_migration>
<author>#{data[:author]}</author>
<profile>
<author>#{data[:profile].author}</author>
<first_name>#{data[:profile].first_name}</first_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>
<bio>#{data[:profile].bio}</bio>
<birthday>#{data[:profile].birthday}</birthday>
<gender>#{data[:profile].gender}</gender>
<location>#{data[:profile].location}</location>
<searchable>#{data[:profile].searchable}</searchable>
<public>#{data[:profile].public}</public>
<nsfw>#{data[:profile].nsfw}</nsfw>
<tag_string>#{data[:profile].tag_string}</tag_string>
</profile>
<signature>#{data[:signature]}</signature>
<old_identity>#{data[:old_identity]}</old_identity>
</account_migration>
XML
it_behaves_like "an account migration entity"
end
context "with new identity as author" do
let(:signer_id) { old_diaspora_id }
let(:signer_pkey) { old_user.private_key }
let(:hash) {
{
author: new_diaspora_id,
profile: Fabricate(:profile_entity, author: new_diaspora_id),
old_identity: old_diaspora_id
}
}
let(:xml) { <<-XML }
<account_migration>
<author>#{data[:author]}</author>
<profile>
<author>#{data[:profile].author}</author>
<first_name>#{data[:profile].first_name}</first_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>
<bio>#{data[:profile].bio}</bio>
<birthday>#{data[:profile].birthday}</birthday>
<gender>#{data[:profile].gender}</gender>
<location>#{data[:profile].location}</location>
<searchable>#{data[:profile].searchable}</searchable>
<public>#{data[:profile].public}</public>
<nsfw>#{data[:profile].nsfw}</nsfw>
<tag_string>#{data[:profile].tag_string}</tag_string>
</profile>
<signature>#{data[:signature]}</signature>
<old_identity>#{data[:old_identity]}</old_identity>
</account_migration>
XML
it_behaves_like "an account migration entity"
end
context "when author is the new identity and old_identity prop is missing" do
let(:signer_id) { old_diaspora_id }
let(:signer_pkey) { old_user.private_key }
let(:hash) {
{
author: new_diaspora_id,
profile: Fabricate(:profile_entity, author: new_diaspora_id)
}
}
let(:xml) { <<-XML }
<account_migration>
@ -37,81 +201,16 @@ module DiasporaFederation
</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)
it "fails validation on construction" do
expect {
Entities::AccountMigration.new(hash).to_xml
}.to raise_error Entities::AccountMigration::NewPrivateKeyNotFound
end
described_class.new(hash)
}.to raise_error Entity::ValidationError
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)
it "fails validation on parsing" do
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
DiasporaFederation::Salmon::XmlPayload.unpack(Nokogiri::XML(xml).root)
}.to raise_error Entity::ValidationError
end
end
end

View file

@ -8,12 +8,18 @@ module DiasporaFederation
let(:property) { :author }
end
describe "#person" do
describe "#profile" do
it_behaves_like "a property with a value validation/restriction" do
let(:property) { :profile }
let(:wrong_values) { [nil] }
let(:correct_values) { [] }
end
end
describe "#old_identity" do
it_behaves_like "a diaspora* ID validator" do
let(:property) { :old_identity }
end
end
end
end