Welcome to mirror list, hosted at ThFree Co, Russian Federation.

active_record_test.rb « test « attr_encrypted « gems « vendor - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 4c903bc3cd882a4c65de1aad2751b0b68122e7fa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# frozen_string_literal: true

require_relative 'test_helper'

ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')

def create_tables
  ActiveRecord::Schema.define(version: 1) do
    self.verbose = false
    create_table :people do |t|
      t.string   :encrypted_email
      t.string   :password
      t.string   :encrypted_credentials
      t.binary   :salt
      t.binary   :key_iv
      t.string   :encrypted_email_salt
      t.string   :encrypted_credentials_salt
      t.string   :encrypted_email_iv
      t.string   :encrypted_credentials_iv
    end
    create_table :accounts do |t|
      t.string :encrypted_password
      t.string :encrypted_password_iv
      t.string :encrypted_password_salt
      t.string :key
    end
    create_table :users do |t|
      t.string :login
      t.string :encrypted_password
      t.string :encrypted_password_iv
      t.boolean :is_admin
    end
    create_table :prime_ministers do |t|
      t.string :encrypted_name
      t.string :encrypted_name_iv
    end
    create_table :addresses do |t|
      t.binary :encrypted_street
      t.binary :encrypted_street_iv
      t.binary :encrypted_zipcode
      t.string :mode
    end
  end
end

ActiveRecord::MissingAttributeError = ActiveModel::MissingAttributeError unless defined?(ActiveRecord::MissingAttributeError)

if ::ActiveRecord::VERSION::STRING > "4.0"
  module Rack
    module Test
      class UploadedFile; end
    end
  end

  require 'action_controller/metal/strong_parameters'
end

class Person < ActiveRecord::Base
  self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt
  attr_encrypted :email, key: SECRET_KEY
  attr_encrypted :credentials, key: Proc.new { |user| Encryptor.encrypt(value: user.salt, key: SECRET_KEY, iv: user.key_iv) }, marshal: true

  after_initialize :initialize_salt_and_credentials

  protected

  def initialize_salt_and_credentials
    self.key_iv ||= SecureRandom.random_bytes(12)
    self.salt ||= Digest::SHA256.hexdigest((Time.now.to_i * rand(1000)).to_s)[0..15]
    self.credentials ||= { username: 'example', password: 'test' }
  end
end

class PersonWithValidation < Person
  validates_presence_of :email
end

class PersonWithProcMode < Person
  attr_encrypted :email,       key: SECRET_KEY, mode: Proc.new { :per_attribute_iv_and_salt }
  attr_encrypted :credentials, key: SECRET_KEY, mode: Proc.new { :single_iv_and_salt }, insecure_mode: true
end

class PersonWithInstanceAttribute < Person
  attr_encrypted :age
end

class Account < ActiveRecord::Base
  ACCOUNT_ENCRYPTION_KEY = SecureRandom.urlsafe_base64(24)
  attr_encrypted :password, key: :password_encryption_key

  def encrypting?(attr)
    encrypted_attributes[attr][:operation] == :encrypting
  end

  def password_encryption_key
    if encrypting?(:password)
      self.key = ACCOUNT_ENCRYPTION_KEY
    else
      self.key
    end
  end
end

class PersonWithSerialization < ActiveRecord::Base
  self.table_name = 'people'
  attr_encrypted :email, key: SECRET_KEY
  serialize :password
end

class UserWithProtectedAttribute < ActiveRecord::Base
  self.table_name = 'users'
  attr_encrypted :password, key: SECRET_KEY
  attr_protected :is_admin if ::ActiveRecord::VERSION::STRING < "4.0"
end

class PersonUsingAlias < ActiveRecord::Base
  self.table_name = 'people'
  attr_encryptor :email, key: SECRET_KEY
end

class PrimeMinister < ActiveRecord::Base
  attr_encrypted :name, marshal: true, key: SECRET_KEY
end

class Address < ActiveRecord::Base
  self.attr_encrypted_options[:marshal] = false
  self.attr_encrypted_options[:encode] = false
  attr_encrypted :street, encode_iv: false, key: SECRET_KEY
  attr_encrypted :zipcode, key: SECRET_KEY, mode: Proc.new { |address| address.mode.to_sym }, insecure_mode: true
