protected_attributes 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. data/.gitignore +17 -0
  2. data/.travis.yml +17 -0
  3. data/Gemfile +7 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +111 -0
  6. data/Rakefile +11 -0
  7. data/lib/action_controller/accessible_params_wrapper.rb +29 -0
  8. data/lib/active_model/mass_assignment_security.rb +353 -0
  9. data/lib/active_model/mass_assignment_security/permission_set.rb +40 -0
  10. data/lib/active_model/mass_assignment_security/sanitizer.rb +74 -0
  11. data/lib/active_record/mass_assignment_security.rb +23 -0
  12. data/lib/active_record/mass_assignment_security/associations.rb +116 -0
  13. data/lib/active_record/mass_assignment_security/attribute_assignment.rb +88 -0
  14. data/lib/active_record/mass_assignment_security/core.rb +27 -0
  15. data/lib/active_record/mass_assignment_security/inheritance.rb +18 -0
  16. data/lib/active_record/mass_assignment_security/nested_attributes.rb +148 -0
  17. data/lib/active_record/mass_assignment_security/persistence.rb +81 -0
  18. data/lib/active_record/mass_assignment_security/reflection.rb +9 -0
  19. data/lib/active_record/mass_assignment_security/relation.rb +47 -0
  20. data/lib/active_record/mass_assignment_security/validations.rb +24 -0
  21. data/lib/protected_attributes.rb +14 -0
  22. data/lib/protected_attributes/railtie.rb +18 -0
  23. data/lib/protected_attributes/version.rb +3 -0
  24. data/protected_attributes.gemspec +26 -0
  25. data/test/abstract_unit.rb +156 -0
  26. data/test/accessible_params_wrapper_test.rb +76 -0
  27. data/test/ar_helper.rb +67 -0
  28. data/test/attribute_sanitization_test.rb +929 -0
  29. data/test/mass_assignment_security/black_list_test.rb +20 -0
  30. data/test/mass_assignment_security/permission_set_test.rb +36 -0
  31. data/test/mass_assignment_security/sanitizer_test.rb +50 -0
  32. data/test/mass_assignment_security/white_list_test.rb +19 -0
  33. data/test/mass_assignment_security_test.rb +118 -0
  34. data/test/models/company.rb +105 -0
  35. data/test/models/keyboard.rb +3 -0
  36. data/test/models/mass_assignment_specific.rb +76 -0
  37. data/test/models/person.rb +82 -0
  38. data/test/models/subscriber.rb +5 -0
  39. data/test/models/task.rb +5 -0
  40. data/test/test_helper.rb +3 -0
  41. metadata +199 -0
