has_secure_attribute 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 791d8b4c2ad53a2ab79fe663e7e6e7e19b355e90
4
+ data.tar.gz: ecebf104e0f461d50c064d86d9441581f6950b9f
5
+ SHA512:
6
+ metadata.gz: 758ae145496f31c48766909bffd81d03dd316a458e3bea9d6027061de6bf2b6437d94e58fb6f57db087d9579e003a98b2b70f8760cc74fd9ab37f28eee32e7b1
7
+ data.tar.gz: 42305c94aede34da459b9f0611ca57a157808c5556027c3225628651485adc5107a9d53893de1d950cf50a46c2daad708120fd88dda4e48ddb6e9ff3f2b45771
data/MIT-LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2013 Panayotis Matsinopoulos
2
+
3
+ Contact: panayotis@matsinopoulos.gr
4
+ Site: http://www.matsinopoulos.gr
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ Under construction
2
+ ==================
@@ -0,0 +1,71 @@
1
+ require 'bcrypt'
2
+
3
+ module ActiveModel
4
+ module SecureAttribute
5
+ extend ActiveSupport::Concern
6
+
7
+ class << self
8
+ attr_accessor :min_cost
9
+ end
10
+ self.min_cost = false
11
+
12
+ module ClassMethods
13
+ def method_missing(meth, *args, &block)
14
+ if meth.to_s =~ /^has_secure_(.+)$/
15
+ has_secure_attribute($1, *args, &block)
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ def has_secure_attribute(meth, *args, &block)
22
+ attribute_sym = meth.to_sym
23
+ attr_reader attribute_sym # setter is defined later on
24
+ options = {:validations => true, :protect_setter_for_digest => false}
25
+ options.merge! args[0] unless args.blank?
26
+ if options[:validations]
27
+ validates attribute_sym, confirmation: true, if: lambda { |m| m.send(attribute_sym).present? }
28
+ validates attribute_sym, presence: true, on: :create
29
+ validates "#{attribute_sym}_confirmation".to_sym, presence: true, if: lambda { |m| m.send(attribute_sym).present? }
30
+ before_create { raise "#{attribute_sym}_digest missing on new record" if send("#{attribute_sym}_digest").blank? }
31
+ end
32
+
33
+ define_setter(attribute_sym)
34
+ protect_setter_for_digest(attribute_sym) if options[:protect_setter_for_digest]
35
+
36
+ define_authenticate_method(attribute_sym)
37
+ end
38
+
39
+ def define_setter(attribute_sym)
40
+ define_method "#{attribute_sym.to_s}=" do |unencrypted_value|
41
+ unless unencrypted_value.blank?
42
+ instance_variable_set("@#{attribute_sym.to_s}".to_sym, unencrypted_value)
43
+ cost = ActiveModel::SecureAttribute.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine::DEFAULT_COST
44
+ send("#{attribute_sym.to_s}_digest=".to_sym, BCrypt::Password.create(unencrypted_value, cost: cost))
45
+ end
46
+ end
47
+ end
48
+
49
+ def protect_setter_for_digest(attribute_sym)
50
+ define_method "#{attribute_sym}_digest=" do |value|
51
+ write_attribute "#{attribute_sym}_digest".to_sym, value
52
+ end
53
+ protected "#{attribute_sym}_digest=".to_sym
54
+ end
55
+
56
+ def define_authenticate_method(attribute_sym)
57
+ define_method "authenticate_#{attribute_sym}" do |value|
58
+ BCrypt::Password.new(send("#{attribute_sym}_digest")) == value && self
59
+ end
60
+ end
61
+
62
+ protected :has_secure_attribute
63
+ protected :define_setter
64
+ protected :protect_setter_for_digest
65
+ protected :define_authenticate_method
66
+ end
67
+
68
+ end
69
+ end
70
+
71
+ ActiveRecord::Base.send :include, ActiveModel::SecureAttribute
@@ -0,0 +1,3 @@
1
+ module HasSecureAttribute
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1 @@
1
+ require 'active_model/secure_attribute/has_secure_attribute'
@@ -0,0 +1,33 @@
1
+ development:
2
+ adapter: mysql2
3
+ encoding: utf8
4
+ reconnect: false
5
+ database: has_secure_attribute_development
6
+ pool: 5
7
+ username: root
8
+ password:
9
+ socket: /var/run/mysqld/mysqld.sock
10
+
11
+ # Warning: The database defined as "test" will be erased and
12
+ # re-generated from your development database when you run "rake".
13
+ # Do not set this db to the same as development or production.
14
+ test:
15
+ adapter: mysql2
16
+ encoding: utf8
17
+ reconnect: false
18
+ database: has_secure_attribute_test
19
+ pool: 5
20
+ username: root
21
+ password:
22
+ socket: /var/run/mysqld/mysqld.sock
23
+
24
+ production:
25
+ adapter: mysql2
26
+ encoding: utf8
27
+ reconnect: false
28
+ database: has_secure_attribute_production
29
+ pool: 5
30
+ username: root
31
+ password:
32
+ socket: /var/run/mysqld/mysqld.sock
33
+
@@ -0,0 +1,24 @@
1
+ class CreateTestModelWithAttributes < ActiveRecord::Migration
2
+ def up
3
+ HasSecureAttribute::DbHelper.new.connect_to_database
4
+ return if ActiveRecord::Base.connection.table_exists? :test_model_with_attributes
5
+ create_table :test_model_with_attributes 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
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :test_model_with_attributes, [:username], :unique => true, :name => 'test_model_with_attributes_username_uidx'
15
+ end
16
+
17
+ def down
18
+ HasSecureAttribute::DbHelper.new.connect_to_database
19
+ return unless ActiveRecord::Base.connection.table_exists? :test_model_with_attributes
20
+ drop_table :test_model_with_attributes
21
+ end
22
+
23
+
24
+ end
@@ -0,0 +1,15 @@
1
+ module HasSecureAttribute
2
+ class DbHelper
3
+ def connect_to_database
4
+ filename = File.expand_path("../../../config/database.yml", __FILE__)
5
+ database_settings = YAML.load_file(filename)
6
+ ActiveRecord::Base.establish_connection database_settings['test']
7
+ end
8
+
9
+ def connect_to_server
10
+ filename = File.expand_path("../../../config/database.yml", __FILE__)
11
+ database_settings = YAML.load_file(filename)
12
+ ActiveRecord::Base.establish_connection database_settings['test'].delete_if{|k,v| 'database' == k}
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ FactoryGirl.define do
2
+ factory :test_model_with_attribute do
3
+ username 'username'
4
+ password 'password'
5
+ password_confirmation 'password'
6
+ security_question 'question'
7
+ security_answer 'answer'
8
+ security_answer_confirmation 'answer'
9
+ end
10
+
11
+ factory :test_model_with_attribute_no_validation do
12
+ username 'username_no_validation'
13
+ password 'password'
14
+ password_confirmation 'password'
15
+ security_question 'question'
16
+ security_answer 'answer'
17
+ end
18
+
19
+ factory :test_model_with_attribute_protect_setter_for_digest do
20
+ username 'username_protect'
21
+ password 'password'
22
+ password_confirmation 'password'
23
+ security_question 'question'
24
+ security_answer 'answer'
25
+ security_answer_confirmation 'answer'
26
+ end
27
+ end
@@ -0,0 +1,165 @@
1
+ require 'spec_helper'
2
+
3
+ describe TestModelWithAttribute do
4
+ it { should respond_to(:security_answer) }
5
+ it { should respond_to(:security_answer=) }
6
+ it { should respond_to(:security_answer_confirmation) }
7
+ it { should respond_to(:security_answer_confirmation=) }
8
+ it { should respond_to(:authenticate_security_answer) }
9
+
10
+ it 'should confirm security answer' do
11
+ subject.security_answer = 'hello there'
12
+ subject.security_answer_confirmation = 'there hello'
13
+ subject.valid?
14
+ subject.errors[:security_answer_confirmation].should include("doesn't match Security answer")
15
+ end
16
+
17
+ it 'should not confirm security answer if not given' do
18
+ subject.security_answer = nil
19
+ subject.security_answer_confirmation = 'there hello'
20
+ subject.valid?
21
+ subject.errors[:security_answer_confirmation].should be_blank
22
+ end
23
+
24
+ it 'should require security answer on create' do
25
+ subject.should be_new_record
26
+ subject.security_answer = nil
27
+ subject.valid?
28
+ subject.errors[:security_answer].should include("can't be blank")
29
+ end
30
+
31
+ it 'should require security answer confirmation if security answer given ' do
32
+ subject.security_answer = 'hello there'
33
+ subject.valid?
34
+ subject.errors[:security_answer_confirmation].should include "can't be blank"
35
+ end
36
+
37
+ it 'should not require security answer confirmation if security answer is not given' do
38
+ subject.security_answer = ''
39
+ subject.valid?
40
+ subject.errors[:security_answer_confirmation].should be_blank
41
+ end
42
+
43
+ it 'should require security answer digest on create' do
44
+ subject = FactoryGirl.build :test_model_with_attribute
45
+ subject.should be_new_record
46
+
47
+ # change the security_answer_digest to verify the test
48
+ subject.security_answer_digest = ''
49
+ lambda do
50
+ begin
51
+ subject.save!
52
+ rescue Exception => ex
53
+ ex.message.should include("security_answer_digest missing on new record")
54
+ raise
55
+ end
56
+ end.should raise_error RuntimeError
57
+ end
58
+
59
+ it 'should not require security answer digest on update' do
60
+ subject = FactoryGirl.build :test_model_with_attribute
61
+ subject.should be_new_record
62
+
63
+ subject.save!
64
+
65
+ # change the security_answer_digest to verify the test
66
+ subject.security_answer_digest = ''
67
+
68
+ subject.save!
69
+
70
+ subject.reload
71
+ subject.security_answer_digest.should be_blank
72
+ end
73
+
74
+ it 'should allow to call security answer digest directly if protect setter for digest is not given as option' do
75
+ lambda do
76
+ subject.security_answer_digest = 'hello'
77
+ end.should_not raise_error
78
+ end
79
+
80
+ describe "#security_answer=" do
81
+ it 'should set the security answer and save it encrypted' do
82
+ tmwa = FactoryGirl.create :test_model_with_attribute, security_answer: 'old answer', security_answer_confirmation: 'old answer'
83
+ tmwa.security_answer_digest.should_not be_blank
84
+ old_security_answer_digest = tmwa.security_answer_digest
85
+
86
+ tmwa.security_answer = 'new answer'
87
+ tmwa.security_answer_confirmation = 'new answer'
88
+ tmwa.instance_variable_get(:@security_answer).should == 'new answer'
89
+ tmwa.save!
90
+ tmwa.security_answer_digest.should_not be_blank
91
+ tmwa.security_answer_digest.should_not == old_security_answer_digest
92
+ end
93
+ end
94
+
95
+ describe '#authenticate_security_answer' do
96
+ it 'should return subject if security answer given matches the one stored' do
97
+ tmwa = FactoryGirl.create :test_model_with_attribute, security_answer: 'some answer', security_answer_confirmation: 'some answer'
98
+ tmwa.authenticate_security_answer('some answer').should eq tmwa
99
+ end
100
+
101
+ it 'should return false if security answer given does not match the one stored' do
102
+ tmwa = FactoryGirl.create :test_model_with_attribute, security_answer: 'some answer', security_answer_confirmation: 'some answer'
103
+ tmwa.authenticate_security_answer('some other answer').should be_false
104
+ end
105
+ end
106
+ end
107
+
108
+ describe TestModelWithAttributeNoValidation do
109
+ it { should respond_to(:security_answer) }
110
+ it { should respond_to(:security_answer=) }
111
+ it { should_not respond_to(:security_answer_confirmation) }
112
+ it { should_not respond_to(:security_answer_confirmation=) }
113
+ it { should respond_to(:authenticate_security_answer) }
114
+
115
+ it 'should not require security answer on create' do
116
+ subject.should be_new_record
117
+ subject.security_answer = nil
118
+ subject.valid?
119
+ subject.errors[:security_answer].should be_blank
120
+ end
121
+
122
+ it 'should not require security answer confirmation if security answer given ' do
123
+ subject.security_answer = 'hello there'
124
+ subject.valid?
125
+ subject.errors[:security_answer_confirmation].should be_blank
126
+ end
127
+
128
+ it 'should not require security answer confirmation if security answer is not given' do
129
+ subject.security_answer = ''
130
+ subject.valid?
131
+ subject.errors[:security_answer_confirmation].should be_blank
132
+ end
133
+
134
+ it 'should not require security answer digest on create' do
135
+ subject = FactoryGirl.build :test_model_with_attribute_no_validation
136
+ subject.should be_new_record
137
+
138
+ # change the security_answer_digest to verify the test
139
+ subject.security_answer_digest = ''
140
+ subject.save!
141
+ end
142
+
143
+ it 'should not require security answer digest on update' do
144
+ subject = FactoryGirl.build :test_model_with_attribute_no_validation
145
+ subject.should be_new_record
146
+
147
+ subject.save!
148
+
149
+ # change the security_answer_digest to verify the test
150
+ subject.send :security_answer_digest=, ''
151
+
152
+ subject.save!
153
+
154
+ subject.reload
155
+ subject.security_answer_digest.should be_blank
156
+ end
157
+ end
158
+
159
+ describe TestModelWithAttributeProtectSetterForDigest do
160
+ it 'should not allow to call security answer digest directly' do
161
+ lambda do
162
+ subject.security_answer_digest = 'hello'
163
+ end.should raise_error NoMethodError
164
+ end
165
+ end
@@ -0,0 +1,7 @@
1
+ class TestModelWithAttribute < ActiveRecord::Base
2
+ has_secure_password
3
+ has_secure_security_answer
4
+
5
+ validates :username, :presence => true, :uniqueness => {:case_sensitive => false}
6
+ validates :security_question, :presence => true
7
+ end
@@ -0,0 +1,8 @@
1
+ class TestModelWithAttributeNoValidation < ActiveRecord::Base
2
+ self.table_name = "test_model_with_attributes"
3
+ has_secure_password
4
+ has_secure_security_answer :validations => false
5
+
6
+ validates :username, :presence => true, :uniqueness => {:case_sensitive => false}
7
+ validates :security_question, :presence => true
8
+ end
@@ -0,0 +1,8 @@
1
+ class TestModelWithAttributeProtectSetterForDigest < ActiveRecord::Base
2
+ self.table_name = "test_model_with_attributes"
3
+ has_secure_password
4
+ has_secure_security_answer :protect_setter_for_digest => true
5
+
6
+ validates :username, :presence => true, :uniqueness => {:case_sensitive => false}
7
+ validates :security_question, :presence => true
8
+ end
@@ -0,0 +1,35 @@
1
+ require 'active_record'
2
+
3
+ require File.expand_path("../db/migrate/db_helper", __FILE__)
4
+ HasSecureAttribute::DbHelper.new.connect_to_database
5
+
6
+ require File.expand_path("../../lib/active_model/secure_attribute/has_secure_attribute", __FILE__)
7
+ require File.expand_path("../models/test_model_with_attribute", __FILE__)
8
+ require File.expand_path("../models/test_model_with_attribute_no_validation", __FILE__)
9
+ require File.expand_path("../models/test_model_with_attribute_protect_setter_for_digest", __FILE__)
10
+
11
+ require 'factory_girl'
12
+ FactoryGirl.find_definitions
13
+ require 'database_cleaner'
14
+
15
+ RSpec.configure do |config|
16
+
17
+ # Run specs in random order to surface order dependencies. If you find an
18
+ # order dependency and want to debug it, you can fix the order by providing
19
+ # the seed, which is printed after each run.
20
+ # --seed 1234
21
+ config.order = "random"
22
+
23
+ config.before(:suite) do
24
+ DatabaseCleaner.strategy = :transaction
25
+ DatabaseCleaner.clean_with(:truncation)
26
+ end
27
+
28
+ config.before(:each) do
29
+ DatabaseCleaner[:active_record].start
30
+ end
31
+
32
+ config.after(:each) do
33
+ DatabaseCleaner.clean
34
+ end
35
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_secure_attribute
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Panayotis Matsinopoulos
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-08-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bcrypt-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 3.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 3.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: mysql2
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: factory_girl
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: database_cleaner
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Does what `has_secure_password` does, but for any attribute that you
112
+ want. It does not have to be a `password` attribute. It may be for example `security_answer`
113
+ email:
114
+ - panayotis@matsinopoulos.gr
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - lib/active_model/secure_attribute/has_secure_attribute.rb
120
+ - lib/has_secure_attribute.rb
121
+ - lib/has_secure_attribute/version.rb
122
+ - spec/spec_helper.rb
123
+ - spec/factories/test_model_with_attributes.rb
124
+ - spec/config/database.yml
125
+ - spec/models/has_secure_attribute_spec.rb
126
+ - spec/models/test_model_with_attribute_no_validation.rb
127
+ - spec/models/test_model_with_attribute_protect_setter_for_digest.rb
128
+ - spec/models/test_model_with_attribute.rb
129
+ - spec/db/migrate/db_helper.rb
130
+ - spec/db/migrate/create_test_model_with_attributes.rb
131
+ - README.md
132
+ - MIT-LICENSE
133
+ homepage: https://github.com/pmatsinopoulos/has_secure_attribute
134
+ licenses: []
135
+ metadata: {}
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - '>='
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 2.0.3
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: Allows an ActiveRecord::Base class to declare an attribute that will be saved
156
+ one-way encrypted and not clear text
157
+ test_files: []