Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: diaspora/diaspora
...
head fork: diaspora/diaspora
Checking mergeability… Don't worry, you can still create the pull request.
  • 2 commits
  • 24 files changed
  • 0 commit comments
  • 1 contributor
Commits on May 18, 2012
@maxwell maxwell Refactored receivers to use new Federated::Validator objects. Now there
is a decent phase for validation of federated objects, after which they
are ready to be persisted to the database.  Some slight modifications to
behavior however, now as the model validations which are checked now
happen earlier in the phase.  This means more execeptions are thrown
currently as to maintain the most backwards compatibiltiy with the
current behavior, we are raising on federation failure events.  A next
refactor would involve making 'expected' exceptions less exceptional and
being less noisy.
d229a3a
@maxwell maxwell dont override that stuff d45ca19
View
3  app/models/conversation.rb
@@ -2,6 +2,7 @@ class Conversation < ActiveRecord::Base
include Diaspora::Federated::Base
include Diaspora::Guid
+
xml_attr :subject
xml_attr :created_at
xml_attr :messages, :as => [Message]
@@ -66,4 +67,6 @@ def receive(user, person)
Notification.notify(user, received_msg, person) if msg.respond_to?(:notification_type)
end
end
+
+ include Diaspora::Federated::Lint # unless Rails.env.production?
end
View
2  app/models/message.rb
@@ -3,6 +3,8 @@ class Message < ActiveRecord::Base
include Diaspora::Federated::Base
include Diaspora::Guid
include Diaspora::Relayable
+ include Diaspora::Federated::Lint # unless Rails.env.production?
+
xml_attr :text
xml_attr :created_at
View
1  app/models/person.rb
@@ -6,6 +6,7 @@
require File.join(Rails.root, 'lib/hcard')
class Person < ActiveRecord::Base
+ #this is special, as every request contains a wrapped person, but we dont federate a person directly
include ROXML
include Encryptor::Public
include Diaspora::Guid
View
10 app/models/profile.rb
@@ -44,6 +44,12 @@ class Profile < ActiveRecord::Base
:image_url_small, :birthday, :gender, :bio, :location, :searchable, :date, :tag_string, :nsfw
belongs_to :person
+
+
+
+ #FEDERATION HAX
+ alias :author :person
+
before_validation do
self.tag_string = self.tag_string.split[0..4].join(' ')
end
@@ -181,6 +187,8 @@ def valid_birthday
end
end
+ include Diaspora::Federated::Lint # unless Rails.env.production?
+
private
def clearable_fields
self.attributes.keys - Profile.protected_attributes.to_a - ["created_at", "updated_at", "person_id"]
@@ -191,4 +199,4 @@ def absolutify_local_url url
pod_url.chop! if AppConfig[:pod_url][-1,1] == '/'
"#{pod_url}#{url}"
end
-end
+end
View
13 app/models/request.rb
@@ -5,10 +5,17 @@
class Request
include Diaspora::Federated::Base
- include ActiveModel::Validations
-
+
attr_accessor :sender, :recipient, :aspect
+
+
+ #FEDERATION HAX
+ alias :author :sender
+
+
+
+
xml_accessor :sender_handle
xml_accessor :recipient_handle
@@ -99,4 +106,6 @@ def not_friending_yourself
errors[:base] << 'You can not friend yourself'
end
end
+
+ include Diaspora::Federated::Lint # unless Rails.env.production?
end
View
38 app/models/retraction.rb
@@ -8,11 +8,17 @@ class Retraction
xml_accessor :post_guid
xml_accessor :diaspora_handle
xml_accessor :type
-
attr_accessor :person, :object, :subscribers
+ validates :target, :presence => true
+ validate :sender_is_the_one_being_retracted, :if => :person_retraction?
+ validate :retractor_controls_target, :unless => :person_retraction?
+
+ #FEDERATION HAX
+ alias :author :person
+
def subscribers(user)
- unless self.type == 'Person'
+ unless person_retraction?
@subscribers ||= self.object.subscribers(user)
@subscribers -= self.object.resharers unless self.object.is_a?(Photo)
@subscribers
@@ -48,17 +54,31 @@ def perform receiving_user
end
def receive(user, person)
- if self.type == 'Person'
- unless self.person.guid.to_s == self.post_guid.to_s
- Rails.logger.info("event=receive status=abort reason='sender is not the person he is trying to retract' recipient=#{self.diaspora_handle} sender=#{self.person.diaspora_handle} payload_type=#{self.class} retraction_type=person")
- return
- end
+ if person_retraction?
user.disconnected_by(self.target)
- elsif self.target.nil? || self.target.author != self.person
- Rails.logger.info("event=retraction status=abort reason='no post found authored by retractor' sender=#{person.diaspora_handle} post_guid=#{post_guid}")
else
self.perform(user)
end
self
end
+
+ def person_retraction?
+ self.type == 'Person'
+ end
+
+ def sender_is_the_one_being_retracted
+ unless self.person.guid.to_s == self.post_guid.to_s
+ Rails.logger.info("event=receive status=abort reason='sender is not the person he is trying to retract' recipient=#{self.diaspora_handle} sender=#{self.person.diaspora_handle} payload_type=#{self.class} retraction_type=person")
+ errors.add :base, "Retractor does not control target"
+ end
+ end
+
+ def retractor_controls_target
+ unless self.target.present? && self.target.author == self.person
+ Rails.logger.info("event=retraction status=abort reason='no post found authored by retractor' sender=#{person.diaspora_handle} post_guid=#{post_guid}")
+ errors.add :base, "Retractor does not control target"
+ end
+ end
+
+ include Diaspora::Federated::Lint # unless Rails.env.production?
end
View
1  app/models/status_message.rb
@@ -7,6 +7,7 @@ class StatusMessage < Post
include ActionView::Helpers::TextHelper
include PeopleHelper
+ include Diaspora::Federated::Lint # unless Rails.env.production?
acts_as_taggable_on :tags
extract_tags_from :raw_message
View
4 app/models/user.rb
@@ -263,7 +263,9 @@ def build_post(class_name, opts={})
opts[:diaspora_handle] = opts[:author].diaspora_handle
model_class = class_name.to_s.camelize.constantize
- model_class.diaspora_initialize(opts)
+ object = model_class.diaspora_initialize(opts)
+ object.set_guid
+ object
end
def dispatch_post(post, opts={})
View
2  config/application.rb
@@ -38,6 +38,8 @@ class Application < Rails::Application
config.autoload_paths += %W(#{config.root}/lib/*)
config.autoload_paths += %W(#{config.root}/lib/*/*)
config.autoload_paths += %W(#{config.root}/lib/*/*/*)
+ config.autoload_paths += %W(#{config.root}/lib/**/**/*)
+ config.autoload_paths += %W(#{config.root}/lib/**/**/**/*)
# Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named
View
100 db/schema.rb
@@ -21,8 +21,8 @@
create_table "aspect_memberships", :force => true do |t|
t.integer "aspect_id", :null => false
t.integer "contact_id", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
add_index "aspect_memberships", ["aspect_id", "contact_id"], :name => "index_aspect_memberships_on_aspect_id_and_contact_id", :unique => true
@@ -32,8 +32,8 @@
create_table "aspect_visibilities", :force => true do |t|
t.integer "shareable_id", :null => false
t.integer "aspect_id", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.string "shareable_type", :default => "Post", :null => false
end
@@ -44,8 +44,8 @@
create_table "aspects", :force => true do |t|
t.string "name", :null => false
t.integer "user_id", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.boolean "contacts_visible", :default => true, :null => false
t.integer "order_id"
end
@@ -65,8 +65,8 @@
t.string "guid", :null => false
t.text "author_signature"
t.text "parent_author_signature"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.integer "likes_count", :default => 0, :null => false
t.string "commentable_type", :limit => 60, :default => "Post", :null => false
end
@@ -78,8 +78,8 @@
create_table "contacts", :force => true do |t|
t.integer "user_id", :null => false
t.integer "person_id", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.boolean "sharing", :default => false, :null => false
t.boolean "receiving", :default => false, :null => false
end
@@ -91,8 +91,8 @@
t.integer "conversation_id", :null => false
t.integer "person_id", :null => false
t.integer "unread", :default => 0, :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
add_index "conversation_visibilities", ["conversation_id", "person_id"], :name => "index_conversation_visibilities_usefully", :unique => true
@@ -103,8 +103,8 @@
t.string "subject"
t.string "guid", :null => false
t.integer "author_id", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
add_index "conversations", ["author_id"], :name => "conversations_author_id_fk"
@@ -113,8 +113,8 @@
t.string "token"
t.integer "user_id"
t.integer "count"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
create_table "invitations", :force => true do |t|
@@ -122,8 +122,8 @@
t.integer "sender_id"
t.integer "recipient_id"
t.integer "aspect_id"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.string "service"
t.string "identifier"
t.boolean "admin", :default => false
@@ -141,8 +141,8 @@
t.string "guid"
t.text "author_signature"
t.text "parent_author_signature"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.string "target_type", :limit => 60, :null => false
end
@@ -165,8 +165,8 @@
t.integer "author_id", :null => false
t.string "guid", :null => false
t.text "text", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.text "author_signature"
t.text "parent_author_signature"
end
@@ -177,8 +177,8 @@
create_table "notification_actors", :force => true do |t|
t.integer "notification_id"
t.integer "person_id"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
add_index "notification_actors", ["notification_id", "person_id"], :name => "index_notification_actors_on_notification_id_and_person_id", :unique => true
@@ -190,8 +190,8 @@
t.integer "target_id"
t.integer "recipient_id", :null => false
t.boolean "unread", :default => true, :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.string "type"
end
@@ -213,8 +213,8 @@
t.integer "author_id"
t.text "author_signature"
t.text "parent_author_signature"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
add_index "participations", ["guid"], :name => "index_participations_on_guid"
@@ -226,8 +226,8 @@
t.string "diaspora_handle", :null => false
t.text "serialized_public_key", :null => false
t.integer "owner_id"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.boolean "closed_account", :default => false
end
@@ -261,8 +261,8 @@
create_table "pods", :force => true do |t|
t.string "host"
t.boolean "ssl"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
create_table "posts", :force => true do |t|
@@ -277,8 +277,8 @@
t.string "remote_photo_name"
t.string "random_string"
t.string "processed_image"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.string "unprocessed_image"
t.string "object_url"
t.string "image_url"
@@ -319,8 +319,8 @@
t.text "bio"
t.boolean "searchable", :default => true, :null => false
t.integer "person_id", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.string "location"
t.string "full_name", :limit => 70
t.boolean "nsfw", :default => false
@@ -338,8 +338,8 @@
t.string "table"
t.integer "month", :limit => 2
t.integer "year", :limit => 8
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
add_index "rails_admin_histories", ["item", "table", "month", "year"], :name => "index_rails_admin_histories"
@@ -347,8 +347,8 @@
create_table "roles", :force => true do |t|
t.integer "person_id"
t.string "name"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
create_table "services", :force => true do |t|
@@ -358,8 +358,8 @@
t.string "access_token"
t.string "access_secret"
t.string "nickname"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
add_index "services", ["type", "uid"], :name => "index_services_on_type_and_uid"
@@ -367,8 +367,8 @@
create_table "share_visibilities", :force => true do |t|
t.integer "shareable_id", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.boolean "hidden", :default => false, :null => false
t.integer "contact_id", :null => false
t.string "shareable_type", :limit => 60, :default => "Post", :null => false
@@ -382,8 +382,8 @@
create_table "tag_followings", :force => true do |t|
t.integer "tag_id", :null => false
t.integer "user_id", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
add_index "tag_followings", ["tag_id", "user_id"], :name => "index_tag_followings_on_tag_id_and_user_id", :unique => true
@@ -414,8 +414,8 @@
create_table "user_preferences", :force => true do |t|
t.string "email_type"
t.integer "user_id"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
create_table "users", :force => true do |t|
@@ -436,8 +436,8 @@
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.string "invitation_service", :limit => 127
t.string "invitation_identifier", :limit => 127
t.integer "invitation_limit"
View
16 lib/diaspora/federated/base.rb
@@ -17,19 +17,27 @@ module Base
def self.included(model)
model.instance_eval do
include ROXML
+ include ActiveModel::Validations
+
include Diaspora::Federated::Base::InstanceMethods
+
+ #requires author
end
end
module InstanceMethods
def to_diaspora_xml
<<-XML
- <XML>
- <post>#{to_xml.to_s}</post>
- </XML>
- XML
+ <XML>
+ <post>#{to_xml.to_s}</post>
+ </XML>
+ XML
end
+ # def author
+ # raise 'You must override author in order to enable federation on this model'
+ # end
+
def x(input)
input.to_s.to_xs
end
View
72 lib/diaspora/federated/lint.rb
@@ -0,0 +1,72 @@
+module Diaspora
+ module Federated
+ module Lint
+ MUTE = true
+ REQUIRED_METHODS = %w{
+ subscribers
+ diaspora_handle
+ receive
+ to_diaspora_xml
+ after_dispatch
+ author
+ notification_type
+ }
+
+ OPTIONAL_METHODS = %w{
+ parent
+ relayable
+ }
+
+ def self.included(model)
+ unless MUTE
+ lint_check(model)
+
+ #type check federation methods to make sure they are giving us expected stuff
+ model.instance_eval do
+ alias_method_chain :receive, :lint
+ alias_method_chain :subscribers, :lint
+ end
+ end
+ end
+
+ def self.lint_check(model)
+ object = model.new
+
+ [REQUIRED_METHODS, OPTIONAL_METHODS].each do |set|
+ unresponsive_methods(object, set).each do |missing_method|
+ puts "WARNING: #{model} does not have method:#{missing_method}"
+ end
+ end
+ puts'-------'
+ end
+
+ def self.unresponsive_methods(object, methods)
+ methods.find_all do |method|
+ !object.respond_to?(method.to_sym)
+ end
+ end
+
+
+
+ #recieve should always return an instance of itself (or nil?????)
+ def receive_with_lint(*args)
+ object = receive_without_lint(*args)
+ #if this object is not returning an instance of itself
+ if object.class.to_s != self.class.to_s
+ puts "WARNING: #{self.class.to_s}'s receive is returing object of type: #{object.class.to_s}" unless MUTE
+ end
+ object
+ end
+
+ #subscribers should always return ActiveRecord::Relation
+ def subscribers_with_lint(*args)
+ subscribers = subscribers_without_lint(*args)
+ unless subscribers.class.to_s == 'ActiveRecord::Relation'
+ puts "WARNING: #{self.class.to_s}'s subscribers is returning object of type: #{subscribers.class}" unless MUTE
+ end
+
+ subscribers
+ end
+ end
+ end
+end
View
40 lib/diaspora/federated/parser.rb
@@ -0,0 +1,40 @@
+#takes anything that can be handled by Diaspora::Parser, so basically
+# anything ROXMLified
+
+require File.join(Rails.root, 'lib/diaspora/parser')
+
+class Diaspora::Federated::Parser
+ attr_accessor :xml, :sender, :object
+
+ def initialize(xml, sender)
+ self.xml = xml
+ self.sender = sender
+ end
+
+ def parse!
+ #we might need a begin rescue here
+ self.object = Diaspora::Parser.from_xml(xml)
+
+ #crazy munging side effects :(
+ assign_sender_handle_if_request!
+ update_actor_to_reflect_object_author!
+
+ self.object
+ end
+
+ private
+
+ def assign_sender_handle_if_request!
+ #special casey
+ if object.is_a?(Request)
+ object.sender_handle = sender.diaspora_handle
+ end
+ end
+
+ def update_actor_to_reflect_object_author!
+ if actor = Webfinger.new(object.diaspora_handle).fetch
+ object.author = actor if object.respond_to? :author=
+ object.person = actor if object.respond_to? :person=
+ end
+ end
+end
View
13 lib/diaspora/federated/shareable.rb
@@ -69,7 +69,8 @@ def subscribers(user)
user.people_in_aspects(user.aspects_with_shareable(self.class, self.id))
end
end
- protected
+
+ protected
# @return [Shareable,void]
def persisted_shareable
@@ -92,7 +93,7 @@ def receive_persisted(user, person, local_shareable)
false
end
else
- user.contact_for(person).receive_shareable(local_shareable)
+ create_share_visibility_for_contact(user, person, local_shareable)
user.notify_if_mentioned(local_shareable)
Rails.logger.info("event=receive payload_type=#{self.class} update=true status=complete sender=#{self.diaspora_handle}") #existing_shareable=#{local_shareable.id}")
true
@@ -101,7 +102,7 @@ def receive_persisted(user, person, local_shareable)
def receive_non_persisted(user, person)
if self.save
- user.contact_for(person).receive_shareable(self)
+ create_share_visibility_for_contact(user, person, self)
user.notify_if_mentioned(self)
Rails.logger.info("event=receive payload_type=#{self.class} update=false status=complete sender=#{self.diaspora_handle}")
true
@@ -110,6 +111,12 @@ def receive_non_persisted(user, person)
false
end
end
+
+ def create_share_visibility_for_contact(user, person, shareable)
+ if contact = user.contact_for(person)
+ contact.receive_shareable(shareable)
+ end
+ end
end
end
end
View
6 lib/diaspora/federated/validator.rb
@@ -0,0 +1,6 @@
+module Diaspora
+ module Federated
+ module Validator
+ end
+ end
+end
View
103 lib/diaspora/federated/validator/private.rb
@@ -0,0 +1,103 @@
+#things I don't like about this
+ #requiring the model to be valid here.... it is nice to move object specific validations
+ # to the model, but should we just not even call them here?
+
+ # federation validations are related, but not the same as normal validations
+ # ie , in the case of updates
+ # validations here are not fail fast(there is a reasonable order to be had)
+ #
+
+
+class Diaspora::Federated::Validator::Private
+ include ActiveModel::Validations
+
+ attr_accessor :salmon, :user, :sender, :object
+
+ validate :relayable_object_has_parent
+ validate :contact_required
+ validate :sender_is_someone_who_has_authority_of_the_post
+ validate :model_is_valid?
+
+ def initialize(salmon, user, sender)
+ self.salmon = salmon
+ self.user = user
+ self.sender = sender
+ end
+
+ def process!
+ return nil unless valid_signature_on_envelope? #parsing can be $$$ so do it first
+
+ #may need to handle case where parse returns nil
+ if required_attributes_present? && self.valid?
+ object
+ else
+ #this is a hack to make tests pass for now
+
+ raise self.errors.full_messages.join if self.errors.present?
+ FEDERATION_LOGGER.info("Failed Private Receive: #{self.errors.inspect}")
+ nil
+ end
+ end
+
+ def required_attributes_present?
+ self.salmon.present? && self.user.present? && self.sender.present?
+ end
+
+ def object
+ @object ||= Diaspora::Federated::Parser.new(salmon.parsed_data, sender).parse!
+ end
+
+ private
+
+ #weird
+ def valid_signature_on_envelope?
+ if(sender.present? && !self.salmon.verified_for_key?(sender.public_key))
+ return false
+ else
+ true
+ #errors.add :salmon, "sender failed key check"
+ end
+ end
+
+ # the diaspora handle of the person we expect to be sending us the message
+ # if it is a relayable, the parent author is sending us the object
+ # otherwise, it is the author of the thing itself(duh)
+ def expected_object_authority
+ if object.respond_to?(:relayable?) && object.parent.present?
+ #if A and B are friends, and A sends B a comment from C, we delegate the validation to the owner of the post being commented on
+ if user.owns?(object.parent)
+ object.diaspora_handle
+ else
+ object.parent.author.diaspora_handle
+ end
+ else
+ object.diaspora_handle
+ end
+ end
+
+ #validations
+
+ def relayable_object_has_parent
+ if object.respond_to?(:relayable?) && object.parent.nil?
+ errors.add :base, "Relayable Object has no known parent."
+ end
+ end
+
+ def contact_required
+ unless object.is_a?(Request) || user.contact_for(sender).present?
+ errors.add :base, "Contact required to receive object."
+ end
+ end
+
+ def sender_is_someone_who_has_authority_of_the_post
+ unless sender.diaspora_handle == expected_object_authority
+ errors.add :base, "Message sent from someone who does not have write access to the object"
+ end
+ end
+
+ def model_is_valid?
+ unless object.valid?
+ errors.add :object, "#{object.class}: #{object.errors.full_messages.join(', ')}"
+ end
+ end
+end
View
2  lib/diaspora/guid.rb
@@ -6,7 +6,7 @@ def self.included(model)
model.class_eval do
before_create :set_guid
xml_attr :guid
- validates :guid, :uniqueness => true
+ validates :guid, :uniqueness => true#, :presence => true
end
end
View
151 lib/postzord/receiver/private.rb
@@ -1,123 +1,90 @@
-# Copyright (c) 2010-2011, Diaspora Inc. This file is
+ # Copyright (c) 2010-2011, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
-
require File.join(Rails.root, 'lib/webfinger')
-require File.join(Rails.root, 'lib/diaspora/parser')
+require File.join(Rails.root, 'lib/diaspora/federated/parser')
+require File.join(Rails.root, 'lib/diaspora/federated/validator/private')
-class Postzord::Receiver::Private < Postzord::Receiver
- def initialize(user, opts={})
- @user = user
- @user_person = @user.person
- @salmon_xml = opts[:salmon_xml]
- @sender = opts[:person] || Webfinger.new(self.salmon.author_id).fetch
- @author = @sender
+#there are three phases of this object
+# 1. accept xml
+# 2. validate and emit the object
+# 3. recieve the object by the user
+# 4. post recieve callbacks
+#
+# currently, there are two gross ways into this object, one which takes encrypted xml,
+# and another which takes an object directly
+
+
+#note, two code paths to extract here :/
+# decent federation case 1. call with salmon_xml from federation request, and call receive
+# bad internal case: 2. with non salmonified xml (this is because requests and retractions are not persisted, but still unique to individuals)
+# bad internal case: 3: with an object instance directly, which is for persisted objects, but same reasoning as above.
+class Postzord::Receiver::Private < Postzord::Receiver
+ attr_accessor :object, :user, :sender, :salmon, :salmon_xml
- @object = opts[:object]
+ def initialize(user, opts={})
+ self.user = user
+ self.salmon_xml = opts[:salmon_xml]
+ self.sender = opts[:person] || Webfinger.new(self.salmon.author_id).fetch
+ @actor = @sender
+ self.object = opts[:object]
end
+
+ #called from xml provided by the outside world
def receive!
- begin
- if @sender && self.salmon.verified_for_key?(@sender.public_key)
- parse_and_receive(salmon.parsed_data)
- else
- FEDERATION_LOGGER.info("event=receive status=abort recipient=#{@user.diaspora_handle} sender=#{@salmon.author_id} reason='not_verified for key'")
- false
- end
- rescue => e
- #this sucks
- FEDERATION_LOGGER.info("Failure to receive #{@object.inspect} for sender:#{@sender.id} for user:#{@user.id}: #{e.message}")
- raise e
+ validator = Diaspora::Federated::Validator::Private.new(self.salmon, @user, @sender)
+ if self.object = validator.process! #this should raise
+ refreshed_object = accept_object_for_user #this SHOULD emit an instance of the object if it already exists, or itself
+ post_receive_hooks(refreshed_object)
+ else
+ FEDERATION_LOGGER.info("failed to receive object: #{validator.errors.inspect}")
+ false
end
end
+ #called from local code paths only, so we dont need to do all the validation checks
def parse_and_receive(xml)
- @object ||= Diaspora::Parser.from_xml(xml)
- return if @object.nil?
-
- FEDERATION_LOGGER.info("user:#{@user.id} starting private receive from person:#{@sender.guid}")
+ self.object = create_object_from_local(xml)
+ receive_object
+ end
- if self.validate_object
- set_author!
- receive_object
- FEDERATION_LOGGER.info("object received #{@object.class}")
- else
- FEDERATION_LOGGER.info("failed to receive object from #{@object.author}: #{@object.inspect}")
- raise "not a valid object:#{@object.inspect}"
- end
+ #this is a method to get the tests to pass
+ # it is used where we manaully pass an already parsed object into be received
+ def receive_object
+ obj = accept_object_for_user
+ post_receive_hooks(obj)
end
+
# @return [Object]
- def receive_object
- obj = @object.receive(@user, @author)
- Notification.notify(@user, obj, @author) if obj.respond_to?(:notification_type)
+ def accept_object_for_user
+ obj = object.receive(@user, object.author)
FEDERATION_LOGGER.info("user:#{@user.id} successfully received private post from person#{@sender.guid} #{@object.inspect}")
obj
end
- protected
- def salmon
- @salmon ||= Salmon::EncryptedSlap.from_xml(@salmon_xml, @user)
- end
-
- def xml_author
- if @object.respond_to?(:relayable?)
- #if A and B are friends, and A sends B a comment from C, we delegate the validation to the owner of the post being commented on
- xml_author = @user.owns?(@object.parent) ? @object.diaspora_handle : @object.parent.author.diaspora_handle
- @author = Webfinger.new(@object.diaspora_handle).fetch if @object.author
- else
- xml_author = @object.diaspora_handle
- end
- xml_author
- end
-
- def validate_object
- raise "Contact required unless request" if contact_required_unless_request
- raise "Relayable object, but no parent object found" if relayable_without_parent?
-
- assign_sender_handle_if_request
-
- raise "Author does not match XML author" if author_does_not_match_xml_author?
-
- @object
- end
-
- def set_author!
- return unless @author
- @object.author = @author if @object.respond_to? :author=
- @object.person = @author if @object.respond_to? :person=
+ def post_receive_hooks(obj)
+ notify_receiver(obj)
end
- private
-
- #validations
- def relayable_without_parent?
- if @object.respond_to?(:relayable?) && @object.parent.nil?
- FEDERATION_LOGGER.info("event=receive status=abort reason='received a comment but no corresponding post' recipient=#{@user_person.diaspora_handle} sender=#{@sender.diaspora_handle} payload_type=#{@object.class})")
- return true
- end
- end
+ protected
- def author_does_not_match_xml_author?
- if (@author.diaspora_handle != xml_author)
- FEDERATION_LOGGER.info("event=receive status=abort reason='author in xml does not match retrieved person' payload_type=#{@object.class} recipient=#{@user_person.diaspora_handle} sender=#{@sender.diaspora_handle}")
- return true
- end
+ def create_object_from_local(xml)
+ Diaspora::Federated::Parser.new(xml, @sender).parse!
end
- def contact_required_unless_request
- unless @object.is_a?(Request) || @user.contact_for(@sender)
- FEDERATION_LOGGER.info("event=receive status=abort reason='sender not connected to recipient' recipient=#{@user_person.diaspora_handle} sender=#{@sender.diaspora_handle}")
- return true
- end
+ def salmon
+ @salmon ||= Salmon::EncryptedSlap.from_xml(@salmon_xml, @user)
end
- def assign_sender_handle_if_request
- #special casey
- if @object.is_a?(Request)
- @object.sender_handle = @sender.diaspora_handle
+ def notify_receiver(obj)
+ if obj.respond_to?(:notification_type)
+ Notification.notify(@user, obj, @object.author)
+ else
+ FEDERATION_LOGGER.info("WARNING: object #{obj.inspect}: did not respond_t0 notification_type")
end
end
-end
+end
View
6 lib/postzord/receiver/public.rb
@@ -13,6 +13,12 @@ def initialize(xml)
FEDERATION_LOGGER.info("Receving public post from person:#{@author.id}")
end
+
+ # def new_receive!
+ # stub_object = Diaspora::FederationValidator::Public.new(@author, @salmon).validate_object!
+ # actual_object = stub_object.commit!
+ # actual_object.receive_public!
+ # end
# @return [Boolean]
def verified_signature?
@salmon.verified_for_key?(@author.public_key)
View
127 spec/integration/attack_vectors_spec.rb
@@ -52,12 +52,12 @@ def legit_post_from_user1_to_user2(user1, user2)
end
describe "attack vectors" do
+ include FederationIntegrationHelper
let(:eves_aspect) { eve.aspects.find_by_name("generic") }
let(:alices_aspect) { alice.aspects.find_by_name("generic") }
context "testing side effects of validation phase" do
-
describe 'Contact Required Unless Request' do
#CUSTOM SETUP; cant use helpers here
it 'does not save a post from a non-contact as a side effect' do
@@ -114,7 +114,7 @@ def legit_post_from_user1_to_user2(user1, user2)
profile.first_name = "Not BOB"
expect {
- expect_error /Author does not match XML author/ do
+ expect_error /Message sent from someone who does not have write access to the object/ do
receive(profile, :from => alice, :by => bob)
end
}.should_not change(eve.profile, :first_name)
@@ -122,52 +122,44 @@ def legit_post_from_user1_to_user2(user1, user2)
end
end
+ context 'around retractions' do
+ it "(1 of 2) ignores INTACT retractions sent by someone other than retraction author" do
+ #alice can not retract eves message with eves retraction
+ original_message = legit_post_from_user1_to_user2(eve, bob)
-
- context 'malicious contact attack vector' do
- describe 'mass assignment on id' do
- it "does not save a message over an old message with a different author" do
- #setup: A user has a message with a given guid and author
- original_message = legit_post_from_user1_to_user2(eve, bob)
-
- #someone else tries to make a message with the same guid
- malicious_message = Factory.build(:status_message, :id => original_message.id, :guid => original_message.guid, :author => alice.person)
-
- expect{
- receive(malicious_message, :from => alice, :by => bob)
- }.should_not change(original_message, :author_id)
+ ret = bogus_retraction do |retraction|
+ retraction.post_guid = original_message.guid
+ retraction.diaspora_handle = eve.person.diaspora_handle
+ retraction.type = original_message.class.to_s
end
- it 'does not save a message over an old message with the same author' do
- #setup:
- # i have a legit message from eve
- original_message = legit_post_from_user1_to_user2(eve, bob)
-
- #eve tries to send me another message with the same ID
- malicious_message = Factory.build( :status_message, :id => original_message.id, :text => 'BAD!!!', :author => eve.person)
-
- expect {
- receive(malicious_message, :from => eve, :by => bob)
- }.should_not change(original_message, :text)
- end
+ expect {
+ expect_error /Message sent from someone who does not have write access to the object/ do
+ receive(ret, :from => alice, :by => bob)
+ end
+ }.should_not change(StatusMessage, :count)
end
+ #NOTE: THIS VALIDATION IS SAVED BY SOME MODEL LEVEL VALIDATION :()
+ it '(2 of 2) ignores retractions generated for posts that the retractor does not own' do
+ #alice can not retract eves message with alice's
- it "ignores retractions on a post not owned by the retraction's sender" do
original_message = legit_post_from_user1_to_user2(eve, bob)
- ret = bogus_retraction do |retraction|
- retraction.post_guid = original_message.guid
- retraction.diaspora_handle = alice.person.diaspora_handle
- retraction.type = original_message.class.to_s
+ retraction = bogus_retraction do |ret|
+ ret.post_guid = original_message.guid
+ ret.diaspora_handle = alice.person.diaspora_handle
+ ret.type = original_message.class.to_s
end
expect {
- receive(ret, :from => alice, :by => bob)
+ expect_error /Retractor does not control target/ do
+ receive(retraction, :from => alice, :by => bob)
+ end
}.should_not change(StatusMessage, :count)
end
- it "silently disregards retractions for non-existent posts(that are from someone other than the post's author)" do
+ it "complains loudly when you send a retraction with no target" do
bogus_retraction = temporary_post(eve) do |original_message|
bogus_retraction do |ret|
ret.post_guid = original_message.guid
@@ -177,27 +169,12 @@ def legit_post_from_user1_to_user2(user1, user2)
end
expect{
receive(bogus_retraction, :from => alice, :by => bob)
- }.should_not raise_error
+ }.should raise_error
end
- it 'should not receive retractions where the retractor and the salmon author do not match' do
- original_message = legit_post_from_user1_to_user2(eve, bob)
-
- retraction = bogus_retraction do |ret|
- ret.post_guid = original_message.guid
- ret.diaspora_handle = eve.person.diaspora_handle
- ret.type = original_message.class.to_s
- end
-
- expect {
- expect_error /Author does not match XML author/ do
- receive(retraction, :from => alice, :by => bob)
- end
- }.should_not change(bob.visible_shareables(Post), :count)
- end
- it 'it should not allow you to send retractions for other people' do
+ it 'it should not allow you unfriend other users' do
#we are banking on bob being friends with alice and eve
#here, alice is trying to disconnect bob and eve
@@ -208,7 +185,9 @@ def legit_post_from_user1_to_user2(user1, user2)
end
expect{
- receive(retraction, :from => alice, :by => bob)
+ expect_error /Retractor does not control target/ do
+ receive(retraction, :from => alice, :by => bob)
+ end
}.should_not change{bob.reload.contacts.count}
end
@@ -220,14 +199,50 @@ def legit_post_from_user1_to_user2(user1, user2)
end
expect{
- expect_error /Author does not match XML author/ do
+ expect_error /Message sent from someone who does not have write access to the object/ do
receive(retraction, :from => alice, :by => bob)
end
}.should_not change(bob.contacts, :count)
end
+ end
+
+ context 'malicious contact attack vector' do
+ describe 'mass assignment on id' do
+ it "does not save a message over an old message with a different author" do
+ #setup: A user has a message with a given guid and author
+ original_message = legit_post_from_user1_to_user2(eve, bob)
+
+ #someone else tries to make a message with the same guid
+ malicious_message = Factory.build(:status_message, :id => original_message.id, :guid => original_message.guid, :author => alice.person)
+
+ expect{
+ expect_error /Guid has already been taken/ do
+ receive(malicious_message, :from => alice, :by => bob)
+ end
+ }.should_not change(original_message, :author_id)
+ end
+
+ it 'does not save a message over an old message with the same author' do
+ #setup:
+ # i have a legit message from eve
+ original_message = legit_post_from_user1_to_user2(eve, bob)
+
+ #eve tries to send me another message with the same ID
+ malicious_message = Factory.build( :status_message, :id => original_message.id, :text => 'BAD!!!', :author => eve.person)
+
+ expect {
+ receive(malicious_message, :from => eve, :by => bob)
+ }.should_not change(original_message, :text)
+ end
+ end
+
+
+
+
it 'does not let another user update other persons post' do
- original_message = eve.post(:photo, :user_file => uploaded_photo, :text => "store this!", :to => eves_aspect.id)
+ original_message = eve.build_post(:photo, :user_file => uploaded_photo, :text => "store this!", :to => eves_aspect.id)
+
receive(original_message, :from => eve, :by => bob)
#is this testing two things?
@@ -236,7 +251,9 @@ def legit_post_from_user1_to_user2(user1, user2)
new_message.text = "bad bad bad"
expect{
- receive(new_message, :from => alice, :by => bob)
+ expect_error /Guid has already been taken/ do
+ receive(new_message, :from => alice, :by => bob)
+ end
}.should_not change(original_message, :text)
end
end
View
18 spec/integration/receiving_spec.rb
@@ -5,6 +5,7 @@
require 'spec_helper'
describe 'a user receives a post' do
+ include FederationIntegrationHelper
def receive_with_zord(user, person, xml)
zord = Postzord::Receiver::Private.new(user, :person => person)
@@ -279,16 +280,15 @@ def receive_with_zord(user, person, xml)
describe 'salmon' do
- let(:post){alice.post :status_message, :text => "hello", :to => @alices_aspect.id}
- let(:salmon){alice.salmon( post )}
-
it 'processes a salmon for a post' do
- salmon_xml = salmon.xml_for(bob.person)
-
- zord = Postzord::Receiver::Private.new(bob, :salmon_xml => salmon_xml)
- zord.perform!
-
- bob.visible_shareables(Post).include?(post).should be_true
+ salmon_xml = temporary_post(alice) do |post|
+ alice.salmon(post).xml_for(bob.person)
+ end
+
+ expect{
+ zord = Postzord::Receiver::Private.new(bob, :salmon_xml => salmon_xml)
+ zord.perform!
+ }.to change(Post, :count).by(1)
end
end
View
90 spec/lib/diaspora/federated/private_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+def only_error_message_should_include(partial_message)
+ error_messages_should_include([partial_message])
+ @validator.errors.full_messages.count.should == 1
+end
+
+def error_messages_should_include(array_of_regexes)
+ @validator.should_not be_valid
+ array_of_regexes.each do |partial_message|
+ @validator.errors.full_messages.to_s.should match partial_message
+ end
+end
+
+describe Diaspora::Federated::Validator::Private do
+ before do
+ #this sucks
+ salmon = stub(:verified_for_key? => true)
+ @validator = Diaspora::Federated::Validator::Private.new(salmon, bob, alice.person)
+ @object = Factory.build(:status_message, :author => alice.person)
+ @validator.stub(:object).and_return(@object)
+ end
+
+ describe '#process!' do
+ it 'does not save the object' do
+ object = @validator.process!
+ object.should_not be_persisted
+ end
+
+ it 'returns nil if salmon signature does not check out' do
+ @validator.stub(:valid_signature_on_envelope?).and_return false
+ @validator.process!.should be_nil
+ end
+
+ it 'returns the object if the validator is valid' do
+ @validator.process!.should == @object
+ end
+
+ it 'raises an error if the validations fail #temporary' do
+ @validator.stub(:valid? => false)
+ @validator.errors.add(:sender, "an example showing any error raises")
+ expect{
+ @validator.process!
+ }.to raise_error
+ end
+
+ end
+
+ context 'validations' do
+ it 'starts as a valid instance' do
+ @validator.should be_valid
+ end
+
+
+ describe '#model_is_valid?' do
+ it 'adds an error the associated model is not valid' do
+ #setup: the model parsed is not valid on its own accord
+ @validator.object.stub(:valid?).and_return false
+ only_error_message_should_include /Object/
+ end
+ end
+
+ describe '#xml_author_matches_a_known_party' do
+ it 'adds an error message if the object is not the same as the known party' do
+ #setup: the known party is not the author of the parsed post
+ @validator.stub(:expected_object_authority => "dog@bountyhunter.com")
+ only_error_message_should_include /does not have write access/
+ end
+ end
+
+ describe '#contact_required' do
+ it 'adds error if object is not a request, and there is no a contact' do
+ #setup: the sender is someone who is not a contact
+ @validator.sender = Factory(:person) #set sender as unknown to bob
+ error_messages_should_include([/Contact required/, /write access/])
+ end
+ end
+
+ describe '#relayable_object_had_parent' do
+ it 'adds an error if the object is relayable and has no parent' do
+ #setup: the object is a relayable and has no parent, also it is valid?#why
+ dh = @validator.object.diaspora_handle
+ @validator.stub(:known_party).and_return(dh)
+ @validator.object.stub(:respond_to? => true, :parent => nil, :valid? => true, :diaspora_handle => dh)
+
+ only_error_message_should_include /Relayable Object has no known parent/
+ end
+ end
+ end
+end
View
2  spec/lib/postzord/receiver/private_spec.rb
@@ -78,7 +78,7 @@
end
it 'calls Notification.notify if object responds to notification_type' do
- cm = Comment.new
+ cm = Factory(:comment, :author => alice.person)
cm.stub(:receive).and_return(cm)
Notification.should_receive(:notify).with(bob, cm, alice.person)
View
52 spec/support/federation_integration_helper.rb
@@ -0,0 +1,52 @@
+module FederationIntegrationHelper
+ def receive(post, opts)
+ sender = opts.fetch(:from)
+ receiver = opts.fetch(:by)
+ salmon_xml = sender.salmon(post).xml_for(receiver.person)
+ zord = Postzord::Receiver::Private.new(receiver, :salmon_xml => salmon_xml)
+ zord.perform!
+ end
+
+ def temporary_user(&block)
+ user = Factory(:user)
+ block_return_value = yield user
+ user.delete
+ block_return_value
+ end
+
+ def temporary_post(user, &block)
+ temp_post = user.post(:status_message, :text => 'hi')
+ block_return_value = yield temp_post
+ temp_post.delete
+ block_return_value
+ end
+
+ def expect_error(partial_message, &block)
+ begin
+ yield
+ rescue => e
+ ensure
+ e.should be_present
+ e.message.should match partial_message
+ end
+ end
+
+ def bogus_retraction(&block)
+ ret = Retraction.new
+ yield ret
+ ret
+ end
+
+ def user_should_not_see_guid(user, guid)
+ user.reload.visible_shareables(Post).where(:guid => guid).should be_blank
+ end
+
+ #returns the message
+ #should this be persisted? This actually fails a validation, as it is trying to save over the guid... :()
+ def legit_post_from_user1_to_user2(user1, user2)
+ original_message = user1.build_post(:status_message, :text => 'store this!', :to => user1.aspects.find_by_name("generic").id)
+
+ receive(original_message, :from => user1, :by => user2)
+ original_message
+ end
+end

No commit comments for this range

Something went wrong with that request. Please try again.