devise-security 0.13.0 → 0.14.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -1
  3. data/.gitignore +1 -0
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +30 -12
  6. data/Appraisals +21 -5
  7. data/Gemfile +8 -1
  8. data/README.md +47 -7
  9. data/app/views/devise/paranoid_verification_code/show.html.erb +1 -1
  10. data/app/views/devise/password_expired/show.html.erb +1 -1
  11. data/config/locales/de.yml +11 -11
  12. data/config/locales/fr.yml +13 -13
  13. data/config/locales/ja.yml +29 -0
  14. data/devise-security.gemspec +12 -6
  15. data/gemfiles/rails_4.2_stable.gemfile +9 -1
  16. data/gemfiles/rails_5.0_stable.gemfile +8 -1
  17. data/gemfiles/rails_5.1_stable.gemfile +8 -1
  18. data/gemfiles/{rails_5.2.0.gemfile → rails_5.2_stable.gemfile} +8 -1
  19. data/gemfiles/rails_6.0_beta.gemfile +15 -0
  20. data/lib/devise-security.rb +2 -4
  21. data/lib/devise-security/models/{old_password.rb → active_record/old_password.rb} +1 -2
  22. data/lib/devise-security/models/compatibility.rb +6 -15
  23. data/lib/devise-security/models/compatibility/active_record.rb +29 -0
  24. data/lib/devise-security/models/compatibility/mongoid.rb +21 -0
  25. data/lib/devise-security/models/database_authenticatable_patch.rb +1 -1
  26. data/lib/devise-security/models/expirable.rb +6 -2
  27. data/lib/devise-security/models/mongoid/old_password.rb +21 -0
  28. data/lib/devise-security/models/password_archivable.rb +16 -7
  29. data/lib/devise-security/models/password_expirable.rb +5 -0
  30. data/lib/devise-security/models/secure_validatable.rb +2 -2
  31. data/lib/devise-security/orm/mongoid.rb +7 -0
  32. data/lib/devise-security/patches/controller_security_question.rb +1 -1
  33. data/lib/devise-security/version.rb +1 -1
  34. data/lib/generators/devise_security/install_generator.rb +1 -1
  35. data/test/dummy/app/models/application_record.rb +8 -2
  36. data/test/dummy/app/models/application_user_record.rb +11 -0
  37. data/test/dummy/app/models/captcha_user.rb +5 -2
  38. data/test/dummy/app/models/mongoid/confirmable_fields.rb +13 -0
  39. data/test/dummy/app/models/mongoid/database_authenticable_fields.rb +17 -0
  40. data/test/dummy/app/models/mongoid/expirable_fields.rb +11 -0
  41. data/test/dummy/app/models/mongoid/lockable_fields.rb +13 -0
  42. data/test/dummy/app/models/mongoid/mappings.rb +13 -0
  43. data/test/dummy/app/models/mongoid/omniauthable_fields.rb +11 -0
  44. data/test/dummy/app/models/mongoid/paranoid_verification_fields.rb +10 -0
  45. data/test/dummy/app/models/mongoid/password_archivable_fields.rb +9 -0
  46. data/test/dummy/app/models/mongoid/password_expirable_fields.rb +10 -0
  47. data/test/dummy/app/models/mongoid/recoverable_fields.rb +11 -0
  48. data/test/dummy/app/models/mongoid/registerable_fields.rb +19 -0
  49. data/test/dummy/app/models/mongoid/rememberable_fields.rb +10 -0
  50. data/test/dummy/app/models/mongoid/secure_validatable_fields.rb +11 -0
  51. data/test/dummy/app/models/mongoid/security_questionable_fields.rb +13 -0
  52. data/test/dummy/app/models/mongoid/session_limitable_fields.rb +10 -0
  53. data/test/dummy/app/models/mongoid/timeoutable_fields.rb +9 -0
  54. data/test/dummy/app/models/mongoid/trackable_fields.rb +14 -0
  55. data/test/dummy/app/models/mongoid/validatable_fields.rb +7 -0
  56. data/test/dummy/app/models/secure_user.rb +5 -1
  57. data/test/dummy/app/models/security_question_user.rb +7 -4
  58. data/test/dummy/app/models/user.rb +5 -0
  59. data/test/dummy/app/models/widget.rb +4 -0
  60. data/test/dummy/app/mongoid/admin.rb +31 -0
  61. data/test/dummy/app/mongoid/one_user.rb +58 -0
  62. data/test/dummy/app/mongoid/shim.rb +25 -0
  63. data/test/dummy/app/mongoid/user_on_engine.rb +41 -0
  64. data/test/dummy/app/mongoid/user_on_main_app.rb +41 -0
  65. data/test/dummy/app/mongoid/user_with_validations.rb +37 -0
  66. data/test/dummy/app/mongoid/user_without_email.rb +35 -0
  67. data/test/dummy/config/application.rb +10 -7
  68. data/test/dummy/config/environments/test.rb +2 -2
  69. data/test/dummy/config/initializers/devise.rb +3 -4
  70. data/test/dummy/config/initializers/migration_class.rb +8 -6
  71. data/test/dummy/config/mongoid.yml +6 -0
  72. data/test/dummy/lib/shared_expirable_columns.rb +14 -0
  73. data/test/dummy/lib/shared_security_questions_fields.rb +16 -0
  74. data/test/dummy/lib/shared_user.rb +32 -0
  75. data/test/dummy/lib/shared_user_with_password_verification.rb +13 -0
  76. data/test/dummy/lib/shared_user_without_email.rb +28 -0
  77. data/test/dummy/lib/shared_user_without_omniauth.rb +15 -0
  78. data/test/dummy/lib/shared_verification_fields.rb +15 -0
  79. data/test/orm/active_record.rb +12 -0
  80. data/test/orm/mongoid.rb +12 -0
  81. data/test/support/mongoid.yml +6 -0
  82. data/test/test_helper.rb +16 -9
  83. data/test/test_install_generator.rb +1 -0
  84. data/test/test_password_archivable.rb +6 -7
  85. data/test/test_password_expirable.rb +3 -1
  86. data/test/test_secure_validatable.rb +11 -10
  87. data/test/test_security_question_controller.rb +9 -11
  88. metadata +125 -16