@@ -0,0 +1,20 @@
1
+ require "test_helper"
2
+
3
+ class BlackListTest < ActiveModel::TestCase
4
+
5
+ def setup
6
+ @black_list = ActiveModel::MassAssignmentSecurity::BlackList.new
7
+ @included_key = 'admin'
8
+ @black_list += [ @included_key ]
9
+ end
10
+
11
+ test "deny? is true for included items" do
12
+ assert_equal true, @black_list.deny?(@included_key)
13
+ end
14
+
15
+ test "deny? is false for non-included items" do
16
+ assert_equal false, @black_list.deny?('first_name')
17
+ end
18
+
19
+
20
+ end
@@ -0,0 +1,36 @@
1
+ require "test_helper"
2
+
3
+ class PermissionSetTest < ActiveModel::TestCase
4
+
5
+ def setup
6
+ @permission_list = ActiveModel::MassAssignmentSecurity::PermissionSet.new
7
+ end
8
+
9
+ test "+ stringifies added collection values" do
10
+ symbol_collection = [ :admin ]
11
+ new_list = @permission_list += symbol_collection
12
+
13
+ assert new_list.include?('admin'), "did not add collection to #{@permission_list.inspect}}"
14
+ end
15
+
16
+ test "+ compacts added collection values" do
17
+ added_collection = [ nil ]
18
+ new_list = @permission_list + added_collection
19
+ assert_equal new_list, @permission_list, "did not add collection to #{@permission_list.inspect}}"
20
+ end
21
+
22
+ test "include? normalizes multi-parameter keys" do
23
+ multi_param_key = 'admin(1)'
24
+ new_list = @permission_list += [ 'admin' ]
25
+
26
+ assert new_list.include?(multi_param_key), "#{multi_param_key} not found in #{@permission_list.inspect}"
27
+ end
28
+
29
+ test "include? normal keys" do
30
+ normal_key = 'admin'
31
+ new_list = @permission_list += [ normal_key ]
32
+
33
+ assert new_list.include?(normal_key), "#{normal_key} not found in #{@permission_list.inspect}"
34
+ end
35
+
36
+ end
@@ -0,0 +1,50 @@
1
+ require "test_helper"
2
+ require 'active_support/logger'
3
+
4
+ class SanitizerTest < ActiveModel::TestCase
5
+ attr_accessor :logger
6
+
7
+ class Authorizer < ActiveModel::MassAssignmentSecurity::PermissionSet
8
+ def deny?(key)
9
+ ['admin', 'id'].include?(key)
10
+ end
11
+ end
12
+
13
+ def setup
14
+ @logger_sanitizer = ActiveModel::MassAssignmentSecurity::LoggerSanitizer.new(self)
15
+ @strict_sanitizer = ActiveModel::MassAssignmentSecurity::StrictSanitizer.new(self)
16
+ @authorizer = Authorizer.new
17
+ end
18
+
19
+ test "sanitize attributes" do
20
+ original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' }
21
+ attributes = @logger_sanitizer.sanitize(self.class, original_attributes, @authorizer)
22
+
23
+ assert attributes.key?('first_name'), "Allowed key shouldn't be rejected"
24
+ assert !attributes.key?('admin'), "Denied key should be rejected"
25
+ end
26
+
27
+ test "debug mass assignment removal with LoggerSanitizer" do
28
+ original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' }
29
+ log = StringIO.new
30
+ self.logger = ActiveSupport::Logger.new(log)
31
+ @logger_sanitizer.sanitize(self.class, original_attributes, @authorizer)
32
+ assert_match(/admin/, log.string, "Should log removed attributes: #{log.string}")
33
+ end
34
+
35
+ test "debug mass assignment removal with StrictSanitizer" do
36
+ original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' }
37
+ assert_raise ActiveModel::MassAssignmentSecurity::Error do
38
+ @strict_sanitizer.sanitize(self.class, original_attributes, @authorizer)
39
+ end
40
+ end
41
+
42
+ test "mass assignment insensitive attributes" do
43
+ original_attributes = {'id' => 1, 'first_name' => 'allowed'}
44
+
45
+ assert_nothing_raised do
46
+ @strict_sanitizer.sanitize(self.class, original_attributes, @authorizer)
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,19 @@
1
+ require "test_helper"
2
+
3
+ class WhiteListTest < ActiveModel::TestCase
4
+
5
+ def setup
6
+ @white_list = ActiveModel::MassAssignmentSecurity::WhiteList.new
7
+ @included_key = 'first_name'
8
+ @white_list += [ @included_key ]
9
+ end
10
+
11
+ test "deny? is false for included items" do
12
+ assert_equal false, @white_list.deny?(@included_key)
13
+ end
14
+
15
+ test "deny? is true for non-included items" do
16
+ assert_equal true, @white_list.deny?('admin')
17
+ end
18
+
19
+ end
@@ -0,0 +1,118 @@
1
+ require 'test_helper'
2
+ require 'active_model/mass_assignment_security'
3
+ require 'models/mass_assignment_specific'
4
+
5
+ class CustomSanitizer < ActiveModel::MassAssignmentSecurity::Sanitizer
6
+
7
+ def process_removed_attributes(klass, attrs)
8
+ raise StandardError
9
+ end
10
+
11
+ end
12
+
13
+ class MassAssignmentSecurityTest < ActiveModel::TestCase
14
+ def test_attribute_protection
15
+ user = User.new
16
+ expected = { "name" => "John Smith", "email" => "john@smith.com" }
17
+ sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true))
18
+ assert_equal expected, sanitized
19
+ end
20
+
21
+ def test_attribute_protection_when_role_is_nil
22
+ user = User.new
23
+ expected = { "name" => "John Smith", "email" => "john@smith.com" }
24
+ sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true), nil)
25
+ assert_equal expected, sanitized
26
+ end
27
+
28
+ def test_only_moderator_role_attribute_accessible
29
+ user = SpecialUser.new
30
+ expected = { "name" => "John Smith", "email" => "john@smith.com" }
31
+ sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true), :moderator)
32
+ assert_equal expected, sanitized
33
+
34
+ sanitized = user.sanitize_for_mass_assignment({ "name" => "John Smith", "email" => "john@smith.com", "admin" => true })
35
+ assert_equal({}, sanitized)
36
+ end
37
+
38
+ def test_attributes_accessible
39
+ user = Person.new
40
+ expected = { "name" => "John Smith", "email" => "john@smith.com" }
41
+ sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true))
42
+ assert_equal expected, sanitized
43
+ end
44
+
45
+ def test_attributes_accessible_with_admin_role
46
+ user = Person.new
47
+ expected = { "name" => "John Smith", "email" => "john@smith.com", "admin" => true }
48
+ sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true), :admin)
49
+ assert_equal expected, sanitized
50
+ end
51
+
52
+ def test_attributes_accessible_with_roles_given_as_array
53
+ user = Account.new
54
+ expected = { "name" => "John Smith", "email" => "john@smith.com" }
55
+ sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true))
56
+ assert_equal expected, sanitized
57
+ end
58
+
59
+ def test_attributes_accessible_with_admin_role_when_roles_given_as_array
60
+ user = Account.new
61
+ expected = { "name" => "John Smith", "email" => "john@smith.com", "admin" => true }
62
+ sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true), :admin)
63
+ assert_equal expected, sanitized
64
+ end
65
+
66
+ def test_attributes_protected_by_default
67
+ firm = Firm.new
68
+ expected = { }
69
+ sanitized = firm.sanitize_for_mass_assignment({ "type" => "Client" })
70
+ assert_equal expected, sanitized
71
+ end
72
+
73
+ def test_mass_assignment_protection_inheritance
74
+ assert SpecialLoosePerson.accessible_attributes.blank?
75
+ assert_equal Set.new(['credit_rating', 'administrator']), SpecialLoosePerson.protected_attributes
76
+
77
+ assert SpecialLoosePerson.accessible_attributes.blank?
78
+ assert_equal Set.new(['credit_rating']), SpecialLoosePerson.protected_attributes(:admin)
79
+
80
+ assert LooseDescendant.accessible_attributes.blank?
81
+ assert_equal Set.new(['credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes
82
+
83
+ assert LooseDescendantSecond.accessible_attributes.blank?
84
+ assert_equal Set.new(['credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes,
85
+ 'Running attr_protected twice in one class should merge the protections'
86
+
87
+ assert((SpecialTightPerson.protected_attributes - SpecialTightPerson.attributes_protected_by_default).blank?)
88
+ assert_equal Set.new(['name', 'address']), SpecialTightPerson.accessible_attributes
89
+
90
+ assert((SpecialTightPerson.protected_attributes(:admin) - SpecialTightPerson.attributes_protected_by_default).blank?)
91
+ assert_equal Set.new(['name', 'address', 'admin']), SpecialTightPerson.accessible_attributes(:admin)
92
+
93
+ assert((TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default).blank?)
94
+ assert_equal Set.new(['name', 'address', 'phone_number']), TightDescendant.accessible_attributes
95
+
96
+ assert((TightDescendant.protected_attributes(:admin) - TightDescendant.attributes_protected_by_default).blank?)
97
+ assert_equal Set.new(['name', 'address', 'admin', 'super_powers']), TightDescendant.accessible_attributes(:admin)
98
+ end
99
+
100
+ def test_mass_assignment_multiparameter_protector
101
+ task = Task.new
102
+ attributes = { "starting(1i)" => "2004", "starting(2i)" => "6", "starting(3i)" => "24" }
103
+ sanitized = task.sanitize_for_mass_assignment(attributes)
104
+ assert_equal sanitized, { }
105
+ end
106
+
107
+ def test_custom_sanitizer
108
+ old_sanitizer = User._mass_assignment_sanitizer
109
+
110
+ user = User.new
111
+ User.mass_assignment_sanitizer = CustomSanitizer.new
112
+ assert_raise StandardError do
113
+ user.sanitize_for_mass_assignment("admin" => true)
114
+ end
115
+ ensure
116
+ User.mass_assignment_sanitizer = old_sanitizer
117
+ end
118
+ end
@@ -0,0 +1,105 @@
1
+ class AbstractCompany < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
4
+
5
+ class Company < AbstractCompany
6
+ attr_protected :rating
7
+ self.sequence_name = :companies_nonstd_seq
8
+
9
+ validates_presence_of :name
10
+
11
+ has_one :dummy_account, :foreign_key => "firm_id", :class_name => "Account"
12
+ has_many :contracts
13
+ has_many :developers, :through => :contracts
14
+
15
+ def arbitrary_method
16
+ "I am Jack's profound disappointment"
17
+ end
18
+
19
+ private
20
+
21
+ def private_method
22
+ "I am Jack's innermost fears and aspirations"
23
+ end
24
+ end
25
+
26
+ class Firm < Company
27
+ ActiveSupport::Deprecation.silence do
28
+ has_many :clients, -> { order "id" }, :dependent => :destroy, :counter_sql =>
29
+ "SELECT COUNT(*) FROM companies WHERE firm_id = 1 " +
30
+ "AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )",
31
+ :before_remove => :log_before_remove,
32
+ :after_remove => :log_after_remove
33
+ end
34
+ has_many :unsorted_clients, :class_name => "Client"
35
+ has_many :unsorted_clients_with_symbol, :class_name => :Client
36
+ has_many :clients_sorted_desc, -> { order "id DESC" }, :class_name => "Client"
37
+ has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client"
38
+ has_many :clients_ordered_by_name, -> { order "name" }, :class_name => "Client"
39
+ has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false
40
+ has_many :dependent_clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :dependent => :destroy
41
+ has_many :exclusively_dependent_clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
42
+ has_many :limited_clients, -> { limit 1 }, :class_name => "Client"
43
+ has_many :clients_with_interpolated_conditions, ->(firm) { where "rating > #{firm.rating}" }, :class_name => "Client"
44
+ has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client"
45
+ has_many :clients_like_ms_with_hash_conditions, -> { where(:name => 'Microsoft').order("id") }, :class_name => "Client"
46
+ ActiveSupport::Deprecation.silence do
47
+ has_many :clients_using_sql, :class_name => "Client", :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" }
48
+ has_many :clients_using_counter_sql, :class_name => "Client",
49
+ :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id} " },
50
+ :counter_sql => proc { "SELECT COUNT(*) FROM companies WHERE client_of = #{id}" }
51
+ has_many :clients_using_zero_counter_sql, :class_name => "Client",
52
+ :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" },
53
+ :counter_sql => proc { "SELECT 0 FROM companies WHERE client_of = #{id}" }
54
+ has_many :no_clients_using_counter_sql, :class_name => "Client",
55
+ :finder_sql => 'SELECT * FROM companies WHERE client_of = 1000',
56
+ :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000'
57
+ has_many :clients_using_finder_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE 1=1'
58
+ end
59
+ has_many :plain_clients, :class_name => 'Client'
60
+ has_many :readonly_clients, -> { readonly }, :class_name => 'Client'
61
+ has_many :clients_using_primary_key, :class_name => 'Client',
62
+ :primary_key => 'name', :foreign_key => 'firm_name'
63
+ has_many :clients_using_primary_key_with_delete_all, :class_name => 'Client',
64
+ :primary_key => 'name', :foreign_key => 'firm_name', :dependent => :delete_all
65
+ has_many :clients_grouped_by_firm_id, -> { group("firm_id").select("firm_id") }, :class_name => "Client"
66
+ has_many :clients_grouped_by_name, -> { group("name").select("name") }, :class_name => "Client"
67
+
68
+ has_one :account, :foreign_key => "firm_id", :dependent => :destroy, :validate => true
69
+ has_one :unvalidated_account, :foreign_key => "firm_id", :class_name => 'Account', :validate => false
70
+ has_one :account_with_select, -> { select("id, firm_id") }, :foreign_key => "firm_id", :class_name=>'Account'
71
+ has_one :readonly_account, -> { readonly }, :foreign_key => "firm_id", :class_name => "Account"
72
+ # added order by id as in fixtures there are two accounts for Rails Core
73
+ # Oracle tests were failing because of that as the second fixture was selected
74
+ has_one :account_using_primary_key, -> { order('id') }, :primary_key => "firm_id", :class_name => "Account"
75
+ has_one :account_using_foreign_and_primary_keys, :foreign_key => "firm_name", :primary_key => "name", :class_name => "Account"
76
+ has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete
77
+
78
+ has_one :account_limit_500_with_hash_conditions, -> { where :credit_limit => 500 }, :foreign_key => "firm_id", :class_name => "Account"
79
+
80
+ has_one :unautosaved_account, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false
81
+ has_many :accounts
82
+ has_many :unautosaved_accounts, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false
83
+
84
+ has_many :association_with_references, -> { references(:foo) }, :class_name => 'Client'
85
+
86
+ def log
87
+ @log ||= []
88
+ end
89
+
90
+ private
91
+ def log_before_remove(record)
92
+ log << "before_remove#{record.id}"
93
+ end
94
+
95
+ def log_after_remove(record)
96
+ log << "after_remove#{record.id}"
97
+ end
98
+ end
99
+
100
+ class Corporation < Company
101
+ attr_accessible :type, :name, :description
102
+ end
103
+
104
+ class SpecialCorporation < Corporation
105
+ end
@@ -0,0 +1,3 @@
1
+ class Keyboard < ActiveRecord::Base
2
+ self.primary_key = 'key_number'
3
+ end
@@ -0,0 +1,76 @@
1
+ class User
2
+ include ActiveModel::MassAssignmentSecurity
3
+ attr_protected :admin
4
+
5
+ public :sanitize_for_mass_assignment
6
+ end
7
+
8
+ class SpecialUser
9
+ include ActiveModel::MassAssignmentSecurity
10
+ attr_accessible :name, :email, :as => :moderator
11
+
12
+ public :sanitize_for_mass_assignment
13
+ end
14
+
15
+ class Person
16
+ include ActiveModel::MassAssignmentSecurity
17
+ attr_accessible :name, :email
18
+ attr_accessible :name, :email, :admin, :as => :admin
19
+
20
+ public :sanitize_for_mass_assignment
21
+ end
22
+
23
+ class Account
24
+ include ActiveModel::MassAssignmentSecurity
25
+ attr_accessible :name, :email, :as => [:default, :admin]
26
+ attr_accessible :admin, :as => :admin
27
+
28
+ public :sanitize_for_mass_assignment
29
+ end
30
+
31
+ class Firm
32
+ include ActiveModel::MassAssignmentSecurity
33
+
34
+ public :sanitize_for_mass_assignment
35
+
36
+ def self.attributes_protected_by_default
37
+ ["type"]
38
+ end
39
+ end
40
+
41
+ class Task
42
+ include ActiveModel::MassAssignmentSecurity
43
+ attr_protected :starting
44
+
45
+ public :sanitize_for_mass_assignment
46
+ end
47
+
48
+ class SpecialLoosePerson
49
+ include ActiveModel::MassAssignmentSecurity
50
+ attr_protected :credit_rating, :administrator
51
+ attr_protected :credit_rating, :as => :admin
52
+ end
53
+
54
+ class LooseDescendant < SpecialLoosePerson
55
+ attr_protected :phone_number
56
+ end
57
+
58
+ class LooseDescendantSecond< SpecialLoosePerson
59
+ attr_protected :phone_number
60
+ attr_protected :name
61
+ end
62
+
63
+ class SpecialTightPerson
64
+ include ActiveModel::MassAssignmentSecurity
65
+ attr_accessible :name, :address
66
+ attr_accessible :name, :address, :admin, :as => :admin
67
+
68
+ def self.attributes_protected_by_default
69
+ ["mobile_number"]
70
+ end
71
+ end
72
+
73
+ class TightDescendant < SpecialTightPerson
74
+ attr_accessible :phone_number
75
+ attr_accessible :super_powers, :as => :admin
76
+ end
@@ -0,0 +1,82 @@
1
+ class Person < ActiveRecord::Base
2
+ has_many :readers
3
+ has_many :secure_readers
4
+ has_one :reader
5
+
6
+ has_many :posts, :through => :readers
7
+ has_many :secure_posts, :through => :secure_readers
8
+ has_many :posts_with_no_comments, -> { includes(:comments).where('comments.id is null').references(:comments) },
9
+ :through => :readers, :source => :post
10
+
11
+ has_many :followers, foreign_key: 'friend_id', class_name: 'Friendship'
12
+
13
+ has_many :references
14
+ has_many :bad_references
15
+ has_many :fixed_bad_references, -> { where :favourite => true }, :class_name => 'BadReference'
16
+ has_one :favourite_reference, -> { where 'favourite=?', true }, :class_name => 'Reference'
17
+ has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order('comments.id') }, :through => :readers, :source => :post
18
+
19
+ has_many :jobs, :through => :references
20
+ has_many :jobs_with_dependent_destroy, :source => :job, :through => :references, :dependent => :destroy
21
+ has_many :jobs_with_dependent_delete_all, :source => :job, :through => :references, :dependent => :delete_all
22
+ has_many :jobs_with_dependent_nullify, :source => :job, :through => :references, :dependent => :nullify
23
+
24
+ belongs_to :primary_contact, :class_name => 'Person'
25
+ has_many :agents, :class_name => 'Person', :foreign_key => 'primary_contact_id'
26
+ has_many :agents_of_agents, :through => :agents, :source => :agents
27
+ belongs_to :number1_fan, :class_name => 'Person'
28
+
29
+ has_many :agents_posts, :through => :agents, :source => :posts
30
+ has_many :agents_posts_authors, :through => :agents_posts, :source => :author
31
+
32
+ scope :males, -> { where(:gender => 'M') }
33
+ scope :females, -> { where(:gender => 'F') }
34
+ end
35
+
36
+ class LoosePerson < ActiveRecord::Base
37
+ self.table_name = 'people'
38
+ self.abstract_class = true
39
+
40
+ attr_protected :comments, :best_friend_id, :best_friend_of_id
41
+ attr_protected :as => :admin
42
+
43
+ has_one :best_friend, :class_name => 'LoosePerson', :foreign_key => :best_friend_id
44
+ belongs_to :best_friend_of, :class_name => 'LoosePerson', :foreign_key => :best_friend_of_id
45
+ has_many :best_friends, :class_name => 'LoosePerson', :foreign_key => :best_friend_id
46
+
47
+ accepts_nested_attributes_for :best_friend, :best_friend_of, :best_friends
48
+ end
49
+
50
+ class TightPerson < ActiveRecord::Base
51
+ self.table_name = 'people'
52
+
53
+ attr_accessible :first_name, :gender
54
+ attr_accessible :first_name, :gender, :comments, :as => :admin
55
+ attr_accessible :best_friend_attributes, :best_friend_of_attributes, :best_friends_attributes
56
+ attr_accessible :best_friend_attributes, :best_friend_of_attributes, :best_friends_attributes, :as => :admin
57
+
58
+ has_one :best_friend, :class_name => 'TightPerson', :foreign_key => :best_friend_id
59
+ belongs_to :best_friend_of, :class_name => 'TightPerson', :foreign_key => :best_friend_of_id
60
+ has_many :best_friends, :class_name => 'TightPerson', :foreign_key => :best_friend_id
61
+
62
+ accepts_nested_attributes_for :best_friend, :best_friend_of, :best_friends
63
+ end
64
+
65
+ class NestedPerson < ActiveRecord::Base
66
+ self.table_name = 'people'
67
+
68
+ attr_accessible :first_name, :best_friend_first_name, :best_friend_attributes
69
+ attr_accessible :first_name, :gender, :comments, :as => :admin
70
+ attr_accessible :best_friend_attributes, :best_friend_first_name, :as => :admin
71
+
72
+ has_one :best_friend, :class_name => 'NestedPerson', :foreign_key => :best_friend_id
73
+ accepts_nested_attributes_for :best_friend, :update_only => true
74
+
75
+ def comments=(new_comments)
76
+ raise RuntimeError
77
+ end
78
+
79
+ def best_friend_first_name=(new_name)
80
+ assign_attributes({ :best_friend_attributes => { :first_name => new_name } })
81
+ end
82
+ end