signed_form 0.1.2 → 0.2.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.
@@ -0,0 +1,2 @@
1
+ <%= f.text_field :name %>
2
+ <%= f.text_field :birthdate %>
@@ -0,0 +1,4 @@
1
+ <%= form_for :user, signed: true, url: '/users', method: :post do |f| %>
2
+ <%= f.text_field :address %>
3
+ <%= render partial: '/fields', locals: { f: f } %>
4
+ <% end %>
@@ -20,18 +20,35 @@ class Widget
20
20
  end
21
21
  end
22
22
 
23
+ class ControllerRenderer < AbstractController::Base
24
+ include AbstractController::Rendering
25
+ self.view_paths = [ActionView::FileSystemResolver.new(File.join(File.dirname(__FILE__), 'fixtures', 'views'))]
26
+
27
+ view_context_class.class_eval do
28
+ def url_for(*args)
29
+ '/users'
30
+ end
31
+
32
+ def protect_against_forgery?
33
+ false
34
+ end
35
+ end
36
+ end
37
+
38
+ class MockBuilder < ActionView::Helpers::FormBuilder; end
39
+
23
40
  describe SignedForm::FormBuilder do
24
41
  include SignedFormViewHelper
25
42
 
26
- before { SignedForm::HMAC.secret_key = "abc123" }
27
- after { SignedForm::HMAC.secret_key = nil }
43
+ before { SignedForm.secret_key = "abc123" }
44
+ before { SignedForm.options[:digest] = false }
28
45
 
29
46
  let(:user) { User.new }
30
47
  let(:widget) { Widget.new }
31
48
 
32
- describe "signed_form_for" do
49
+ describe "form_for tag" do
33
50
  it "should build a form with signature" do
34
- content = signed_form_for(User.new) do |f|
51
+ content = form_for(User.new, signed: true) do |f|
35
52
  f.text_field :name
36
53
  end
37
54
 
@@ -43,17 +60,73 @@ describe SignedForm::FormBuilder do
43
60
  content.should =~ Regexp.new(regex, Regexp::MULTILINE)
44
61
  end
45
62
 
46
- it "should set a target" do
47
- content = signed_form_for(User.new, sign_destination: true) do |f|
63
+ it "should not sign if no option is present" do
64
+ content = form_for(User.new) do |f|
48
65
  f.text_field :name
49
66
  end
50
67
 
68
+ content.should_not =~ /form_signature/
69
+ end
70
+
71
+ context "signed default" do
72
+ before { SignedForm.options[:signed] = true }
73
+
74
+ it "should sign a form when the default option is set" do
75
+ content = form_for(User.new) do |f|
76
+ f.text_field :name
77
+ end
78
+
79
+ content.should =~ /form_signature/
80
+ end
81
+
82
+ it "should allow the form to be overriden" do
83
+ content = form_for(User.new, signed: false) do |f|
84
+ f.text_field :name
85
+ end
86
+
87
+ content.should_not =~ /form_signature/
88
+ end
89
+ end
90
+ end
91
+
92
+ describe "third party builders" do
93
+ it "should build a signed form" do
94
+ content = form_for(User.new, signed: true, builder: MockBuilder) do |f|
95
+ f.text_field :name
96
+ end
51
97
  data = get_data_from_form(content)
52
- data.size.should == 2
53
- data.should include(:__options__)
54
- data[:__options__].should include(:method, :url)
55
- data[:__options__][:method].should == :post
56
- data[:__options__][:url].should == '/users'
98
+ data['user'].should include(:name)
99
+ end
100
+
101
+ it "should raise if a builder isn't supported" do
102
+ expect { form_for(User.new, signed: true, builder: Class.new) {} }.to raise_error
103
+ end
104
+ end
105
+
106
+ describe "sign_destination" do
107
+ after do
108
+ @data.should include(:_options_)
109
+ @data[:_options_].should include(:method, :url)
110
+ @data[:_options_][:method].should == :post
111
+ @data[:_options_][:url].should == '/users'
112
+ end
113
+
114
+ it "should set a target" do
115
+ content = form_for(User.new, signed: true, sign_destination: true) do |f|
116
+ f.text_field :name
117
+ end
118
+
119
+ @data = get_data_from_form(content)
120
+ end
121
+
122
+ it "should set a target when the default options are enabled" do
123
+ SignedForm.options[:sign_destination] = true
124
+
125
+ content = form_for(User.new, signed: true) do |f|
126
+ f.text_field :name
127
+ end
128
+
129
+ @data = get_data_from_form(content)
57
130
  end
58
131
  end
59
132
 