@@ -2,7 +2,14 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "omniauth"
6
5
  gem "rails", "~> 5.2.0"
7
6
 
7
+ group :active_record do
8
+ gem "sqlite3", "~> 1.3.0"
9
+ end
10
+
11
+ group :mongoid do
12
+ gem "mongoid", "~> 6.0"
13
+ end
14
+
8
15
  gemspec path: "../"
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 6.0.0.beta1"
6
+
7
+ group :active_record do
8
+ gem "sqlite3", "~> 1.3.0"
9
+ end
10
+
11
+ group :mongoid do
12
+ gem "mongoid", "~> 6.0"
13
+ end
14
+
15
+ gemspec path: "../"
@@ -1,13 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record'
3
+ require DEVISE_ORM.to_s if DEVISE_ORM.in? [:active_record, :mongoid]
4
4
  require 'active_support/core_ext/integer'
5
5
  require 'active_support/ordered_hash'
6
6
  require 'active_support/concern'
7
7
  require 'devise'
8
8
 
9
9
  module Devise
10
-
11
10
  # Number of seconds that passwords are valid (e.g 3.months)
12
11
  # Disable pasword expiration with +false+
13
12
  # Expire only on demand with +true+
@@ -104,7 +103,6 @@ Devise.add_module :paranoid_verification, controller: :paranoid_verification_cod
104
103
  # requires
105
104
  require 'devise-security/routes'
106
105
  require 'devise-security/rails'
107
- require 'devise-security/orm/active_record'
108
- require 'devise-security/models/old_password'
106
+ require "devise-security/orm/#{DEVISE_ORM}"
109
107
  require 'devise-security/models/database_authenticatable_patch'
110
108
  require 'devise-security/models/paranoid_verification'
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record'
4
- class OldPassword < ActiveRecord::Base
3
+ class OldPassword < ApplicationRecord
5
4
  belongs_to :password_archivable, polymorphic: true
6
5
  end
@@ -1,24 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "compatibility/#{DEVISE_ORM}"
4
+
3
5
  module Devise
4
6
  module Models
7
+ # These compatibility modules define methods used by devise-security
8
+ # that may need to be defined or re-defined for compatibility between ORMs
9
+ # and/or older versions of ORMs.
5
10
  module Compatibility
6
11
  extend ActiveSupport::Concern