end

class ActiveRecordTest < Minitest::Test

  def setup
    drop_all_tables
    create_tables
  end

  def test_should_encrypt_email
    @person = Person.create(email: 'test@example.com')
    refute_nil @person.encrypted_email
    refute_equal @person.email, @person.encrypted_email
    assert_equal @person.email, Person.first.email
  end

  def test_should_marshal_and_encrypt_credentials
    @person = Person.create
    refute_nil @person.encrypted_credentials
    refute_equal @person.credentials, @person.encrypted_credentials
    assert_equal @person.credentials, Person.first.credentials
  end

  def test_should_encode_by_default
    assert Person.attr_encrypted_options[:encode]
  end

  def test_should_validate_presence_of_email
    @person = PersonWithValidation.new
    assert !@person.valid?
    assert !@person.errors[:email].empty? || @person.errors.on(:email)
  end

  def test_should_encrypt_decrypt_with_iv
    @person = Person.create(email: 'test@example.com')
    @person2 = Person.find(@person.id)
    refute_nil @person2.encrypted_email_iv
    assert_equal 'test@example.com', @person2.email
  end

  def test_should_ensure_attributes_can_be_deserialized
    @person = PersonWithSerialization.new(email: 'test@example.com', password: %w(an array of strings))
    @person.save
    assert_equal @person.password, %w(an array of strings)
  end

  def test_should_create_an_account_regardless_of_arguments_order
    Account.create!(key: SECRET_KEY, password: "password")
    Account.create!(password: "password" , key: SECRET_KEY)
  end

  def test_should_set_attributes_regardless_of_arguments_order
    # minitest does not implement `assert_nothing_raised` https://github.com/seattlerb/minitest/issues/112
    Account.new.attributes = { password: "password", key: SECRET_KEY }
  end

  def test_should_create_changed_predicate
    person = Person.create!(email: 'test@example.com')
    refute person.email_changed?
    person.email = 'test@example.com'
    refute person.email_changed?
    person.email = nil
    assert person.email_changed?
    person.email = 'test2@example.com'
    assert person.email_changed?
  end

  # PENDING - this test is failing because attr_encrypted does not adhere to the
  # interface contract for ActiveModel::Dirty as of ActiveRecord 6.1:
  # https://devdocs.io/rails~6.1/activemodel/dirty
  def pending_test_should_create_was_predicate
    original_email = 'test@example.com'
    person = Person.create!(email: original_email)
    assert_equal original_email, person.email_was
    person.email = 'test2@example.com'
    assert_equal original_email, person.email_was
    old_pm_name = "Winston Churchill"
    pm = PrimeMinister.create!(name: old_pm_name)
    assert_equal old_pm_name, pm.name_was
    old_zipcode = "90210"
    address = Address.create!(zipcode: old_zipcode, mode: "single_iv_and_salt")
    assert_equal old_zipcode, address.zipcode_was
  end

  # PENDING - this test is failing because attr_encrypted does not adhere to the
  # interface contract for ActiveModel::Dirty as of ActiveRecord 6.1:
  # https://devdocs.io/rails~6.1/activemodel/dirty
  def pending_test_attribute_was_works_when_options_for_old_encrypted_value_are_different_than_options_for_new_encrypted_value
    pw = 'password'
    crypto_key = SecureRandom.urlsafe_base64(24)
    old_iv = SecureRandom.random_bytes(12)
    account = Account.create
    encrypted_value = Encryptor.encrypt(value: pw, iv: old_iv, key: crypto_key)
    Account.where(id: account.id).update_all(key: crypto_key, encrypted_password_iv: [old_iv].pack('m'), encrypted_password: [encrypted_value].pack('m'))
    account = Account.find(account.id)
    assert_equal pw, account.password
    account.password = pw.reverse
    assert_equal pw, account.password_was
    account.save
    account.reload
    assert_equal Account::ACCOUNT_ENCRYPTION_KEY, account.key
    assert_equal pw.reverse, account.password
  end

  if ::ActiveRecord::VERSION::STRING > "4.0"
    def test_should_assign_attributes
      @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
      @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login)
      assert_equal 'modified', @user.login
    end

    def test_should_not_assign_protected_attributes
      @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
      @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login)
      assert !@user.is_admin?
    end

    def test_should_raise_exception_if_not_permitted
      @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
      assert_raises ActiveModel::ForbiddenAttributesError do
        @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true)
      end
    end

    def test_should_raise_exception_on_init_if_not_permitted
      assert_raises ActiveModel::ForbiddenAttributesError do
        @user = UserWithProtectedAttribute.new ActionController::Parameters.new(login: 'modified', is_admin: true)
      end
    end
  else
    def test_should_assign_attributes
      @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
      @user.attributes = { login: 'modified', is_admin: true }
      assert_equal 'modified', @user.login
    end

    def test_should_not_assign_protected_attributes
      @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
      @user.attributes = { login: 'modified', is_admin: true }
      assert !@user.is_admin?
    end

    def test_should_assign_protected_attributes
      @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false)
      if ::ActiveRecord::VERSION::STRING > "3.1"
        @user.send(:assign_attributes, { login: 'modified', is_admin: true }, without_protection: true)
      else
        @user.send(:attributes=, { login: 'modified', is_admin: true }, false)
      end
      assert @user.is_admin?
    end
  end

  def test_should_allow_assignment_of_nil_attributes
    @person = Person.new
    assert_nil(@person.attributes = nil)
  end

  def test_should_allow_proc_based_mode
    @person = PersonWithProcMode.create(email: 'test@example.com', credentials: 'password123')

    # Email is :per_attribute_iv_and_salt
    assert_equal @person.class.encrypted_attributes[:email][:mode].class, Proc
    assert_equal @person.class.encrypted_attributes[:email][:mode].call, :per_attribute_iv_and_salt
    refute_nil @person.encrypted_email_salt
    refute_nil @person.encrypted_email_iv

    # Credentials is :single_iv_and_salt
    assert_equal @person.class.encrypted_attributes[:credentials][:mode].class, Proc
    assert_equal @person.class.encrypted_attributes[:credentials][:mode].call, :single_iv_and_salt
    assert_nil @person.encrypted_credentials_salt
    assert_nil @person.encrypted_credentials_iv
  end

  if ::ActiveRecord::VERSION::STRING > "3.1"
    def test_should_allow_assign_attributes_with_nil
      @person = Person.new
      assert_nil(@person.assign_attributes nil)
    end
  end

  def test_that_alias_encrypts_column
    user = PersonUsingAlias.new
    user.email = 'test@example.com'
    user.save

    refute_nil user.encrypted_email
    refute_equal user.email, user.encrypted_email
    assert_equal user.email, PersonUsingAlias.first.email
  end

  # See https://github.com/attr-encrypted/attr_encrypted/issues/68
  def test_should_invalidate_virtual_attributes_on_reload
    old_pm_name = 'Winston Churchill'
    new_pm_name = 'Neville Chamberlain'
    pm = PrimeMinister.create!(name: old_pm_name)
    assert_equal old_pm_name, pm.name
    pm.name = new_pm_name
    assert_equal new_pm_name, pm.name

    result = pm.reload
    assert_equal pm, result
    assert_equal old_pm_name, pm.name
  end

  def test_should_save_encrypted_data_as_binary
    street = '123 Elm'
    address = Address.create!(street: street)
    refute_equal address.encrypted_street, street
    assert_equal Address.first.street, street
  end

  def test_should_evaluate_proc_based_mode
    street = '123 Elm'
    zipcode = '12345'
    address = Address.create(street: street, zipcode: zipcode, mode: :single_iv_and_salt)
    address.reload
    refute_equal address.encrypted_zipcode, zipcode
    assert_equal address.zipcode, zipcode
  end

  # See https://github.com/attr-encrypted/attr_encrypted/issues/332
  def test_attribute_instance_methods_as_symbols_available_returns_false
    assert_equal false, ActiveRecord::Base.__send__(:attribute_instance_methods_as_symbols_available?)
  end

  # See https://github.com/attr-encrypted/attr_encrypted/issues/332
  def test_does_not_define_virtual_attributes
    instance = Person.new

    %w[
      encrypted_age encrypted_age=
      encrypted_age_iv encrypted_age_iv=
      encrypted_age_salt encrypted_age_salt=
    ].each do |method_name|
      assert_equal false, instance.respond_to?(method_name), "should not define #{method_name}"
    end
  end
end