@@ -69,18 +142,16 @@ describe SignedForm::FormBuilder do
69
142
  :object, :radio_button, :parent_builder,
70
143
  :collection_check_boxes, :grouped_collection_select, :select,
71
144
  :collection_select, :collection_radio_buttons, :time_select,
72
- :datetime_select, :time_zone_select, :date_select]
145
+ :datetime_select, :time_zone_select, :date_select, :search_field]
73
146
 
74
147
  after do
75
- @data.should be
76
- @data.size.should == 1
77
148
  @data['user'].size.should == 1
78
149
  @data['user'].should include(:name)
79
150
  end
80
151
 
81
152
  fields.each do |field|
82
153
  it "should add to the allowed attributes when #{field} is used" do
83
- content = signed_form_for(User.new) do |f|
154
+ content = form_for(User.new, signed: true) do |f|
84
155
  f.send field, :name
85
156
  end
86
157
 
@@ -89,7 +160,7 @@ describe SignedForm::FormBuilder do
89
160
  end
90
161
 
91
162
  it "should add to the allowed attributes when collection_check_boxes is used", action_pack: /4\.\d+/ do
92
- content = signed_form_for(User.new) do |f|
163
+ content = form_for(User.new, signed: true) do |f|
93
164
  f.collection_check_boxes :name, ['a', 'b'], :to_s, :to_s
94
165
  end
95
166
 
@@ -100,7 +171,7 @@ describe SignedForm::FormBuilder do
100
171
  continent = Struct.new('Continent', :continent_name, :countries)
101
172
  country = Struct.new('Country', :country_id, :country_name)
102
173
 
103
- content = signed_form_for(User.new) do |f|
174
+ content = form_for(User.new, signed: true) do |f|
104
175
  f.grouped_collection_select(:name, [continent.new("<Africa>", [country.new("<sa>", "<South Africa>")])],
105
176
  :countries, :continent_name, :country_id, :country_name)
106
177
  end
@@ -109,7 +180,7 @@ describe SignedForm::FormBuilder do
109
180
  end
110
181
 
111
182
  it "should add to the allowed attributes when select is used" do
112
- content = signed_form_for(User.new) do |f|
183
+ content = form_for(User.new, signed: true) do |f|
113
184
  f.select :name, %w(a b)
114
185
  end
115
186
 
@@ -117,7 +188,7 @@ describe SignedForm::FormBuilder do
117
188
  end
118
189
 
119
190
  it "should add to the allowed attributes when collection_select is used" do
120
- content = signed_form_for(User.new) do |f|
191
+ content = form_for(User.new, signed: true) do |f|
121
192
  f.collection_select :name, %w(a b), :to_s, :to_s
122
193
  end
123
194
 
@@ -125,7 +196,7 @@ describe SignedForm::FormBuilder do
125
196
  end
126
197
 
127
198
  it "should add to the allowed attributes when collection_radio_buttons is used", action_pack: /4\.\d+/ do
128
- content = signed_form_for(User.new) do |f|
199
+ content = form_for(User.new, signed: true) do |f|
129
200
  f.collection_radio_buttons :name, %w(a b), :to_s, :to_s
130
201
  end
131
202
 
@@ -133,7 +204,7 @@ describe SignedForm::FormBuilder do
133
204
  end
134
205
 
135
206
  it "should add to the allowed attributes when date_select is used" do
136
- content = signed_form_for(User.new) do |f|
207
+ content = form_for(User.new, signed: true) do |f|
137
208
  f.date_select :name
138
209
  end
139
210
 
@@ -141,7 +212,7 @@ describe SignedForm::FormBuilder do
141
212
  end
142
213
 
143
214
  it "should add to the allowed attributes when time_select is used" do
144
- content = signed_form_for(User.new) do |f|
215
+ content = form_for(User.new, signed: true) do |f|
145
216
  f.time_select :name
146
217
  end
147
218
 
@@ -149,7 +220,7 @@ describe SignedForm::FormBuilder do
149
220
  end
150
221
 
151
222
  it "should add to the allowed attributes when datetime_select is used" do
152
- content = signed_form_for(User.new) do |f|
223
+ content = form_for(User.new, signed: true) do |f|
153
224
  f.datetime_select :name
154
225
  end
155
226
 
@@ -157,7 +228,7 @@ describe SignedForm::FormBuilder do
157
228
  end
158
229
 
159
230
  it "should add to the allowed attributes when time_zone_select is used" do
160
- content = signed_form_for(User.new) do |f|
231
+ content = form_for(User.new, signed: true) do |f|
161
232
  f.time_zone_select :name