7
-
8
- # for backwards compatibility with Rails < 5.1.x
9
- unless Devise.activerecord51?
10
- def saved_change_to_encrypted_password?
11
- encrypted_password_changed?
12
- end
13
-
14
- def encrypted_password_before_last_save
15
- previous_changes['encrypted_password'].try(:first)
16
- end
17
-
18
- def will_save_change_to_encrypted_password?
19
- changed_attributes['encrypted_password'].present?
20
- end
21
- end
12
+ include "Devise::Models::Compatibility::#{DEVISE_ORM.to_s.classify}".constantize
22
13
  end
23
14
  end
24
15
  end
@@ -0,0 +1,29 @@
1
+ module Devise
2
+ module Models
3
+ module Compatibility
4
+ module ActiveRecord
5
+ extend ActiveSupport::Concern
6
+ unless Devise.activerecord51?
7
+ # When the record was saved, was the +encrypted_password+ changed?
8
+ # @return [Boolean]
9
+ def saved_change_to_encrypted_password?
10
+ encrypted_password_changed?
11
+ end
12
+
13
+ # The encrypted password that existed before the record was saved
14
+ # @return [String]
15
+ # @return [nil] if an +encrypted_password+ had not been set
16
+ def encrypted_password_before_last_save
17
+ previous_changes['encrypted_password'].try(:first)
18
+ end
19
+
20
+ # When the record is saved, will the +encrypted_password+ be changed?
21
+ # @return [Boolean]
22
+ def will_save_change_to_encrypted_password?
23
+ changed_attributes['encrypted_password'].present?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ module Devise
2
+ module Models
3
+ module Compatibility
4
+ module Mongoid
5
+ extend ActiveSupport::Concern
6
+
7
+ # Will saving this record change the +email+ attribute?
8
+ # @return [Boolean]
9
+ def will_save_change_to_email?
10
+ changed.include? 'email'
11
+ end
12
+
13
+ # Will saving this record change the +encrypted_password+ attribute?
14
+ # @return [Boolean]
15
+ def will_save_change_to_encrypted_password?
16
+ changed.include? 'encrypted_password'
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -10,7 +10,7 @@ module Devise
10
10
  new_password_confirmation = params[:password_confirmation]
11
11
 
12
12
  result = if valid_password?(current_password) && new_password.present? && new_password_confirmation.present?
13
- update_attributes(params, *options)
13
+ update(params, *options)
14
14
  else
15
15
  self.assign_attributes(params, *options)
16
16
  self.valid?
@@ -22,7 +22,11 @@ module Devise
22
22
 
23
23
  # Updates +last_activity_at+, called from a Warden::Manager.after_set_user hook.
24
24
  def update_last_activity!
25
- self.update_column(:last_activity_at, Time.now.utc)
25
+ if respond_to?(:update_column)
26
+ self.update_column(:last_activity_at, Time.now.utc)
27
+ elsif defined? Mongoid
28
+ self.update_attribute(:last_activity_at, Time.now.utc)
29
+ end
26
30
  end
27
31
 
28
32
  # Tells if the account has expired
@@ -103,7 +107,7 @@ module Devise
103
107
  # @example Overwritten version to blank out the object.
104
108
  # def self.delete_all_expired_for(time = 90.days)
105
109
  # expired_for(time).each do |u|
106
- # u.update_attributes first_name: nil, last_name: nil
110
+ # u.update first_name: nil, last_name: nil
107
111
  # end
108
112
  # end
109
113
  def delete_all_expired_for(time)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OldPassword
4
+ include Mongoid::Document
5
+
6
+ ## Database authenticatable
7
+ field :encrypted_password, type: String
8
+ validates_presence_of :encrypted_password
9
+ field :password_salt, type: String
10
+
11
+ field :password_archivable_type, type: String
12
+ validates_presence_of :password_archivable_type
13
+
14
+ field :password_archivable_id, type: String
15
+ validates_presence_of :password_archivable_id
16
+ index({ password_archivable_type: 1, password_archivable_id: 1 }, name: :index_password_archivable)
17
+
18
+ include Mongoid::Timestamps
19
+
20
+ belongs_to :password_archivable, polymorphic: true
21
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'compatibility'
4
+ require_relative "#{DEVISE_ORM}/old_password"
4
5
 
5
6
  module Devise
6
7
  module Models
