attr_digest 1.0.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1 -0
- data/MIT-LICENSE +20 -0
- data/README.md +143 -0
- data/lib/attr_digest/attr_digest.rb +72 -0
- data/lib/attr_digest/version.rb +14 -0
- data/lib/attr_digest.rb +2 -0
- data/spec/factories/models.rb +46 -0
- data/spec/lib/attr_digest_spec.rb +209 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/support/models.rb +47 -0
- data/spec/support/schema.rb +17 -0
- data/spec/tmp/test.sqlite3 +0 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cf52ffb7a487f0b9c13e9fb6e2cb3d1882d21a4a
|
4
|
+
data.tar.gz: a6918d2fd2c13d8a204eb095ddc6c403af4fd246
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5dd307ca3c3dbb20782064d484fcac971c3ad46da759dd048cee458a6996702199a407e8f44076f02a5c829ff279d8c78fa106b12facb8eacc197d02ad07f373
|
7
|
+
data.tar.gz: b83b2d233a00f196ecfb7656ed37144f948e12972f97a38c8f6f680fbf62dc956b45af587a24f245f1da9b77fe6abc7041b4c31e6eb63b669eb6455f45a718a7
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
v1.0.0 - Initial release.
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2016 Brightcommerce, Inc. All rights reserved.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
[](https://badge.fury.io/rb/attr_digest)
|
2
|
+
[](https://travis-ci.org/brightcommerce/attr_digest)
|
3
|
+
[](https://codecov.io/github/brightcommerce/attr_digest?branch=master)
|
4
|
+
[](https://github.com/brightcommerce/attr_digest)
|
5
|
+
[](https://github.com/dwyl/esta/issues)
|
6
|
+
|
7
|
+
# AttrDigest
|
8
|
+
|
9
|
+
[**AttrDigest**](https://github.com/brightcommerce/attr_digest) provides functionality to store a hash digest of an attribute using [Argon2](https://github.com/P-H-C/phc-winner-argon2).
|
10
|
+
|
11
|
+
Argon2 is the official winner and recommendation of the [Password Hashing Competition (PHC)](https://password-hashing.net) which ran between 2013 and 2015, and is a password-hashing function that summarizes the state of the art in the design of memory-hard functions and can be used to hash passwords for credential storage, key derivation, or other applications.
|
12
|
+
|
13
|
+
This Gem uses the [Ruby Argon2 Gem](https://github.com/technion/ruby-argon2) which provides FFI bindings, and a simplified interface, to the Argon2 algorithm.
|
14
|
+
|
15
|
+
**AttrDigest** provides similar functionality to Rails `has_secure_password`, but permits any number attributes to be hashed in a model, and obviously you're not limited to just the `password` attribute.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
To install add the following line to your `Gemfile`:
|
20
|
+
|
21
|
+
``` ruby
|
22
|
+
gem 'attr_digest', '~> 1.0'
|
23
|
+
```
|
24
|
+
|
25
|
+
And run `bundle install`.
|
26
|
+
|
27
|
+
## Dependencies
|
28
|
+
|
29
|
+
Runtime:
|
30
|
+
- activerecord (~> 4.2.5.1)
|
31
|
+
- activesupport (~> 4.2.5.1)
|
32
|
+
- argon2 (~> 0.1.4)
|
33
|
+
|
34
|
+
Development/Test:
|
35
|
+
- rake (~> 10.5)
|
36
|
+
- rspec (~> 3.4)
|
37
|
+
- sqlite3 (~> 1.3)
|
38
|
+
- simplecov (~> 0.11.2)
|
39
|
+
- factory_girl (~> 4.5)
|
40
|
+
|
41
|
+
## Compatibility
|
42
|
+
|
43
|
+
Tested with Ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-darwin15] against ActiveRecord 4.2.5.1 on Mac OS X El Capitan 10.11.3 (15D21).
|
44
|
+
|
45
|
+
Argon2 requires Ruby 2.2 minimum and an OS platform that supports Ruby FFI Bindings, so unfortunately Windows is out.
|
46
|
+
|
47
|
+
|
48
|
+
## Usage
|
49
|
+
|
50
|
+
Attributes to be digested are declared using the `attr_digest` class method in your model:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
ActiveRecord::Schema.define do
|
54
|
+
create_table :users, force: true do |t|
|
55
|
+
t.string :security_question, null: false
|
56
|
+
t.string :security_answer_digest, null: false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class User < ActiveRecord::Base
|
61
|
+
attr_digest :security_answer
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
**AttrDigest** automatically creates the `#security_answer` getter and `#security_answer=` setter. The setter creates a digest of the value provided and stores it in the `security_answer_digest` column.
|
66
|
+
|
67
|
+
**AttrDigest** also defines the method `authenticate_security_answer(value)` which returns `false` if the `value` given does not correspond to the saved digest, or returns `true` if it does.
|
68
|
+
|
69
|
+
### Validations
|
70
|
+
|
71
|
+
**AttrDigest** adds some default validations. Using the example above:
|
72
|
+
* it creates a `confirmation` validation on `security_answer`, but only if `security_answer` is given (for confirmation validations see [ActiveRecord Validations](http://http://guides.rubyonrails.org/active_record_validations.html#confirmation)).
|
73
|
+
* it creates a `presence` validation on `security_answer` but only on `create`.
|
74
|
+
* it creates a `presence` validation on `security_answer_confirmation` but only if `security_answer` has been given; and
|
75
|
+
* it raises an `exception` if `security_answer_digest` is empty on `create`.
|
76
|
+
|
77
|
+
You can disable all validations by passing `false` to the `validations` option:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
attr_digest :security_answer, validations: false
|
81
|
+
```
|
82
|
+
|
83
|
+
#### Case Sensitivity
|
84
|
+
|
85
|
+
If you want values passed to be case insensitive, you can pass `false` to the `case_sensitive` option:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
attr_digest :security_answer, case_sensitive: false
|
89
|
+
```
|
90
|
+
|
91
|
+
Then differing cases will match, e.g. `pizza` will match `PizzA`.
|
92
|
+
|
93
|
+
#### Confirmations
|
94
|
+
|
95
|
+
If you prefer to skip confirmations for the attribute you are hashing, you can pass `false` to the `confirmation` option:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
attr_digest :security_answer, confirmation: false
|
99
|
+
```
|
100
|
+
|
101
|
+
#### Protected Digest Setter
|
102
|
+
|
103
|
+
If you want to prevent the attribute's digest being set directly, you can include the `protected` option:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
attr_digest :security_answer, protected: true
|
107
|
+
```
|
108
|
+
|
109
|
+
The attribute's digest is *not* protected from direct setting by default.
|
110
|
+
|
111
|
+
## Tests
|
112
|
+
|
113
|
+
Tests are written using Rspec, FactoryGirl and Sqlite3. There are 31 examples with 100% code coverage.
|
114
|
+
|
115
|
+
To run the tests, execute the default rake task:
|
116
|
+
|
117
|
+
``` bash
|
118
|
+
bundle exec rake
|
119
|
+
```
|
120
|
+
|
121
|
+
## Roadmap
|
122
|
+
|
123
|
+
I like to add the ability to pass a `secret` to the Argon2 hasher. This functionality exists in the Ruby Argon2 Gem.
|
124
|
+
|
125
|
+
## Contributing
|
126
|
+
|
127
|
+
1. Fork it
|
128
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
129
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
130
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
131
|
+
5. Create new Pull Request
|
132
|
+
|
133
|
+
## Credit
|
134
|
+
|
135
|
+
I like to thank [Panayotis Matsinopoulos](http://www.matsinopoulos.gr) for his [has_secure_attribute](https://github.com/pmatsinopoulos/has_secure_attribute) gem which provided a lot of the inspiration and framework for **AttrDigest**.
|
136
|
+
|
137
|
+
## License
|
138
|
+
|
139
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
140
|
+
|
141
|
+
## Copyright
|
142
|
+
|
143
|
+
Copyright 2016 Brightcommerce, Inc.
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'argon2'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_support/all'
|
4
|
+
|
5
|
+
module AttrDigest
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :time_cost
|
10
|
+
attr_accessor :memory_cost
|
11
|
+
end
|
12
|
+
|
13
|
+
self.time_cost = 2
|
14
|
+
self.memory_cost = 16
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def attr_digest(meth, *args, &block)
|
18
|
+
attribute_sym = meth.to_sym
|
19
|
+
attr_reader attribute_sym
|
20
|
+
|
21
|
+
options = { validations: true, protected: false, case_sensitive: true, confirmation: true }
|
22
|
+
options.merge! args[0] unless args.blank?
|
23
|
+
|
24
|
+
if options[:validations]
|
25
|
+
confirm attribute_sym if options[:confirmation]
|
26
|
+
validates attribute_sym, presence: true, on: :create
|
27
|
+
before_create { raise "#{attribute_sym}_digest missing on new record" if send("#{attribute_sym}_digest").blank? }
|
28
|
+
end
|
29
|
+
|
30
|
+
define_setter(attribute_sym, options)
|
31
|
+
protect_setter(attribute_sym) if options[:protected]
|
32
|
+
define_authenticate_method(attribute_sym, options)
|
33
|
+
end
|
34
|
+
|
35
|
+
def confirm(attribute_sym)
|
36
|
+
validates attribute_sym, confirmation: true, if: lambda { |m| m.send(attribute_sym).present? }
|
37
|
+
validates "#{attribute_sym}_confirmation".to_sym, presence: true, if: lambda { |m| m.send(attribute_sym).present? }
|
38
|
+
end
|
39
|
+
|
40
|
+
def define_setter(attribute_sym, options)
|
41
|
+
define_method "#{attribute_sym.to_s}=" do |unencrypted_value|
|
42
|
+
unless unencrypted_value.blank?
|
43
|
+
instance_variable_set("@#{attribute_sym.to_s}".to_sym, unencrypted_value)
|
44
|
+
password = Argon2::Password.new(t_cost: AttrDigest.time_cost, m_cost: AttrDigest.memory_cost)
|
45
|
+
send("#{attribute_sym.to_s}_digest=".to_sym, password.hash(options[:case_sensitive] ? unencrypted_value : unencrypted_value.downcase))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def protect_setter(attribute_sym)
|
51
|
+
define_method "#{attribute_sym}_digest=" do |value|
|
52
|
+
write_attribute "#{attribute_sym}_digest".to_sym, value
|
53
|
+
end
|
54
|
+
protected "#{attribute_sym}_digest=".to_sym
|
55
|
+
end
|
56
|
+
|
57
|
+
def define_authenticate_method(attribute_sym, options)
|
58
|
+
define_method "authenticate_#{attribute_sym}" do |value|
|
59
|
+
Argon2::Password.verify_password((options[:case_sensitive] ? value : value.downcase), send("#{attribute_sym}_digest"))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
protected :attr_digest
|
64
|
+
protected :confirm
|
65
|
+
protected :define_setter
|
66
|
+
protected :protect_setter
|
67
|
+
protected :define_authenticate_method
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
ActiveRecord::Base.send :include, AttrDigest
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module AttrDigest
|
2
|
+
module VERSION
|
3
|
+
MAJOR = 1
|
4
|
+
MINOR = 0
|
5
|
+
TINY = 0
|
6
|
+
PRE = nil
|
7
|
+
|
8
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
|
9
|
+
|
10
|
+
SUMMARY = "AttrDigest v#{STRING}"
|
11
|
+
|
12
|
+
DESCRIPTION = "Provides functionality to store a hash digest of an attribute using Argon2"
|
13
|
+
end
|
14
|
+
end
|
data/lib/attr_digest.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
|
3
|
+
factory :model_with_attr_digest do
|
4
|
+
username 'username'
|
5
|
+
password 'password'
|
6
|
+
password_confirmation 'password'
|
7
|
+
security_question 'question'
|
8
|
+
security_answer 'answer'
|
9
|
+
security_answer_confirmation 'answer'
|
10
|
+
end
|
11
|
+
|
12
|
+
factory :model_with_attr_digest_and_validations_option do
|
13
|
+
username 'username_no_validation'
|
14
|
+
password 'password'
|
15
|
+
password_confirmation 'password'
|
16
|
+
security_question 'question'
|
17
|
+
security_answer 'answer'
|
18
|
+
end
|
19
|
+
|
20
|
+
factory :model_with_attr_digest_and_protected_option do
|
21
|
+
username 'username_protect'
|
22
|
+
password 'password'
|
23
|
+
password_confirmation 'password'
|
24
|
+
security_question 'question'
|
25
|
+
security_answer 'answer'
|
26
|
+
security_answer_confirmation 'answer'
|
27
|
+
end
|
28
|
+
|
29
|
+
factory :model_with_attr_digest_and_case_sensitive_option do
|
30
|
+
username 'username_protect'
|
31
|
+
password 'password'
|
32
|
+
password_confirmation 'password'
|
33
|
+
security_question 'question'
|
34
|
+
security_answer 'answer'
|
35
|
+
security_answer_confirmation 'answer'
|
36
|
+
end
|
37
|
+
|
38
|
+
factory :model_with_attr_digest_and_confirmation_option do
|
39
|
+
username 'username_protect'
|
40
|
+
password 'password'
|
41
|
+
password_confirmation 'password'
|
42
|
+
security_question 'question'
|
43
|
+
security_answer 'answer'
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ModelWithAttrDigest do
|
4
|
+
it 'responds to :security_answer' do
|
5
|
+
respond_to(:security_answer)
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'responds to :security_answer=' do
|
9
|
+
respond_to(:security_answer=)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'responds to :security_answer_confirmation' do
|
13
|
+
respond_to(:security_answer_confirmation)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'responds to :security_answer_confirmation=' do
|
17
|
+
respond_to(:security_answer_confirmation=)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'responds to :authenticate_security_answer' do
|
21
|
+
respond_to(:authenticate_security_answer)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'confirms :security_answer' do
|
25
|
+
subject.security_answer = 'hello there'
|
26
|
+
subject.security_answer_confirmation = 'there hello'
|
27
|
+
subject.valid?
|
28
|
+
expect(subject.errors[:security_answer_confirmation]).to include("doesn't match Security answer")
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'does not confirm :security_answer if not given' do
|
32
|
+
subject.security_answer = nil
|
33
|
+
subject.security_answer_confirmation = 'there hello'
|
34
|
+
subject.valid?
|
35
|
+
expect(subject.errors[:security_answer_confirmation]).to be_blank
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'requires :security_answer on create' do
|
39
|
+
expect(subject).to be_new_record
|
40
|
+
subject.security_answer = nil
|
41
|
+
subject.valid?
|
42
|
+
expect(subject.errors[:security_answer]).to include("can't be blank")
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'requires :security_answer_confirmation if :security_answer given' do
|
46
|
+
subject.security_answer = 'hello there'
|
47
|
+
subject.valid?
|
48
|
+
expect(subject.errors[:security_answer_confirmation]).to include("can't be blank")
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'does not require :security_answer_confirmation if :security_answer is not given' do
|
52
|
+
subject.security_answer = ''
|
53
|
+
subject.valid?
|
54
|
+
expect(subject.errors[:security_answer_confirmation]).to be_blank
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'requires :security_answer_digest on create' do
|
58
|
+
subject = FactoryGirl.build(:model_with_attr_digest)
|
59
|
+
expect(subject).to be_new_record
|
60
|
+
# change the security_answer_digest to verify the test
|
61
|
+
subject.security_answer_digest = ''
|
62
|
+
expect(lambda do
|
63
|
+
begin
|
64
|
+
subject.save!
|
65
|
+
rescue Exception => exception
|
66
|
+
expect(exception.message).to include("security_answer_digest missing on new record")
|
67
|
+
raise
|
68
|
+
end
|
69
|
+
end).to raise_error(RuntimeError)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'does not require :security_answer_digest on update' do
|
73
|
+
subject = FactoryGirl.build(:model_with_attr_digest)
|
74
|
+
expect(subject).to be_new_record
|
75
|
+
subject.save!
|
76
|
+
# change the security_answer_digest to verify the test
|
77
|
+
subject.security_answer_digest = ''
|
78
|
+
subject.save!
|
79
|
+
subject.reload
|
80
|
+
expect(subject.security_answer_digest).to be_blank
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'allows to call :security_answer_digest directly if :protect_setter_for_digest is not given as option' do
|
84
|
+
lambda do
|
85
|
+
subject.security_answer_digest = 'hello'
|
86
|
+
expect(subject.security_answer_digest).to eq('hello')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "#security_answer=" do
|
91
|
+
it 'sets the :security_answer and saves the digest' do
|
92
|
+
model = FactoryGirl.create(:model_with_attr_digest, security_answer: 'old answer', security_answer_confirmation: 'old answer')
|
93
|
+
expect(model.security_answer_digest).to_not be_blank
|
94
|
+
old_security_answer_digest = model.security_answer_digest
|
95
|
+
model.security_answer = 'new answer'
|
96
|
+
model.security_answer_confirmation = 'new answer'
|
97
|
+
expect(model.instance_variable_get(:@security_answer)).to eq('new answer')
|
98
|
+
model.save!
|
99
|
+
expect(model.security_answer_digest).to_not be_blank
|
100
|
+
expect(model.security_answer_digest).to_not eq(old_security_answer_digest)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#authenticate_security_answer' do
|
105
|
+
it 'returns true if :security_answer given matches the one stored' do
|
106
|
+
model = FactoryGirl.create(:model_with_attr_digest, security_answer: 'some answer', security_answer_confirmation: 'some answer')
|
107
|
+
expect(model.authenticate_security_answer('some answer')).to be(true)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'returns false if :security_answer given does not match the one stored' do
|
111
|
+
model = FactoryGirl.create(:model_with_attr_digest, security_answer: 'some answer', security_answer_confirmation: 'some answer')
|
112
|
+
expect(model.authenticate_security_answer('some other answer')).to be(false)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
describe ModelWithAttrDigestAndValidationsOption do
|
119
|
+
it 'responds to :security_answer' do
|
120
|
+
respond_to(:security_answer)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'responds to :security_answer=' do
|
124
|
+
respond_to(:security_answer=)
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'responds to :security_answer_confirmation' do
|
128
|
+
respond_to(:security_answer_confirmation)
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'responds to :security_answer_confirmation=' do
|
132
|
+
respond_to(:security_answer_confirmation=)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'responds to :authenticate_security_answer' do
|
136
|
+
respond_to(:authenticate_security_answer)
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'does not require :security_answer on create' do
|
140
|
+
expect(subject).to be_new_record
|
141
|
+
subject.security_answer = nil
|
142
|
+
subject.valid?
|
143
|
+
expect(subject.errors[:security_answer]).to be_blank
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'does not require :security_answer_confirmation if :security_answer given' do
|
147
|
+
subject.security_answer = 'hello there'
|
148
|
+
subject.valid?
|
149
|
+
expect(subject.errors[:security_answer_confirmation]).to be_blank
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'does not require :security_answer_confirmation if :security_answer is not given' do
|
153
|
+
subject.security_answer = ''
|
154
|
+
subject.valid?
|
155
|
+
expect(subject.errors[:security_answer_confirmation]).to be_blank
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'does not require :security_answer_digest on create' do
|
159
|
+
subject = FactoryGirl.build(:model_with_attr_digest_and_validations_option)
|
160
|
+
expect(subject).to be_new_record
|
161
|
+
# change the security_answer_digest to verify the test
|
162
|
+
subject.security_answer_digest = ''
|
163
|
+
subject.save!
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'does not require :security_answer_digest on update' do
|
167
|
+
subject = FactoryGirl.build(:model_with_attr_digest_and_validations_option)
|
168
|
+
expect(subject).to be_new_record
|
169
|
+
subject.save!
|
170
|
+
# change the :security_answer_digest to verify the test
|
171
|
+
subject.send(:security_answer_digest=, '')
|
172
|
+
subject.save!
|
173
|
+
subject.reload
|
174
|
+
expect(subject.security_answer_digest).to be_blank
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
describe ModelWithAttrDigestAndProtectedOption do
|
179
|
+
it 'does not allow to call to protected setter for :security_answer_digest' do
|
180
|
+
model = FactoryGirl.create(:model_with_attr_digest_and_protected_option, security_answer: 'Answer', security_answer_confirmation: 'Answer')
|
181
|
+
expect(lambda do
|
182
|
+
model.security_answer_digest = 'hello'
|
183
|
+
end).to raise_error(NoMethodError)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe ModelWithAttrDigestAndCaseSensitiveOption do
|
188
|
+
it 'authenticates even if :security_answer is of different case' do
|
189
|
+
model = FactoryGirl.create(:model_with_attr_digest_and_case_sensitive_option, security_answer: 'Answer', security_answer_confirmation: 'Answer')
|
190
|
+
expect(model.authenticate_security_answer('answer')).to eq(true)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
describe ModelWithAttrDigestAndConfirmationOption do
|
195
|
+
it 'does not respond to :security_answer_confirmation' do
|
196
|
+
respond_to(:security_answer_confirmation) == false
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'does not respond to :security_answer_confirmation=' do
|
200
|
+
respond_to(:security_answer_confirmation=) == false
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'allows to create and save without any confirmation on :security_answer' do
|
204
|
+
model = FactoryGirl.create(:model_with_attr_digest_and_confirmation_option, security_answer: 'Answer')
|
205
|
+
model.save!
|
206
|
+
expect(model.authenticate_security_answer('another answer')).to be(false)
|
207
|
+
expect(model.authenticate_security_answer('Answer')).to be(true)
|
208
|
+
end
|
209
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
begin
|
2
|
+
require 'simplecov'
|
3
|
+
SimpleCov.start
|
4
|
+
if ENV['CI']=='true'
|
5
|
+
require 'codecov'
|
6
|
+
SimpleCov.formatter = SimpleCov::Formatter::Codecov
|
7
|
+
end
|
8
|
+
rescue LoadError
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'active_record'
|
12
|
+
|
13
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: File.dirname(__FILE__) + "/tmp/test.sqlite3")
|
14
|
+
|
15
|
+
puts "Using ActiveRecord #{ActiveRecord::VERSION::STRING}"
|
16
|
+
|
17
|
+
load File.dirname(__FILE__) + '/support/schema.rb'
|
18
|
+
|
19
|
+
require File.dirname(__FILE__) + '/../lib/attr_digest.rb'
|
20
|
+
|
21
|
+
require 'support/models'
|
22
|
+
|
23
|
+
require 'factory_girl'
|
24
|
+
FactoryGirl.find_definitions
|
25
|
+
|
26
|
+
require "rspec/expectations"
|
27
|
+
|
28
|
+
RSpec.configure do |config|
|
29
|
+
config.before(:each) do
|
30
|
+
ModelWithAttrDigest.delete_all
|
31
|
+
end
|
32
|
+
|
33
|
+
config.order = 'random'
|
34
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class ModelWithAttrDigest < ActiveRecord::Base
|
2
|
+
attr_digest :password
|
3
|
+
attr_digest :security_answer
|
4
|
+
|
5
|
+
validates :username, presence: true, uniqueness: { case_sensitive: false }
|
6
|
+
validates :security_question, presence: true
|
7
|
+
end
|
8
|
+
|
9
|
+
class ModelWithAttrDigestAndCaseSensitiveOption < ActiveRecord::Base
|
10
|
+
self.table_name = "model_with_attr_digests"
|
11
|
+
|
12
|
+
attr_digest :password
|
13
|
+
attr_digest :security_answer, case_sensitive: false
|
14
|
+
|
15
|
+
validates :username, presence: true, uniqueness: { case_sensitive: false }
|
16
|
+
validates :security_question, presence: true
|
17
|
+
end
|
18
|
+
|
19
|
+
class ModelWithAttrDigestAndProtectedOption < ActiveRecord::Base
|
20
|
+
self.table_name = "model_with_attr_digests"
|
21
|
+
|
22
|
+
attr_digest :password
|
23
|
+
attr_digest :security_answer, protected: true
|
24
|
+
|
25
|
+
validates :username, presence: true, uniqueness: { case_sensitive: false }
|
26
|
+
validates :security_question, presence: true
|
27
|
+
end
|
28
|
+
|
29
|
+
class ModelWithAttrDigestAndValidationsOption < ActiveRecord::Base
|
30
|
+
self.table_name = "model_with_attr_digests"
|
31
|
+
|
32
|
+
attr_digest :password
|
33
|
+
attr_digest :security_answer, validations: false
|
34
|
+
|
35
|
+
validates :username, presence: true, uniqueness: { case_sensitive: false }
|
36
|
+
validates :security_question, presence: true
|
37
|
+
end
|
38
|
+
|
39
|
+
class ModelWithAttrDigestAndConfirmationOption < ActiveRecord::Base
|
40
|
+
self.table_name = "model_with_attr_digests"
|
41
|
+
|
42
|
+
attr_digest :password
|
43
|
+
attr_digest :security_answer, confirmation: false
|
44
|
+
|
45
|
+
validates :username, presence: true, uniqueness: { case_sensitive: false }
|
46
|
+
validates :security_question, presence: true
|
47
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
|
3
|
+
self.verbose = false
|
4
|
+
|
5
|
+
create_table :model_with_attr_digests, force: true do |t|
|
6
|
+
t.string :username, null: false
|
7
|
+
t.string :password_digest, null: false
|
8
|
+
t.string :security_question, null: false
|
9
|
+
t.string :security_answer_digest, null: false
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
change_table :model_with_attr_digests do |t|
|
14
|
+
t.index :username, unique: true
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
Binary file
|
metadata
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: attr_digest
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jurgen Jocubeit
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-02-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: argon2
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.1.4
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.1.4
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.5'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.5'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.4'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.4'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.3'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.3'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: factory_girl
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '4.5'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '4.5'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: simplecov
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.11.2
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.11.2
|
125
|
+
description: Provides functionality to store a hash digest of an attribute using Argon2
|
126
|
+
email:
|
127
|
+
- support@brightcommerce.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- CHANGELOG.md
|
133
|
+
- MIT-LICENSE
|
134
|
+
- README.md
|
135
|
+
- lib/attr_digest.rb
|
136
|
+
- lib/attr_digest/attr_digest.rb
|
137
|
+
- lib/attr_digest/version.rb
|
138
|
+
- spec/factories/models.rb
|
139
|
+
- spec/lib/attr_digest_spec.rb
|
140
|
+
- spec/spec_helper.rb
|
141
|
+
- spec/support/models.rb
|
142
|
+
- spec/support/schema.rb
|
143
|
+
- spec/tmp/test.sqlite3
|
144
|
+
homepage: https://github.com/brightcommerce/attr_digest
|
145
|
+
licenses:
|
146
|
+
- MIT
|
147
|
+
metadata:
|
148
|
+
copyright: Copyright 2016 Brightcommerce, Inc.
|
149
|
+
post_install_message:
|
150
|
+
rdoc_options: []
|
151
|
+
require_paths:
|
152
|
+
- lib
|
153
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '2.2'
|
158
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
requirements: []
|
164
|
+
rubyforge_project:
|
165
|
+
rubygems_version: 2.4.5.1
|
166
|
+
signing_key:
|
167
|
+
specification_version: 4
|
168
|
+
summary: AttrDigest v1.0.0
|
169
|
+
test_files: []
|