162
233
  end
163
234
 
@@ -165,7 +236,7 @@ describe SignedForm::FormBuilder do
165
236
  end
166
237
 
167
238
  it "should add to the allowed attributes when radio_button is used" do
168
- content = signed_form_for(User.new) do |f|
239
+ content = form_for(User.new, signed: true) do |f|
169
240
  f.radio_button :name, ['bar']
170
241
  end
171
242
 
@@ -175,7 +246,7 @@ describe SignedForm::FormBuilder do
175
246
 
176
247
  describe "add_signed_fields" do
177
248
  it "should add fields to the marshaled data" do
178
- content = signed_form_for(User.new) do |f|
249
+ content = form_for(User.new, signed: true) do |f|
179
250
  f.add_signed_fields :name, :address
180
251
  end
181
252
 
@@ -189,7 +260,7 @@ describe SignedForm::FormBuilder do
189
260
  it "should nest attributes" do
190
261
  user.stub(widgets: [widget])
191
262
 
192
- content = signed_form_for(user) do |f|
263
+ content = form_for(user, signed: true) do |f|
193
264
  f.fields_for :widgets do |ff|
194
265
  ff.text_field :name
195
266
  end
@@ -200,7 +271,7 @@ describe SignedForm::FormBuilder do
200
271
  end
201
272
 
202
273
  it "should deeply nest attributes" do
203
- content = signed_form_for(:author, url: '/') do |f|
274
+ content = form_for(:author, url: '/', signed: true) do |f|
204
275
  f.fields_for :books do |ff|
205
276
  ff.text_field :name
206
277
  ff.check_box :hardcover
@@ -218,7 +289,7 @@ describe SignedForm::FormBuilder do
218
289
  end
219
290
 
220
291
  specify "nested arrays should not have duplicates" do
221
- content = signed_form_for(:author, url: '/') do |f|
292
+ content = form_for(:author, url: '/', signed: true) do |f|
222
293
  f.fields_for :books do |ff|
223
294
  ff.text_field :name
224
295
  ff.text_field :name
@@ -230,7 +301,7 @@ describe SignedForm::FormBuilder do
230
301
  end
231
302
 
232
303
  specify "attribute arrays should not have duplicates" do
233
- content = signed_form_for(:author, url: '/') do |f|
304
+ content = form_for(:author, url: '/', signed: true) do |f|
234
305
  f.text_field :name
235
306
  f.text_field :name
236
307
  end
@@ -239,4 +310,40 @@ describe SignedForm::FormBuilder do
239
310
  data[:author].size.should == 1
240
311
  end
241
312
  end
313
+
314
+ describe "form digests" do
315
+ before { SignedForm.options[:digest] = true }
316
+
317
+ let (:controller) { ControllerRenderer.new }
318
+
319
+ it "should append a digest to the marshaled data" do
320
+ controller.render template: 'form'
321
+
322
+ data = get_data_from_form(controller.response_body)
323
+ data[:_options_].should include(:digest)
324
+ end
325
+
326
+ it "should not digest if the option is disabled" do
327
+ SignedForm.options[:digest] = false
328
+
329
+ controller.render template: 'form'
330
+ data = get_data_from_form(controller.response_body)
331
+ data[:_options_].should_not include(:digest)
332
+ end
333
+
334
+ it "should get the digest from the view paths" do
335
+ controller.render template: 'form'
336
+ data = get_data_from_form(controller.response_body)
337
+ digestor = data[:_options_][:digest]
338
+ digestor.view_paths = controller.view_paths
339
+ digestor.to_s.should == "6a161ab9978322e8251d809b3558ab1a"
340
+ end
341
+
342
+ it "should set a grace period" do
343
+ controller.render template: 'form'
344
+ data = get_data_from_form(controller.response_body)
345
+ data[:_options_].should include(:digest_expiration)
346
+ (Time.now..(Time.now + SignedForm.options[:digest_grace_period])).should cover(data[:_options_][:digest_expiration])
347
+ end
348
+ end
242
349
  end
data/spec/hmac_spec.rb CHANGED
@@ -1,34 +1,24 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe SignedForm::HMAC do
4
- describe 'create_hmac' do
5
- it 'should raise if no key is given' do
6
- expect { SignedForm::HMAC.create_hmac "foo" }.to raise_error(SignedForm::Errors::NoSecretKey)
7
- end
4
+ it 'should raise if no key is given' do
5
+ expect { SignedForm::HMAC.new }.to raise_error(SignedForm::Errors::NoSecretKey)
6
+ end
8
7
 
