authlogic-nicho 6.5

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/lib/authlogic/acts_as_authentic/base.rb +116 -0
  3. data/lib/authlogic/acts_as_authentic/email.rb +30 -0
  4. data/lib/authlogic/acts_as_authentic/logged_in_status.rb +85 -0
  5. data/lib/authlogic/acts_as_authentic/login.rb +63 -0
  6. data/lib/authlogic/acts_as_authentic/magic_columns.rb +38 -0
  7. data/lib/authlogic/acts_as_authentic/password.rb +357 -0
  8. data/lib/authlogic/acts_as_authentic/perishable_token.rb +122 -0
  9. data/lib/authlogic/acts_as_authentic/persistence_token.rb +70 -0
  10. data/lib/authlogic/acts_as_authentic/queries/case_sensitivity.rb +53 -0
  11. data/lib/authlogic/acts_as_authentic/queries/find_with_case.rb +83 -0
  12. data/lib/authlogic/acts_as_authentic/session_maintenance.rb +186 -0
  13. data/lib/authlogic/acts_as_authentic/single_access_token.rb +83 -0
  14. data/lib/authlogic/config.rb +43 -0
  15. data/lib/authlogic/controller_adapters/abstract_adapter.rb +119 -0
  16. data/lib/authlogic/controller_adapters/rack_adapter.rb +72 -0
  17. data/lib/authlogic/controller_adapters/rails_adapter.rb +47 -0
  18. data/lib/authlogic/controller_adapters/sinatra_adapter.rb +67 -0
  19. data/lib/authlogic/cookie_credentials.rb +63 -0
  20. data/lib/authlogic/crypto_providers/bcrypt.rb +113 -0
  21. data/lib/authlogic/crypto_providers/md5/v2.rb +35 -0
  22. data/lib/authlogic/crypto_providers/md5.rb +36 -0
  23. data/lib/authlogic/crypto_providers/scrypt.rb +92 -0
  24. data/lib/authlogic/crypto_providers/sha1/v2.rb +41 -0
  25. data/lib/authlogic/crypto_providers/sha1.rb +42 -0
  26. data/lib/authlogic/crypto_providers/sha256/v2.rb +58 -0
  27. data/lib/authlogic/crypto_providers/sha256.rb +59 -0
  28. data/lib/authlogic/crypto_providers/sha512/v2.rb +39 -0
  29. data/lib/authlogic/crypto_providers/sha512.rb +38 -0
  30. data/lib/authlogic/crypto_providers.rb +87 -0
  31. data/lib/authlogic/errors.rb +50 -0
  32. data/lib/authlogic/i18n/translator.rb +18 -0
  33. data/lib/authlogic/i18n.rb +100 -0
  34. data/lib/authlogic/random.rb +18 -0
  35. data/lib/authlogic/session/base.rb +2207 -0
  36. data/lib/authlogic/session/magic_column/assigns_last_request_at.rb +46 -0
  37. data/lib/authlogic/test_case/mock_api_controller.rb +52 -0
  38. data/lib/authlogic/test_case/mock_controller.rb +58 -0
  39. data/lib/authlogic/test_case/mock_cookie_jar.rb +109 -0
  40. data/lib/authlogic/test_case/mock_logger.rb +12 -0
  41. data/lib/authlogic/test_case/mock_request.rb +35 -0
  42. data/lib/authlogic/test_case/rails_request_adapter.rb +39 -0
  43. data/lib/authlogic/test_case.rb +215 -0
  44. data/lib/authlogic/version.rb +22 -0
  45. data/lib/authlogic.rb +44 -0
  46. metadata +382 -0
