aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/CHANGELOG5
-rw-r--r--activemodel/activemodel.gemspec2
-rw-r--r--activemodel/lib/active_model.rb1
-rw-r--r--activemodel/lib/active_model/dirty.rb1
-rw-r--r--activemodel/lib/active_model/secure_password.rb58
-rw-r--r--activemodel/lib/active_model/validations.rb12
-rw-r--r--activemodel/test/cases/secure_password_test.rb32
-rw-r--r--activemodel/test/cases/validations_test.rb10
-rw-r--r--activemodel/test/models/user.rb8
9 files changed, 125 insertions, 4 deletions
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG
index 4e963c77b0..9dd5e03685 100644
--- a/activemodel/CHANGELOG
+++ b/activemodel/CHANGELOG
@@ -1,15 +1,18 @@
*Rails 3.1.0 (unreleased)*
-* No changes
+* Added ActiveModel::SecurePassword to encapsulate dead-simple password usage with BCrypt encryption and salting [DHH]
+
*Rails 3.0.2 (unreleased)*
* No changes
+
*Rails 3.0.1 (October 15, 2010)*
* No Changes, just a version bump.
+
*Rails 3.0.0 (August 29, 2010)*
* Added ActiveModel::MassAssignmentSecurity [Eric Chapweske, Josh Kalderimis]
diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec
index 1f38e70c36..64aa7ad922 100644
--- a/activemodel/activemodel.gemspec
+++ b/activemodel/activemodel.gemspec
@@ -22,4 +22,6 @@ Gem::Specification.new do |s|
s.add_dependency('activesupport', version)
s.add_dependency('builder', '~> 3.0.0')
s.add_dependency('i18n', '~> 0.5.0')
+ s.add_dependency('bcrypt-ruby', '~> 2.1.2')
+
end
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index be0f24ff92..dd6ee058cc 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -42,6 +42,7 @@ module ActiveModel
autoload :Naming
autoload :Observer, 'active_model/observing'
autoload :Observing
+ autoload :SecurePassword
autoload :Serialization
autoload :TestCase
autoload :Translation
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 1dfd0b6132..a479795d51 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -1,5 +1,4 @@
require 'active_model/attribute_methods'
-require 'active_support/concern'
require 'active_support/hash_with_indifferent_access'
require 'active_support/core_ext/object/duplicable'
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
new file mode 100644
index 0000000000..52941942b8
--- /dev/null
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -0,0 +1,58 @@
+require 'bcrypt'
+
+module ActiveModel
+ module SecurePassword
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Adds methods to set and authenticate against a BCrypt password.
+ # This mechanism requires you to have a password_digest attribute.
+ #
+ # Validations for presence of password, confirmation of password (using
+ # a "password_confirmation" attribute) are automatically added.
+ # You can add more validations by hand if need be.
+ #
+ # Example using Active Record (which automatically includes ActiveModel::SecurePassword):
+ #
+ # # Schema: User(name:string, password_digest:string)
+ # class User < ActiveRecord::Base
+ # has_secure_password
+ # end
+ #
+ # user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch")
+ # user.save # => false, password required
+ # user.password = "mUc3m00RsqyRe"
+ # user.save # => false, confirmation doesn't match
+ # user.password_confirmation = "mUc3m00RsqyRe"
+ # user.save # => true
+ # user.authenticate("notright") # => false
+ # user.authenticate("mUc3m00RsqyRe") # => user
+ # User.find_by_name("david").try(:authenticate, "notright") # => nil
+ # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user
+ def has_secure_password
+ attr_reader :password
+ attr_accessor :password_confirmation
+
+ attr_protected(:password_digest) if respond_to?(:attr_protected)
+
+ validates_confirmation_of :password
+ validates_presence_of :password_digest
+ end
+ end
+
+ # Returns self if the password is correct, otherwise false.
+ def authenticate(unencrypted_password)
+ if BCrypt::Password.new(password_digest) == unencrypted_password
+ self
+ else
+ false
+ end
+ end
+
+ # Encrypts the password into the password_digest attribute.
+ def password=(unencrypted_password)
+ @password = unencrypted_password
+ self.password_digest = BCrypt::Password.create(unencrypted_password)
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index b044caa8d3..6cb015a144 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -104,7 +104,7 @@ module ActiveModel
# end
# end
#
- # Or with a block which is passed with the current record to be validated:
+ # With a block which is passed with the current record to be validated:
#
# class Comment
# include ActiveModel::Validations
@@ -118,6 +118,16 @@ module ActiveModel
# end
# end
#
+ # Or with a block where self points to the current record to be validated:
+ #
+ # class Comment
+ # include ActiveModel::Validations
+ #
+ # validate do
+ # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee)
+ # end
+ # end
+ #
def validate(*args, &block)
options = args.extract_options!
if options.key?(:on)
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
new file mode 100644
index 0000000000..79be715730
--- /dev/null
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -0,0 +1,32 @@
+require 'cases/helper'
+require 'models/user'
+
+class SecurePasswordTest < ActiveModel::TestCase
+
+ setup do
+ @user = User.new
+ end
+
+ test "password must be present" do
+ assert !@user.valid?
+ assert_equal 1, @user.errors.size
+ end
+
+ test "password must match confirmation" do
+ @user.password = "thiswillberight"
+ @user.password_confirmation = "wrong"
+
+ assert !@user.valid?
+
+ @user.password_confirmation = "thiswillberight"
+
+ assert @user.valid?
+ end
+
+ test "authenticate" do
+ @user.password = "secret"
+
+ assert !@user.authenticate("wrong")
+ assert @user.authenticate("secret")
+ end
+end
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
index 55b477dd10..e90dc7d4e3 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -148,6 +148,14 @@ class ValidationsTest < ActiveModel::TestCase
end
def test_validate_block
+ Topic.validate { errors.add("title", "will never be valid") }
+ t = Topic.new("title" => "Title", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["will never be valid"], t.errors["title"]
+ end
+
+ def test_validate_block_with_params
Topic.validate { |topic| topic.errors.add("title", "will never be valid") }
t = Topic.new("title" => "Title", "content" => "whatever")
assert t.invalid?
@@ -187,7 +195,7 @@ class ValidationsTest < ActiveModel::TestCase
assert t.invalid?
assert_equal "can't be blank", t.errors["title"].first
Topic.validates_presence_of :title, :author_name
- Topic.validate {|topic| topic.errors.add('author_email_address', 'will never be valid')}
+ Topic.validate {errors.add('author_email_address', 'will never be valid')}
Topic.validates_length_of :title, :content, :minimum => 2
t = Topic.new :title => ''
diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb
new file mode 100644
index 0000000000..e221bb8091
--- /dev/null
+++ b/activemodel/test/models/user.rb
@@ -0,0 +1,8 @@
+class User
+ include ActiveModel::Validations
+ include ActiveModel::SecurePassword
+
+ has_secure_password
+
+ attr_accessor :password_digest, :password_salt
+end