9
- context 'when a key is present' do
10
- before { SignedForm::HMAC.secret_key = "superdupersecret" }
11
- after { SignedForm::HMAC.secret_key = nil }
8
+ describe 'create' do
9
+ let(:hmac) { SignedForm::HMAC.new(secret_key: "superdupersecret") }
12
10
 
13
- it 'should create a hex signature' do
14
- SignedForm::HMAC.create_hmac("my signed message").length.should == 40
15
- end
11
+ it 'should create a hex signature' do
12
+ hmac.create("my signed message").length.should == 40
13
+ hmac.create("my signed message").should == "93c1ecd4c10122cbf873ca6cf9eff08888565054"
16
14
  end
17
15
  end
18
16
 
19
- describe 'verify_hmac' do
20
- it 'should raise if no key is given' do
21
- expect { SignedForm::HMAC.verify_hmac 'foo', 'bar' }.to raise_error(SignedForm::Errors::NoSecretKey)
22
- end
23
-
24
- context 'when a key is present' do
25
- before { SignedForm::HMAC.secret_key = "superdupersecret" }
26
- after { SignedForm::HMAC.secret_key = nil }
27
-
28
- let(:signature) { SignedForm::HMAC.create_hmac "My super secret" }
17
+ describe 'verify' do
18
+ let(:hmac) { SignedForm::HMAC.new(secret_key: "superdupersecret") }
19
+ let(:signature) { hmac.create "My super secret" }
29
20
 
30
- specify { SignedForm::HMAC.verify_hmac(signature, "My super secret").should be_true }
31
- specify { SignedForm::HMAC.verify_hmac(signature, "My bad secret").should_not be_true }
32
- end
21
+ specify { hmac.verify(signature, "My super secret").should be_true }
22
+ specify { hmac.verify(signature, "My bad secret").should_not be_true }
33
23
  end
34
24
  end
@@ -4,77 +4,94 @@ class Controller < ActionController::Base
4
4
  include SignedForm::ActionController::PermitSignedParams
5
5
 
6
6
  public :permit_signed_form_data
7
+
8
+ def view_paths
9
+ [File.join(File.dirname(__FILE__), 'fixtures', 'views')]
10
+ end
7
11
  end
8
12
 
9
13
  describe SignedForm::ActionController::PermitSignedParams do
10
14
  let(:controller) { Controller.new }
15
+ let(:hmac) { SignedForm::HMAC.new(secret_key: SignedForm.secret_key) }
16
+ let(:params) { controller.params }
17
+ let(:template) { double(view_paths: [File.join(File.dirname(__FILE__), 'fixtures', 'views')], virtual_path: 'form') }
18
+ let(:digestor) { SignedForm::Digestor.new(template) }
19
+
20
+ def marshal_and_sign(data)
21
+ encoded_data = Base64.strict_encode64(Marshal.dump(data))
22
+ signature = hmac.create(encoded_data)
23
+ "#{encoded_data}--#{signature}"
24
+ end
11
25
 
12
26
  before do
13
- SignedForm::HMAC.secret_key = "abc123"
27
+ SignedForm.secret_key = "abc123"
14
28
 
15
29
  Controller.any_instance.stub(request: double('request', method: 'POST', request_method: 'POST', fullpath: '/users', url: '/users'))
16
30
  Controller.any_instance.stub(params: { "user" => { name: "Erich Menge", occupation: 'developer' } })
17
- end
18
31
 
19
- after { SignedForm::HMAC.secret_key = nil }
32
+ params.stub(:[]).and_call_original
33
+ params.stub(:[]).with('user').and_return(params)
34
+ end
20
35
 
21
36
  it "should raise if signature isn't valid" do
22
- controller.params['form_signature'] = "bad signature"
37
+ params['form_signature'] = "bad signature"
23
38
  expect { controller.permit_signed_form_data }.to raise_error(SignedForm::Errors::InvalidSignature)
24
39
  end
25
40
 
26
- it "should permit attributes that are allowed" do
27
- params = controller.params
28
-
29
- data = Base64.strict_encode64(Marshal.dump("user" => [:name]))
30
- signature = SignedForm::HMAC.create_hmac(data)
31
-
32
- params['form_signature'] = "#{data}--#{signature}"
33
-
34
- params.should_receive(:require).with('user').and_return(params)
35
- params.should_receive(:permit).with(:name).and_return(params)
36
- controller.permit_signed_form_data
37
- end
38
-
39
- it "should verify current url matches targeted url" do
40
- params = controller.params
41
+ context "when the parameters are good" do
42
+ before { params.should_receive(:permit).with(:name).and_return(params) }
41
43
 