@@ -11,7 +12,7 @@ module Devise
11
12
  include Devise::Models::DatabaseAuthenticatable
12
13
 
13
14
  included do
14
- has_many :old_passwords, as: :password_archivable, dependent: :destroy
15
+ has_many :old_passwords, class_name: 'OldPassword', as: :password_archivable, dependent: :destroy
15
16
  before_update :archive_password, if: :will_save_change_to_encrypted_password?
16
17
  validate :validate_password_archive, if: :password_present?
17
18
  end
@@ -38,11 +39,15 @@ module Devise
38
39
  # @return [true] if current password was used previously
39
40
  # @return [false] if disabled or not previously used
40
41
  def password_archive_included?
41
- return false unless max_old_passwords > 0
42
- old_passwords_including_cur_change = old_passwords.order(:id).reverse_order.limit(max_old_passwords).pluck(:encrypted_password)
42
+ return false unless max_old_passwords.positive?
43
+
44
+ old_passwords_including_cur_change = old_passwords.order(created_at: :desc).limit(max_old_passwords).pluck(:encrypted_password)
43
45
  old_passwords_including_cur_change << encrypted_password_was # include most recent change in list, but don't save it yet!
44
46
  old_passwords_including_cur_change.any? do |old_password|
45
- self.class.new(encrypted_password: old_password).valid_password?(password)
47
+ # NOTE: we deliberately do not do mass assignment here so that users that
48
+ # rely on `protected_attributes_continued` gem can still use this extension.
49
+ # See issue #68
50
+ self.class.new.tap { |object| object.encrypted_password = old_password }.valid_password?(password)
46
51
  end
47
52
  end
48
53
 
@@ -60,11 +65,15 @@ module Devise
60
65
 
61
66
  private
62
67
 
63
- # archive the last password before save and delete all to old passwords from archive
68
+ # Archive the last password before save and delete all to old passwords from archive
69
+ # @note we check to see if an old password has already been archived because
70
+ # mongoid will keep re-triggering this callback when we add an old password
64
71
  def archive_password
65
- if max_old_passwords > 0
72
+ if max_old_passwords.positive?
73
+ return true if old_passwords.where(encrypted_password: encrypted_password_was).exists?
74
+
66
75
  old_passwords.create!(encrypted_password: encrypted_password_was) if encrypted_password_was.present?
67
- old_passwords.order(:id).reverse_order.offset(max_old_passwords).destroy_all
76
+ old_passwords.order(created_at: :desc).offset(max_old_passwords).destroy_all
68
77
  else
69
78
  old_passwords.destroy_all
70
79
  end
@@ -39,6 +39,7 @@ module Devise::Models
39
39
  # @return [Boolean]
40
40
  def need_change_password!
41
41
  return unless password_expiration_enabled?
42
+
42
43
  need_change_password
43
44
  save(validate: false)
44
45
  end
@@ -51,6 +52,7 @@ module Devise::Models
51
52
  # @return [void]
52
53
  def need_change_password
53
54
  return unless password_expiration_enabled?
55
+
54
56
  self.password_changed_at = nil
55
57
  end
56
58
  alias expire_password need_change_password
@@ -70,6 +72,7 @@ module Devise::Models
70
72
  def password_change_requested?
71
73
  return false unless password_expiration_enabled?
72
74
  return false if new_record?
75
+
73
76
  password_changed_at.nil?
74
77
  end
75
78
 
@@ -79,6 +82,7 @@ module Devise::Models
79
82
  return false if new_record?
80
83
  return false unless password_expiration_enabled?
81
84
  return false if expire_password_on_demand?
85
+
82
86
  password_changed_at < expire_password_after.seconds.ago
83
87
  end
84
88
  alias password_expired? password_too_old?
@@ -89,6 +93,7 @@ module Devise::Models
89
93
  # @note called as a +before_save+ hook
90
94
  def update_password_changed
91
95
  return unless (new_record? || encrypted_password_changed?) && !password_changed_at_changed?
96
+
92
97
  self.password_changed_at = Time.zone.now
93
98
  end
94
99
 
@@ -90,8 +90,8 @@ module Devise
90
90
 
91
91
  def has_uniqueness_validation_of_login?
92
92
  validators.any? do |validator|
