aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md5
-rw-r--r--activerecord/lib/active_record.rb1
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb35
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb33
-rw-r--r--activerecord/lib/active_record/base.rb1
-rw-r--r--activerecord/lib/active_record/secure_token.rb49
-rw-r--r--activerecord/lib/active_record/type/boolean.rb4
-rw-r--r--activerecord/test/cases/finder_test.rb2
-rw-r--r--activerecord/test/cases/secure_token_test.rb39
-rw-r--r--activerecord/test/cases/transactions_test.rb41
-rw-r--r--activerecord/test/models/user.rb4
-rw-r--r--activerecord/test/schema/schema.rb5
12 files changed, 148 insertions, 71 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index d1eee8e73e..5588b24851 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,8 @@
+* Added ActiveRecord::SecureToken in order to encapsulate generation of
+ unique tokens for attributes in a model using SecureRandom
+
+ *Roberto Miranda*
+
* Change the behavior of boolean columns to be closer to Ruby's semantics.
Before this change we had a small set of "truthy", and all others are "falsy".
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 2eec62846b..d9d47c3d99 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -67,6 +67,7 @@ module ActiveRecord
autoload :Transactions
autoload :Translation
autoload :Validations
+ autoload :SecureToken
eager_autoload do
autoload :ActiveRecordError, 'active_record/errors'
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index f6ab543015..4b72fe7d7e 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/module/method_transplanting'
-
module ActiveRecord
module AttributeMethods
module Read
@@ -38,29 +36,22 @@ module ActiveRecord
module ClassMethods
protected
- if Module.methods_transplantable?
- def define_method_attribute(name)
- method = ReaderMethodCache[name]
- generated_attribute_methods.module_eval { define_method name, method }
- end
- else
- def define_method_attribute(name)
- safe_name = name.unpack('h*').first
- temp_method = "__temp__#{safe_name}"
-
- ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+ def define_method_attribute(name)
+ safe_name = name.unpack('h*').first
+ temp_method = "__temp__#{safe_name}"
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def #{temp_method}
- name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
- _read_attribute(name) { |n| missing_attribute(n, caller) }
- end
- STR
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
- generated_attribute_methods.module_eval do
- alias_method name, temp_method
- undef_method temp_method
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def #{temp_method}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ _read_attribute(name) { |n| missing_attribute(n, caller) }
end
+ STR
+
+ generated_attribute_methods.module_eval do
+ alias_method name, temp_method
+ undef_method temp_method
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 16804f86bf..ab017c7b54 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/module/method_transplanting'
-
module ActiveRecord
module AttributeMethods
module Write
@@ -25,27 +23,18 @@ module ActiveRecord
module ClassMethods
protected
- if Module.methods_transplantable?
- def define_method_attribute=(name)
- method = WriterMethodCache[name]
- generated_attribute_methods.module_eval {
- define_method "#{name}=", method
- }
- end
- else
- def define_method_attribute=(name)
- safe_name = name.unpack('h*').first
- ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+ def define_method_attribute=(name)
+ safe_name = name.unpack('h*').first
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}=(value)
- name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
- write_attribute(name, value)
- end
- alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
- undef_method :__temp__#{safe_name}=
- STR
- end
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__#{safe_name}=(value)
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ write_attribute(name, value)
+ end
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
+ undef_method :__temp__#{safe_name}=
+ STR
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index bb01231bca..100d3780f6 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -312,6 +312,7 @@ module ActiveRecord #:nodoc:
include Reflection
include Serialization
include Store
+ include SecureToken
end
ActiveSupport.run_load_hooks(:active_record, Base)
diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb
new file mode 100644
index 0000000000..23d4292cbb
--- /dev/null
+++ b/activerecord/lib/active_record/secure_token.rb
@@ -0,0 +1,49 @@
+module ActiveRecord
+ module SecureToken
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Example using has_secure_token
+ #
+ # # Schema: User(toke:string, auth_token:string)
+ # class User < ActiveRecord::Base
+ # has_secure_token
+ # has_secure_token :auth_token
+ # end
+ #
+ # user = User.new
+ # user.save
+ # user.token # => "44539a6a59835a4ee9d7b112"
+ # user.auth_token # => "e2426a93718d1817a43abbaa"
+ # user.regenerate_token # => true
+ # user.regenerate_auth_token # => true
+ #
+ # SecureRandom is used to generate the 24-character unique token, so collisions are highly unlikely.
+ # We'll check to see if the generated token has been used already using #exists?, and retry up to 10
+ # times to find another unused token. After that a RuntimeError is raised if the problem persists.
+ #
+ # Note that it's still possible to generate a race condition in the database in the same way that
+ # validates_presence_of can. You're encouraged to add a unique index in the database to deal with
+ # this even more unlikely scenario.
+ def has_secure_token(attribute = :token)
+ # Load securerandom only when has_secure_key is used.
+ require 'securerandom'
+ define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token(attribute) }
+ before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token(attribute)) }
+ end
+
+ def generate_unique_secure_token(attribute)
+ 10.times do |i|
+ SecureRandom.hex(12).tap do |token|
+ if exists?(attribute => token)
+ raise "Couldn't generate a unique token in 10 attempts!" if i == 9
+ else
+ return token
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb
index 2e24afc7c0..f6a75512fd 100644
--- a/activerecord/lib/active_record/type/boolean.rb
+++ b/activerecord/lib/active_record/type/boolean.rb
@@ -10,10 +10,8 @@ module ActiveRecord
def cast_value(value)
if value == ''
nil
- elsif ConnectionAdapters::Column::FALSE_VALUES.include?(value)
- false
else
- true
+ !ConnectionAdapters::Column::FALSE_VALUES.include?(value)
end
end
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 02dc5d3ad3..39308866ee 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -55,7 +55,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_symbols_table_ref
- gc_disabled = GC.disable if RUBY_VERSION >= '2.2.0'
+ gc_disabled = GC.disable
Post.where("author_id" => nil) # warm up
x = Symbol.all_symbols.count
Post.where("title" => {"xxxqqqq" => "bar"})
diff --git a/activerecord/test/cases/secure_token_test.rb b/activerecord/test/cases/secure_token_test.rb
new file mode 100644
index 0000000000..400fce8c6e
--- /dev/null
+++ b/activerecord/test/cases/secure_token_test.rb
@@ -0,0 +1,39 @@
+require 'cases/helper'
+require 'models/user'
+
+class SecureTokenTest < ActiveRecord::TestCase
+ setup do
+ @user = User.new
+ end
+
+ test "assing unique token values" do
+ @user.save
+ assert_not_nil @user.token
+ assert_not_nil @user.auth_token
+ end
+
+ test "regenerate the secure key for the attribute" do
+ @user.save
+ old_token = @user.token
+ old_auth_token = @user.auth_token
+ @user.regenerate_token
+ @user.regenerate_auth_token
+
+ assert_not_equal @user.token, old_token
+ assert_not_equal @user.auth_token, old_auth_token
+ end
+
+ test "raise and exception when with 10 attemps is reached" do
+ User.stubs(:exists?).returns(*Array.new(10, true))
+ assert_raises(RuntimeError) do
+ @user.save
+ end
+ end
+
+ test "assing unique token after 9 attemps reached" do
+ User.stubs(:exists?).returns(*Array.new(10){ |i| i == 9 ? false : true})
+ @user.save
+ assert_not_nil @user.token
+ assert_not_nil @user.auth_token
+ end
+end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index e0aecb5996..c4f2ed474d 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -503,35 +503,30 @@ class TransactionTest < ActiveRecord::TestCase
assert topic.frozen?, 'not frozen'
end
- # The behavior of killed threads having a status of "aborting" was changed
- # in Ruby 2.0, so Thread#kill on 1.9 will prematurely commit the transaction
- # and there's nothing we can do about it.
- if !RUBY_VERSION.start_with?('1.9') && !in_memory_db?
- def test_rollback_when_thread_killed
- queue = Queue.new
- thread = Thread.new do
- Topic.transaction do
- @first.approved = true
- @second.approved = false
- @first.save
+ def test_rollback_when_thread_killed
+ queue = Queue.new
+ thread = Thread.new do
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
- queue.push nil
- sleep
+ queue.push nil
+ sleep
- @second.save
- end
+ @second.save
end
+ end
- queue.pop
- thread.kill
- thread.join
+ queue.pop
+ thread.kill
+ thread.join
- assert @first.approved?, "First should still be changed in the objects"
- assert !@second.approved?, "Second should still be changed in the objects"
+ assert @first.approved?, "First should still be changed in the objects"
+ assert !@second.approved?, "Second should still be changed in the objects"
- assert !Topic.find(1).approved?, "First shouldn't have been approved"
- assert Topic.find(2).approved?, "Second should still be approved"
- end
+ assert !Topic.find(1).approved?, "First shouldn't have been approved"
+ assert Topic.find(2).approved?, "Second should still be approved"
end
def test_restore_active_record_state_for_all_records_in_a_transaction
diff --git a/activerecord/test/models/user.rb b/activerecord/test/models/user.rb
new file mode 100644
index 0000000000..23cd2e0e1c
--- /dev/null
+++ b/activerecord/test/models/user.rb
@@ -0,0 +1,4 @@
+class User < ActiveRecord::Base
+ has_secure_token
+ has_secure_token :auth_token
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 5907d6ef97..e283f7a9cc 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -892,6 +892,11 @@ ActiveRecord::Schema.define do
t.string :overloaded_string_with_limit, limit: 255
t.string :string_with_default, default: 'the original default'
end
+
+ create_table :users, force: true do |t|
+ t.string :token
+ t.string :auth_token
+ end
end
Course.connection.create_table :courses, force: true do |t|