42
- data = Base64.strict_encode64(Marshal.dump("user" => [:name], :__options__ => { method: 'post', url: '/users' }))
43
- signature = SignedForm::HMAC.create_hmac(data)
44
+ it "should permit attributes that are allowed" do
45
+ params['form_signature'] = marshal_and_sign "user" => [:name]
46
+ controller.permit_signed_form_data
47
+ end
44
48
 
45
- params['form_signature'] = "#{data}--#{signature}"
49
+ it "should verify current url matches targeted url" do
50
+ params['form_signature'] = marshal_and_sign("user" => [:name], :_options_ => { method: 'post', url: '/users' })
46
51
 
47
- params.stub(:require).with('user').and_return(params)
48
- params.stub(:permit).with(:name).and_return(params)
49
- controller.request.should_receive(:fullpath).and_return '/users'
50
- controller.permit_signed_form_data
52
+ controller.request.should_receive(:fullpath).and_return '/users'
53
+ controller.permit_signed_form_data
54
+ end
51
55
  end
52
56
 
53
57
  it "should reject if url doesn't match" do
54
- params = controller.params
55
-
56
- data = Base64.strict_encode64(Marshal.dump("user" => [:name], :__options__ => { method: 'post', url: '/admin' }))
57
- signature = SignedForm::HMAC.create_hmac(data)
58
-
59
- params['form_signature'] = "#{data}--#{signature}"
60
-
61
- params.stub(:require).with('user').and_return(params)
62
- params.stub(:permit).with(:name).and_return(params)
58
+ params['form_signature'] = marshal_and_sign("user" => [:name], :_options_ => { method: 'post', url: '/admin' })
59
+ expect { controller.permit_signed_form_data }.to raise_error(SignedForm::Errors::InvalidURL)
60
+ end
63
61
 
62
+ it "should reject if the method doesn't match" do
63
+ params['form_signature'] = marshal_and_sign("user" => [:name], :_options_ => { method: 'put', url: '/users' })
64
64
  expect { controller.permit_signed_form_data }.to raise_error(SignedForm::Errors::InvalidURL)
65
65
  end
66
66
 
67
- it "should reject if method doesn't match" do
68
- params = controller.params
67
+ context "when the digest is bad" do
68
+ before { digestor.stub(:to_s).and_return "bad" }
69
+
70
+ it "should not reject if inside grace period" do
71
+ params['form_signature'] = marshal_and_sign("user" => [:name], :_options_ => { digest: digestor, digest_expiration: Time.now + 20 })
72
+ expect { controller.permit_signed_form_data }.not_to raise_error(SignedForm::Errors::ExpiredForm)
73
+ end
69
74
 
70
- data = Base64.strict_encode64(Marshal.dump("user" => [:name], :__options__ => { method: 'put', url: '/users' }))
71
- signature = SignedForm::HMAC.create_hmac(data)
75
+ it "should reject if outside the grace period" do
76
+ params['form_signature'] = marshal_and_sign("user" => [:name], :_options_ => { digest: digestor, digest_expiration: Time.now - 20 })
77
+ expect { controller.permit_signed_form_data }.to raise_error(SignedForm::Errors::ExpiredForm)
78
+ end
72
79
 
73
- params['form_signature'] = "#{data}--#{signature}"
80
+ it "should reject if no grace period" do
81
+ params['form_signature'] = marshal_and_sign("user" => [:name], :_options_ => { digest: digestor })
82
+ expect { controller.permit_signed_form_data }.to raise_error(SignedForm::Errors::ExpiredForm)
83
+ end
84
+ end
74
85
 
75
- params.stub(:require).with('user').and_return(params)
76
- params.stub(:permit).with(:name).and_return(params)
86
+ context "when the digest is good" do
87
+ it "should not reject if outside grace period" do
88
+ params['form_signature'] = marshal_and_sign("user" => [:name], :_options_ => { digest: digestor, digest_expiration: Time.now - 20 })
89
+ expect { controller.permit_signed_form_data }.not_to raise_error(SignedForm::Errors::ExpiredForm)
90
+ end
77
91
 
78
- expect { controller.permit_signed_form_data }.to raise_error(SignedForm::Errors::InvalidURL)
92
+ it "should not reject if no grace period" do
93
+ params['form_signature'] = marshal_and_sign("user" => [:name], :_options_ => { digest: digestor })
94
+ expect { controller.permit_signed_form_data }.not_to raise_error(SignedForm::Errors::ExpiredForm)
95
+ end
79
96
  end
80
97
  end