93
- validator.kind_of?(ActiveRecord::Validations::UniquenessValidator) &&
94
- validator.attributes.include?(login_attribute)
93
+ validator_orm_klass = DEVISE_ORM == :active_record ? ActiveRecord::Validations::UniquenessValidator : ::Mongoid::Validatable::UniquenessValidator
94
+ validator.kind_of?(validator_orm_klass) && validator.attributes.include?(login_attribute)
95
95
  end
96
96
  end
97
97
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveSupport.on_load(:mongoid) do
4
+ require 'orm_adapter/adapters/mongoid'
5
+
6
+ Mongoid::Document::ClassMethods.send :include, Devise::Models
7
+ end
@@ -9,6 +9,7 @@ module DeviseSecurity::Patches
9
9
  end
10
10
 
11
11
  private
12
+
12
13
  def check_security_question
13
14
  # only find via email, not login
14
15
  resource = resource_class.find_or_initialize_with_error_by(:email, params[resource_name][:email], :not_found)
@@ -19,4 +20,3 @@ module DeviseSecurity::Patches
19
20
  end
20
21
  end
21
22
  end
22
-
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeviseSecurity
4
- VERSION = '0.13.0'
4
+ VERSION = '0.14.0.rc1'
5
5
  end
@@ -4,7 +4,7 @@ module DeviseSecurity
4
4
  module Generators
5
5
  # Generator for Rails to create or append to a Devise initializer.
6
6
  class InstallGenerator < Rails::Generators::Base
7
- LOCALES = %w[en es de fr it tr].freeze
7
+ LOCALES = %w[en es de fr it ja tr].freeze
8
8
 
9
9
  source_root File.expand_path('../../templates', __FILE__)
10
10
  desc 'Install the devise security extension'
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ApplicationRecord < ActiveRecord::Base
4
- self.abstract_class = true
3
+ if DEVISE_ORM == :active_record
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ else
8
+ class ApplicationRecord
9
+ include Mongoid::Document
10
+ end
5
11
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ if DEVISE_ORM == :active_record
3
+ class ApplicationUserRecord < ActiveRecord::Base
4
+ self.table_name = 'users'
5
+ end
6
+ else
7
+ class ApplicationUserRecord
8
+ include Mongoid::Document
9
+ store_in collection: 'users'
10
+ end
11
+ end
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CaptchaUser < ApplicationRecord
4
- self.table_name = 'users'
3
+ class CaptchaUser < ApplicationUserRecord
5
4
  devise :database_authenticatable, :password_archivable,
6
5
  :paranoid_verification, :password_expirable
6
+ if DEVISE_ORM == :mongoid
7
+ require './test/dummy/app/models/mongoid/mappings'
8
+ include ::Mongoid::Mappings
9
+ end
7
10
  end
@@ -0,0 +1,13 @@
1
+ module ConfirmableFields
2
+ extend ::ActiveSupport::Concern
3
+
4
+ included do
5
+ include Mongoid::Document
6
+
7
+ ## Confirmable
8
+ field :confirmation_token, type: String
9
+ field :confirmed_at, type: Time
10
+ field :confirmation_sent_at, type: Time
11
+ field :unconfirmed_email, type: String # Only if using reconfirmable
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module DatabaseAuthenticatableFields
2
+ extend ::ActiveSupport::Concern
3
+
4
+ included do
5
+ include Mongoid::Document
6
+
7
+ ## Database authenticatable
8
+ field :username, type: String
9
+ field :email, type: String, default: ""
10
+ #validates_presence_of :email
11
+
12
+ field :encrypted_password, type: String, default: ""
13
+ validates_presence_of :encrypted_password
14
+
15
+ include Mongoid::Timestamps
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module ExpirableFields
2
+ extend ::ActiveSupport::Concern
3
+
4
+ included do
5
+ include Mongoid::Document
6
+
7
+ ## Expirable
8
+ field :expired_at, type: Time
9
+ field :last_activity_at, type: Time
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module LockableFields
2
+ extend ::ActiveSupport::Concern
3
+
4
+ included do
5
+ include Mongoid::Document
6
+
7
+ field :failed_attempts, type: Integer, default: 0 # Only if lock strategy is :failed_attempts
8
+ field :unlock_token, type: String # Only if unlock strategy is :email or :both
9
+ field :locked_at, type: Time
10
+ include Mongoid::Timestamps
11
+ index({ unlock_token: 1 }, { unique: true })
12
+ end
13
+ end