@@ -0,0 +1,357 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authlogic
4
+ module ActsAsAuthentic
5
+ # This module has a lot of neat functionality. It is responsible for encrypting your
6
+ # password, salting it, and verifying it. It can also help you transition to a new
7
+ # encryption algorithm. See the Config sub module for configuration options.
8
+ module Password
9
+ def self.included(klass)
10
+ klass.class_eval do
11
+ extend Config
12
+ add_acts_as_authentic_module(Callbacks)
13
+ add_acts_as_authentic_module(Methods)
14
+ end
15
+ end
16
+
17
+ # All configuration for the password aspect of acts_as_authentic.
18
+ module Config
19
+ # The name of the crypted_password field in the database.
20
+ #
21
+ # * <tt>Default:</tt> :crypted_password, :encrypted_password, :password_hash, or :pw_hash
22
+ # * <tt>Accepts:</tt> Symbol
23
+ def crypted_password_field(value = nil)
24
+ rw_config(
25
+ :crypted_password_field,
26
+ value,
27
+ first_column_to_exist(
28
+ nil,
29
+ :crypted_password,
30
+ :encrypted_password,
31
+ :password_hash,
32
+ :pw_hash
33
+ )
34
+ )
35
+ end
36
+ alias crypted_password_field= crypted_password_field
37
+
38
+ # The name of the password_salt field in the database.
39
+ #
40
+ # * <tt>Default:</tt> :password_salt, :pw_salt, :salt, nil if none exist
41
+ # * <tt>Accepts:</tt> Symbol
42
+ def password_salt_field(value = nil)
43
+ rw_config(
44
+ :password_salt_field,
45
+ value,
46
+ first_column_to_exist(nil, :password_salt, :pw_salt, :salt)
47
+ )
48
+ end
49
+ alias password_salt_field= password_salt_field
50
+
51
+ # Whether or not to require a password confirmation. If you don't want your users
52
+ # to confirm their password just set this to false.
53
+ #
54
+ # * <tt>Default:</tt> true
55
+ # * <tt>Accepts:</tt> Boolean
56
+ def require_password_confirmation(value = nil)
57
+ rw_config(:require_password_confirmation, value, true)
58
+ end
59
+ alias require_password_confirmation= require_password_confirmation
60
+
61
+ # By default passwords are required when a record is new or the crypted_password
62
+ # is blank, but if both of these things are met a password is not required. In
63
+ # this case, blank passwords are ignored.
64
+ #
65
+ # Think about a profile page, where the user can edit all of their information,
66
+ # including changing their password. If they do not want to change their password
67
+ # they just leave the fields blank. This will try to set the password to a blank
68
+ # value, in which case is incorrect behavior. As such, Authlogic ignores this. But
69
+ # let's say you have a completely separate page for resetting passwords, you might
70
+ # not want to ignore blank passwords. If this is the case for you, then just set
71
+ # this value to false.
72
+ #
73
+ # * <tt>Default:</tt> true
74
+ # * <tt>Accepts:</tt> Boolean
75
+ def ignore_blank_passwords(value = nil)
76
+ rw_config(:ignore_blank_passwords, value, true)
77
+ end
78
+ alias ignore_blank_passwords= ignore_blank_passwords
79
+
80
+ # When calling valid_password?("some pass") do you want to check that password
81
+ # against what's in that object or whats in the database. Take this example:
82
+ #
83
+ # u = User.first
84
+ # u.password = "new pass"
85
+ # u.valid_password?("old pass")
86
+ #
87
+ # Should the last line above return true or false? The record hasn't been saved
88
+ # yet, so most would assume true. Other would assume false. So I let you decide by
89
+ # giving you this option.
90
+ #
91
+ # * <tt>Default:</tt> true
92
+ # * <tt>Accepts:</tt> Boolean
93
+ def check_passwords_against_database(value = nil)
94
+ rw_config(:check_passwords_against_database, value, true)
95
+ end
96
+ alias check_passwords_against_database= check_passwords_against_database
97
+
98
+ # The class you want to use to encrypt and verify your encrypted
99
+ # passwords. See the Authlogic::CryptoProviders module for more info on
100
+ # the available methods and how to create your own.
101
+ #
102
+ # The family of adaptive hash functions (BCrypt, SCrypt, PBKDF2) is the
103
+ # best choice for password storage today. We recommend SCrypt. Other
104
+ # one-way functions like SHA512 are inferior, but widely used.
105
+ # Reversible functions like AES256 are the worst choice, and we no
106
+ # longer support them.
107
+ #
108
+ # You can use the `transition_from_crypto_providers` option to gradually
109
+ # transition to a better crypto provider without causing your users any
110
+ # pain.
111
+ #
112
+ # * <tt>Default:</tt> There is no longer a default value. Prior to
113
+ # Authlogic 6, the default was `CryptoProviders::SCrypt`. If you try
114
+ # to read this config option before setting it, it will raise a
115
+ # `NilCryptoProvider` error. See that error's message for further
116
+ # details, and rationale for this change.
117
+ # * <tt>Accepts:</tt> Class
118
+ def crypto_provider
119
+ acts_as_authentic_config[:crypto_provider].tap { |provider|
120
+ raise NilCryptoProvider if provider.nil?
121
+ }
122
+ end
123
+
124
+ def crypto_provider=(value)
125
+ raise NilCryptoProvider if value.nil?
126
+ CryptoProviders::Guidance.new(value).impart_wisdom
127
+ rw_config(:crypto_provider, value)
128
+ end
129
+
130
+ # Let's say you originally encrypted your passwords with Sha1. Sha1 is
131
+ # starting to join the party with MD5 and you want to switch to
132
+ # something stronger. No problem, just specify your new and improved
133
+ # algorithm with the crypt_provider option and then let Authlogic know
134
+ # you are transitioning from Sha1 using this option. Authlogic will take
135
+ # care of everything, including transitioning your users to the new
136
+ # algorithm. The next time a user logs in, they will be granted access
137
+ # using the old algorithm and their password will be resaved with the
138
+ # new algorithm. All new users will obviously use the new algorithm as
139
+ # well.
140
+ #
141
+ # Lastly, if you want to transition again, you can pass an array of
142
+ # crypto providers. So you can transition from as many algorithms as you
143
+ # want.
144
+ #
145
+ # * <tt>Default:</tt> nil
146
+ # * <tt>Accepts:</tt> Class or Array
147
+ def transition_from_crypto_providers(value = nil)
148
+ rw_config(
149
+ :transition_from_crypto_providers,
150
+ (!value.nil? && [value].flatten.compact) || value,
151
+ []
152
+ )
153
+ end
154
+ alias transition_from_crypto_providers= transition_from_crypto_providers
155
+ end
156
+
157
+ # Callbacks / hooks to allow other modules to modify the behavior of this module.
158
+ module Callbacks
159
+ # Does the order of this array matter?
160
+ METHODS = %w[
161
+ password_set
162
+ password_verification
163
+ ].freeze
164
+
165
+ def self.included(klass)
166
+ return if klass.crypted_password_field.nil?
167
+ klass.send :extend, ActiveModel::Callbacks
168
+ METHODS.each do |method|
169
+ klass.define_model_callbacks method, only: %i[before after]
170
+ end
171
+ end
172
+ end
173
+
174
+ # The methods related to the password field.
175
+ module Methods
176
+ def self.included(klass)
177
+ return if klass.crypted_password_field.nil?
178
+
179
+ klass.class_eval do
180
+ include InstanceMethods
181
+ after_save :reset_password_changed
182
+ end
183
+ end
184
+
185
+ # :nodoc:
186
+ module InstanceMethods
187
+ # The password
188
+ def password
189
+ return nil unless defined?(@password)
190
+ @password
191
+ end
192
+
193
+ # This is a virtual method. Once a password is passed to it, it will
194
+ # create new password salt as well as encrypt the password.
195
+ def password=(pass)
196
+ return if ignore_blank_passwords? && pass.blank?
197
+ run_callbacks :password_set do
198
+ @password = pass
199
+ if password_salt_field
200
+ send("#{password_salt_field}=", Authlogic::Random.friendly_token)
201
+ end
202
+ send(
203
+ "#{crypted_password_field}=",
204
+ crypto_provider.encrypt(*encrypt_arguments(@password, false))
205
+ )
206
+ @password_changed = true
207
+ end
208
+ end
209
+
210
+ # Accepts a raw password to determine if it is the correct password.
211
+ #
212
+ # - attempted_password [String] - password entered by user
213
+ # - check_against_database [boolean] - Should we check the password
214
+ # against the value in the database or the value in the object?
215
+ # Default taken from config option check_passwords_against_database.
216
+ # See config method for more information.
217
+ def valid_password?(
218
+ attempted_password,
219
+ check_against_database = check_passwords_against_database?
220
+ )
221
+ crypted = crypted_password_to_validate_against(check_against_database)
222
+ return false if attempted_password.blank? || crypted.blank?
223
+ run_callbacks :password_verification do
224
+ crypto_providers.each_with_index.any? do |encryptor, index|
225
+ if encryptor_matches?(
226
+ crypted,
227
+ encryptor,
228
+ attempted_password,
229
+ check_against_database
230
+ )
231
+ if transition_password?(index, encryptor, check_against_database)
232
+ transition_password(attempted_password)
233
+ end
234
+ true
235
+ else
236
+ false
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ # Resets the password to a random friendly token.
243
+ def reset_password
244
+ friendly_token = Authlogic::Random.friendly_token
245
+ self.password = friendly_token
246
+ self.password_confirmation = friendly_token if self.class.require_password_confirmation
247
+ end
248
+ alias randomize_password reset_password
249
+
250
+ # Resets the password to a random friendly token and then saves the record.
251
+ def reset_password!
252
+ reset_password
253
+ save_without_session_maintenance(validate: false)
254
+ end
255
+ alias randomize_password! reset_password!
256
+
257
+ private
258
+
259
+ def crypted_password_to_validate_against(check_against_database)
260
+ if check_against_database && send("will_save_change_to_#{crypted_password_field}?")
261
+ send("#{crypted_password_field}_in_database")
262
+ else
263
+ send(crypted_password_field)
264
+ end
265
+ end
266
+
267
+ def check_passwords_against_database?
268
+ self.class.check_passwords_against_database == true
269
+ end
270
+
271
+ def crypto_providers
272
+ [crypto_provider] + transition_from_crypto_providers
273
+ end
274
+
275
+ # Returns an array of arguments to be passed to a crypto provider, either its
276
+ # `matches?` or its `encrypt` method.
277
+ def encrypt_arguments(raw_password, check_against_database)
278
+ salt = nil
279
+ if password_salt_field
280
+ salt =
281
+ if check_against_database && send("will_save_change_to_#{password_salt_field}?")
282
+ send("#{password_salt_field}_in_database")
283
+ else
284
+ send(password_salt_field)
285
+ end
286
+ end
287
+ [raw_password, salt].compact
288
+ end
289
+
290
+ # Given `encryptor`, does `attempted_password` match the `crypted` password?
291
+ def encryptor_matches?(crypted, encryptor, attempted_password, check_against_database)
292
+ encryptor_args = encrypt_arguments(attempted_password, check_against_database)
293
+ encryptor.matches?(crypted, *encryptor_args)
294
+ end
295
+
296
+ # Determines if we need to transition the password.
297
+ #
298
+ # - If the index > 0 then we are using a "transition from" crypto
299
+ # provider.
300
+ # - If the encryptor has a cost and the cost it outdated.
301
+ # - If we aren't using database values
302
+ # - If we are using database values, only if the password hasn't
303
+ # changed so we don't overwrite any changes
304
+ def transition_password?(index, encryptor, check_against_database)
305
+ (
306
+ index > 0 ||
307
+ (encryptor.respond_to?(:cost_matches?) &&
308
+ !encryptor.cost_matches?(send(crypted_password_field)))
309
+ ) &&
310
+ (
311
+ !check_against_database ||
312
+ !send("will_save_change_to_#{crypted_password_field}?")
313
+ )
314
+ end
315
+
316
+ def transition_password(attempted_password)
317
+ self.password = attempted_password
318
+ save(validate: false)
319
+ end
320
+
321
+ def require_password?
322
+ # this is _not_ the activemodel changed? method, see below
323
+ new_record? || password_changed? || send(crypted_password_field).blank?
324
+ end
325
+
326
+ def ignore_blank_passwords?
327
+ self.class.ignore_blank_passwords == true
328
+ end
329
+
330
+ def password_changed?
331
+ defined?(@password_changed) && @password_changed == true
332
+ end
333
+
334
+ def reset_password_changed
335
+ @password_changed = nil
336
+ end
337
+
338
+ def crypted_password_field
339
+ self.class.crypted_password_field
340
+ end
341
+
342
+ def password_salt_field
343
+ self.class.password_salt_field
344
+ end
345
+
346
+ def crypto_provider
347
+ self.class.crypto_provider
348
+ end
349
+
350
+ def transition_from_crypto_providers
351
+ self.class.transition_from_crypto_providers
352
+ end
353
+ end
354
+ end
355
+ end
356
+ end
357
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authlogic
4
+ module ActsAsAuthentic
5
+ # This provides a handy token that is "perishable", meaning the token is
6
+ # only good for a certain amount of time.
7
+ #
8
+ # This is useful for resetting password, confirming accounts, etc. Typically
9
+ # during these actions you send them this token in an email. Once they use
10
+ # the token and do what they need to do, that token should expire.
11
+ #
12
+ # Don't worry about maintaining the token, changing it, or expiring it
13
+ # yourself. Authlogic does all of this for you. See the sub modules for all
14
+ # of the tools Authlogic provides to you.
15
+ module PerishableToken
16
+ def self.included(klass)
17
+ klass.class_eval do
18
+ extend Config
19
+ add_acts_as_authentic_module(Methods)
20
+ end
21
+ end
22
+
23
+ # Configure the perishable token.
24
+ module Config
25
+ # When using the find_using_perishable_token method the token can
26
+ # expire. If the token is expired, no record will be returned. Use this
27
+ # option to specify how long the token is valid for.
28
+ #
29
+ # * <tt>Default:</tt> 10.minutes
30
+ # * <tt>Accepts:</tt> Fixnum
31
+ def perishable_token_valid_for(value = nil)
32
+ rw_config(
33
+ :perishable_token_valid_for,
34
+ (!value.nil? && value.to_i) || value,
35
+ 10.minutes.to_i
36
+ )
37
+ end
38
+ alias perishable_token_valid_for= perishable_token_valid_for
39
+
40
+ # Authlogic tries to expire and change the perishable token as much as
41
+ # possible, without compromising its purpose. If you want to manage it
42
+ # yourself, set this to true.
43
+ #
44
+ # * <tt>Default:</tt> false
45
+ # * <tt>Accepts:</tt> Boolean
46
+ def disable_perishable_token_maintenance(value = nil)
47
+ rw_config(:disable_perishable_token_maintenance, value, false)
48
+ end
49
+ alias disable_perishable_token_maintenance= disable_perishable_token_maintenance
50
+ end
51
+
52
+ # All methods relating to the perishable token.
53
+ module Methods
54
+ def self.included(klass)
55
+ return unless klass.column_names.include?("perishable_token")
56
+
57
+ klass.class_eval do
58
+ extend ClassMethods
59
+ include InstanceMethods
60
+
61
+ validates_uniqueness_of :perishable_token, case_sensitive: true,
62
+ if: :will_save_change_to_perishable_token?
63
+ before_save :reset_perishable_token, unless: :disable_perishable_token_maintenance?
64
+ end
65
+ end
66
+
67
+ # :nodoc:
68
+ module ClassMethods
69
+ # Use this method to find a record with a perishable token. This
70
+ # method does 2 things for you:
71
+ #
72
+ # 1. It ignores blank tokens
73
+ # 2. It enforces the perishable_token_valid_for configuration option.
74
+ #
75
+ # If you want to use a different timeout value, just pass it as the
76
+ # second parameter:
77
+ #
78
+ # User.find_using_perishable_token(token, 1.hour)
79
+ def find_using_perishable_token(token, age = perishable_token_valid_for)
80
+ return if token.blank?
81
+ age = age.to_i
82
+
83
+ conditions_sql = "perishable_token = ?"
84
+ conditions_subs = [token.to_s]
85
+
86
+ if column_names.include?("updated_at") && age > 0
87
+ conditions_sql += " and updated_at > ?"
88
+ conditions_subs << age.seconds.ago
89
+ end
90
+
91
+ where(conditions_sql, *conditions_subs).first
92
+ end
93
+
94
+ # This method will raise ActiveRecord::NotFound if no record is found.
95
+ def find_using_perishable_token!(token, age = perishable_token_valid_for)
96
+ find_using_perishable_token(token, age) || raise(ActiveRecord::RecordNotFound)
97
+ end
98
+ end
99
+
100
+ # :nodoc:
101
+ module InstanceMethods
102
+ # Resets the perishable token to a random friendly token.
103
+ def reset_perishable_token
104
+ self.perishable_token = Random.friendly_token
105
+ end
106
+
107
+ # Same as reset_perishable_token, but then saves the record afterwards.
108
+ def reset_perishable_token!
109
+ reset_perishable_token
110
+ save_without_session_maintenance(validate: false)
111
+ end
112
+
113
+ # A convenience method based on the
114
+ # disable_perishable_token_maintenance configuration option.
115
+ def disable_perishable_token_maintenance?
116
+ self.class.disable_perishable_token_maintenance == true
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authlogic
4
+ module ActsAsAuthentic
5
+ # Maintains the persistence token, the token responsible for persisting sessions. This token
6
+ # gets stored in the session and the cookie.
7
+ module PersistenceToken
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ add_acts_as_authentic_module(Methods)
11
+ end
12
+ end
13
+
14
+ # Methods for the persistence token.
15
+ module Methods
16
+ def self.included(klass)
17
+ klass.class_eval do
18
+ extend ClassMethods
19
+ include InstanceMethods
20
+
21
+ # If the table does not have a password column, then
22
+ # `after_password_set` etc. will not be defined. See
23
+ # `Authlogic::ActsAsAuthentic::Password::Callbacks.included`
24
+ if respond_to?(:after_password_set) && respond_to?(:after_password_verification)
25
+ after_password_set :reset_persistence_token
26
+ after_password_verification :reset_persistence_token!, if: :reset_persistence_token?
27
+ end
28
+
29
+ validates_presence_of :persistence_token
30
+ validates_uniqueness_of :persistence_token, case_sensitive: true,
31
+ if: :will_save_change_to_persistence_token?
32
+
33
+ before_validation :reset_persistence_token, if: :reset_persistence_token?
34
+ end
35
+ end
36
+
37
+ # :nodoc:
38
+ module ClassMethods
39
+ # Resets ALL persistence tokens in the database, which will require
40
+ # all users to re-authenticate.
41
+ def forget_all
42
+ # Paginate these to save on memory
43
+ find_each(batch_size: 50, &:forget!)
44
+ end
45
+ end
46
+
47
+ # :nodoc:
48
+ module InstanceMethods
49
+ # Resets the persistence_token field to a random hex value.
50
+ def reset_persistence_token
51
+ self.persistence_token = Authlogic::Random.hex_token
52
+ end
53
+
54
+ # Same as reset_persistence_token, but then saves the record.
55
+ def reset_persistence_token!
56
+ reset_persistence_token
57
+ save_without_session_maintenance(validate: false)
58
+ end
59
+ alias forget! reset_persistence_token!
60
+
61
+ private
62
+
63
+ def reset_persistence_token?
64
+ persistence_token.blank?
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authlogic
4
+ module ActsAsAuthentic
5
+ module Queries
6
+ # @api private
7
+ class CaseSensitivity
8
+ E_UNABLE_TO_DETERMINE_SENSITIVITY = <<~EOS
9
+ Authlogic was unable to determine what case-sensitivity to use when
10
+ searching for email/login. To specify a sensitivity, validate the
11
+ uniqueness of the email/login and use the `case_sensitive` option,
12
+ like this:
13
+
14
+ validates :email, uniqueness: { case_sensitive: false }
15
+
16
+ Authlogic will now perform a case-insensitive query.
17
+ EOS
18
+
19
+ # @api private
20
+ def initialize(model_class, attribute)
21
+ @model_class = model_class
22
+ @attribute = attribute.to_sym
23
+ end
24
+
25
+ # @api private
26
+ def sensitive?
27
+ sensitive = uniqueness_validator_options[:case_sensitive]
28
+ if sensitive.nil?
29
+ ::Kernel.warn(E_UNABLE_TO_DETERMINE_SENSITIVITY)
30
+ false
31
+ else
32
+ sensitive
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # @api private
39
+ def uniqueness_validator
40
+ @model_class.validators.select { |v|
41
+ v.is_a?(::ActiveRecord::Validations::UniquenessValidator) &&
42
+ v.attributes == [@attribute]
43
+ }.first
44
+ end
45
+
46
+ # @api private
47
+ def uniqueness_validator_options
48
+ uniqueness_validator&.options || {}
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authlogic
4
+ module ActsAsAuthentic
5
+ module Queries
6
+ # The query used by public-API method `find_by_smart_case_login_field`.
7
+ #
8
+ # We use the rails methods `case_insensitive_comparison` and
9
+ # `case_sensitive_comparison`. These methods nicely take into account
10
+ # MySQL collations. (Consider the case where a user *says* they want a
11
+ # case-sensitive uniqueness validation, but then they configure their
12
+ # database to have an insensitive collation. Rails will handle this for
13
+ # us, by downcasing, see
14
+ # `active_record/connection_adapters/abstract_mysql_adapter.rb`) So that's
15
+ # great! But, these methods are not part of rails' public API, so there
16
+ # are no docs. So, everything we know about how to use the methods
17
+ # correctly comes from mimicing what we find in
18
+ # `active_record/validations/uniqueness.rb`.
19
+ #
20
+ # @api private
21
+ class FindWithCase
22
+ # Dup ActiveRecord.gem_version before freezing, in case someone
23
+ # else wants to modify it. Freezing modifies an object in place.
24
+ # https://github.com/binarylogic/authlogic/pull/590
25
+ AR_GEM_VERSION = ::ActiveRecord.gem_version.dup.freeze
26
+
27
+ # @api private
28
+ def initialize(model_class, field, value, sensitive)
29
+ @model_class = model_class
30
+ @field = field.to_s
31
+ @value = value
32
+ @sensitive = sensitive
33
+ end
34
+
35
+ # @api private
36
+ def execute
37
+ @model_class.where(comparison).first
38
+ end
39
+
40
+ private
41
+
42
+ # @api private
43
+ # @return Arel::Nodes::Equality
44
+ def comparison
45
+ @sensitive ? sensitive_comparison : insensitive_comparison
46
+ end
47
+
48
+ # @api private
49
+ def insensitive_comparison
50
+ if AR_GEM_VERSION > Gem::Version.new("5.3")
51
+ @model_class.connection.case_insensitive_comparison(
52
+ @model_class.arel_table[@field], @value
53
+ )
54
+ else
55
+ @model_class.connection.case_insensitive_comparison(
56
+ @model_class.arel_table,
57
+ @field,
58
+ @model_class.columns_hash[@field],
59
+ @value
60
+ )
61
+ end
62
+ end
63
+
64
+ # @api private
65
+ def sensitive_comparison
66
+ bound_value = @model_class.predicate_builder.build_bind_attribute(@field, @value)
67
+ if AR_GEM_VERSION > Gem::Version.new("5.3")
68
+ @model_class.connection.case_sensitive_comparison(
69
+ @model_class.arel_table[@field], bound_value
70
+ )
71
+ else
72
+ @model_class.connection.case_sensitive_comparison(
73
+ @model_class.arel_table,
74
+ @field,
75
+ @model_class.columns_hash[@field],
76
+ bound_value
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end