signed_form 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d48194df6c241a277cc79382344715c75aac7d9c
4
- data.tar.gz: 279e08bc94e45b2ce9f5e25364104bd5433ecb34
3
+ metadata.gz: 3ebbb64117232076f2250063ef9e6f06805df345
4
+ data.tar.gz: cb322f4dbce4c9ec4621779215c6aa7a2bbf003a
5
5
  SHA512:
6
- metadata.gz: 75e21c1f42bfa9b1e8df77e4afbabb6d8ffc8a5295ddbc53d90347bfae0ed391e73f0a97b529e7b0e835957b737acf4f2deb9f692063c277408c66602a09d11a
7
- data.tar.gz: 448f4016cf450248230406b3f89947f953398b81dcf56cdf06c9433eda9f9ad806e091aa93d7895ac6e62982dce271055b35101a0349b3952c62292bf1cd5498
6
+ metadata.gz: 90a7f07fcd52abf71694bf4af3a4e10b64c30cb44a0f346f092b639c909ed4c6564419b0aef11bc4f92de52cb11be1c70b6faa62779c0ef1d970ca5e0820239f
7
+ data.tar.gz: 0b64c9465db9bd2ebb543be19b060cf684862da2928190f53e74de3956010692512db40fddf0b35c8983a73c0dd35803db447bd5a28f7f62da801d42ed890a30
data/.travis.yml CHANGED
@@ -1,6 +1,5 @@
1
1
  script: bundle exec rspec
2
2
  language: ruby
3
- before_install: gem install bundler
4
3
 
5
4
  rvm:
6
5
  - 1.9.3
@@ -9,4 +8,9 @@ rvm:
9
8
  env:
10
9
  - RAILS_VERSION=3-1-stable
11
10
  - RAILS_VERSION=3-2-stable
11
+ - RAILS_VERSION=4-0-stable
12
12
  - RAILS_VERSION=master
13
+
14
+ matrix:
15
+ allow_failures:
16
+ - env: RAILS_VERSION=master
data/.yardopts CHANGED
@@ -1,3 +1,2 @@
1
- --exclude features
2
1
  --no-private
3
2
  --markup markdown
data/Changes.md ADDED
@@ -0,0 +1,30 @@
1
+ ## 0.2.0
2
+
3
+ * Instead of using `signed_form_for` add an option for form signing to `form_for` so that signing third party builders
4
+ like SimpleForm doesn't require an adapter.
5
+ * Move configuration options to main module name-space.
6
+ * Add default options hash to be passed to `form_for`.
7
+ * Add a digestor to verify that out dated forms aren't being submitted.
8
+ * Add a test helper to make testing controllers easy.
9
+ * Only permit parameters but don't require them. Requiring them raises an exception if they're missing from the form
10
+ submission. But in cases where other parameters are sent as well and the form object may be optional this would raise
11
+ an exception that would be undesired.
12
+ * Allow all forms to be signed by default.
13
+
14
+ ## 0.1.2
15
+
16
+ * Fix issues where request method was not being compared properly and request
17
+ url would not handle some potential cases leading to an erroneous rejection of
18
+ the form. [Marc Schütz, #6]
19
+
20
+ ## 0.1.1
21
+
22
+ * Add some select and date/time field helpers that were not getting added to the signature [#5].
23
+
24
+ ## 0.1.0
25
+
26
+ * Add `sign_destination` option to `signed_form_for`.
27
+
28
+ ## 0.0.1
29
+
30
+ * Initial Release
data/Gemfile CHANGED
@@ -8,6 +8,8 @@ rails_version = ENV['RAILS_VERSION'] || 'master'
8
8
  case rails_version
9
9
  when /master/
10
10
  gem "rails", github: "rails/rails"
11
+ when /4-0-stable/
12
+ gem "rails", github: "rails/rails", branch: "4-0-stable"
11
13
  when /3-2-stable/
12
14
  gem "rails", github: "rails/rails", branch: "3-2-stable"
13
15
  gem "strong_parameters"
data/README.md CHANGED
@@ -3,9 +3,19 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/signed_form.png)](http://badge.fury.io/rb/signed_form)
4
4
  [![Build Status](https://travis-ci.org/erichmenge/signed_form.png?branch=master)](https://travis-ci.org/erichmenge/signed_form)
5
5
  [![Code Climate](https://codeclimate.com/github/erichmenge/signed_form.png)](https://codeclimate.com/github/erichmenge/signed_form)
6
+ [![Coverage Status](https://coveralls.io/repos/erichmenge/signed_form/badge.png?branch=master)](https://coveralls.io/r/erichmenge/signed_form)
6
7
 
7
8
  SignedForm brings new convenience and security to your Rails 4 or Rails 3 application.
8
9
 
10
+ SignedForm is under active development. Please make sure you're reading the README associated with the version of
11
+ SignedForm you're using. Click the tag link on GitHub to switch to the version you've installed to get the correct
12
+ README.
13
+
14
+ Or be brave and bundle the gem straight from GitHub master.
15
+
16
+ A nicely displayed version of this README complete with table of contents is available
17
+ [here](http://erichmenge.com/signed_form/).
18
+
9
19
  ## How It Works
10
20
 
11
21
  Traditionally, when you create a form with Rails you enter your fields using something like `f.text_field :name` and so
@@ -19,8 +29,8 @@ no more `attr_accessible`. It just works.
19
29
 
20
30
  What this looks like:
21
31
 
22
- ``` erb
23
- <%= signed_form_for(@user) do |f| %>
32
+ ```erb
33
+ <%= form_for @user, signed: true do |f| %>
24
34
  <% f.add_signed_fields :zipcode, :state # Optionally add additional fields to sign %>
25
35
 
26
36
  <%= f.text_field :name %>
@@ -29,9 +39,9 @@ What this looks like:
29
39
  <% end %>
30
40
  ```
31
41
 
32
- ``` ruby
42
+ ```ruby
33
43
  UsersController < ApplicationController
34
- def create
44
+ def update
35
45
  @user = User.find params[:id]
36
46
  @user.update_attributes params[:user]
37
47
  end
@@ -39,11 +49,21 @@ end
39
49
  ```
40
50
 
41
51
  That's it. You're done. Need to add a field? Pop it in the form. You don't need to then update a list of attributes.
42
- `signed_form_for` works just like the standard `form_for`.
43
52
 
44
53
  Of course, you're free to continue using the standard `form_for`. `SignedForm` is strictly opt-in. It won't change the
45
54
  way you use standard forms.
46
55
 
56
+ ## More than just Convenience - Security
57
+
58
+ SignedForm protects you in 3 ways:
59
+
60
+ * Form fields are signed, so no alteration of the fields are allowed.
61
+ * Form actions are signed. That means a form with an action of `/admin/users/3` will not work when submitted to `/users/3`.
62
+ * Form views are digested (see below). So if you remove a field from your form, old forms will not be accepted despite
63
+ a valid signature.
64
+
65
+ The second two methods of security are optional and can be turned off globally or on a form by form basis.
66
+
47
67
  ## Requirements
48
68
 
49
69
  SignedForm requires:
@@ -56,7 +76,9 @@ SignedForm requires:
56
76
 
57
77
  Add this line to your application's Gemfile:
58
78
 
59
- gem 'signed_form'
79
+ ```ruby
80
+ gem 'signed_form'
81
+ ```
60
82
 
61
83
  And then execute:
62
84
 
@@ -70,7 +92,7 @@ If you're using Rails 4, it works out of the box.
70
92
  You'll need to include `SignedForm::ActionController::PermitSignedParams` in the controller(s) you want to use
71
93
  SignedForm with. This can be done application wide by adding the `include` to your ApplicationController.
72
94
 
73
- ``` ruby
95
+ ```ruby
74
96
  ApplicationController < ActionController::Base
75
97
  include SignedForm::ActionController::PermitSignedParams
76
98
 
@@ -80,54 +102,145 @@ end
80
102
 
81
103
  You'll also need to create an initializer:
82
104
 
83
- $ echo 'SignedForm::HMAC.secret_key = SecureRandom.hex(64)' > config/initializers/signed_form.rb
105
+ ```shell
106
+ $ echo "SignedForm.secret_key = '$(rake secret)'" > config/initializers/signed_form.rb
107
+ ```
84
108
 
85
- **IMPORTANT** Please read below for information regarding this secret key.
109
+ You'll probably want to keep this out of version control. Treat this key like you would your session secret, keep it
110
+ private.
86
111
 
87
112
  ## Support for other Builders
88
113
 
89
- * [SimpleForm Adapter](https://github.com/erichmenge/signed_form-simple_form)
114
+ Any form that wraps `form_for` and the default field helpers will work with SignedForm. For example, a signed SimpleForm
115
+ might look like this:
90
116
 
91
- ## Special Considerations
117
+ ```erb
118
+ <%= simple_form_for @user, signed: true do |f| %>
119
+ f.input :name
120
+ <% end %>
121
+ ```
92
122
 
93
- If you're running only a single application server the above initializer should work great for you, with a couple of
94
- caveats. If a user is in process of filling out a form and you restart your server, their form will be invalidated.
95
- You could pick a secret key using `rake secret` and put that in the initializer instead, but then in the event you
96
- remove a field someone could still access it using the old signature if some malicious person were to keep it around.
123
+ This will create a signed form as expected.
97
124
 
98
- If you're running multiple application servers, the above initializer will not work. You'll need to keep the key in sync
99
- between all the servers. The security caveat with that is that if you ever remove a field from a form without updating
100
- that secret key, a malicious user could still access the field with the old signature. So you'll probably want to choose
101
- a new secret in the event you remove access to an attribute in a form.
125
+ For builders that don't use the standard field helpers under the hood, you can create an adapter like this:
102
126
 
103
- My above initializer example errs on the side of caution, generating a new secret key every time the app starts up. Only
104
- you can decide what is right for you with respect to the secret key.
127
+ ```ruby
128
+ class MyAdapter < SomeOtherBuilder
129
+ include SignedForm::FormBuilder
105
130
 
106
- ### Multiple Access Points
131
+ def some_helper(field, *other_args)
132
+ add_signed_fields field
133
+ super
134
+ end
135
+ end
136
+ ```
107
137
 
108
- Take for example the case where you have an administrative backend. You might have `/admin/users/edit`. Users can also
109
- change some information about themselves though, so there's `/users/edit` as well. Now you have an admin that gets
110
- demoted, but still has a user account. If that admin were to retain a form signature from `/admin/users/edit` they could
111
- use that signature to modify the same fields from `/users/edit`. As a means of preventing such access SignedForm provides
112
- the `sign_destination` option to `signed_form_for`. Example:
138
+ Then in your view:
113
139
 
114
- ``` erb
115
- <%= signed_form_for(@user, sign_destination: true) do |f| %>
116
- <%= f.text_field :name %>
117
- <!-- ... -->
140
+ ```erb
141
+ <%= form_for @user, signed: true, builder: MyAdapter do |f| %>
142
+ <%= f.some_helper :name %>
118
143
  <% end %>
119
144
  ```
120
145
 
121
- With `sign_destination` enabled, a form generated with a destination of `/admin/users/5` for example will only be
122
- accepted at that end point. The form would not be accepted at `/users/5`. So in the event you would like to use
123
- SignedForm on forms for the same resource, but different access levels, you have protection against the form being used
124
- elsewhere.
146
+ ## Form Digests
147
+
148
+ SignedForm will create a digest of all the views/partials involved with rendering your form. If the form is modifed old
149
+ forms will be expired. This is done to eliminate the possibility of old forms coming back to bite you.
150
+
151
+ By default, there is a 5 minute grace period before old forms will be rejected. This is done so that if you make a
152
+ trivial change to a form you won't prevent a form a user is currently filling out from being accepted when you
153
+ restart your server.
154
+
155
+ Of course if a critical mistake is made (such as allowing an admin field to be set in the form) you could change the
156
+ secret key to prevent any old form from getting through.
157
+
158
+ By default, these digests are not cached. That means that each form that is submitted will have the views be digested
159
+ again. Most views and partials are relatively small so the cost of computing the MD5 hash of the files is not very
160
+ expensive. However, if this is something you care about SignedForm also provides a memory store
161
+ (`SignedForm::DigestStores::MemoryStore`) that will cache the digests in memory. Other stores could be used as well, as
162
+ long as the object responds to `#fetch` taking the cache key as an argument as well as the block that will return the
163
+ digest.
164
+
165
+ ## Example Configuration
166
+
167
+ An example config/initializers/signed_form.rb might look something like this (these are the defaults, with the exception
168
+ of the key of course):
169
+
170
+ ```ruby
171
+ SignedForm.config do |c|
172
+ c.options[:sign_destination] = true
173
+ c.options[:digest] = true
174
+ c.options[:digest_grace_period] = 300
175
+ c.options[:signed] = false # If true, sign all forms by default
176
+
177
+ c.digest_store = SignedForm::DigestStores::NullStore.new
178
+ c.secret_key = 'supersecret'
179
+ end
180
+ ```
181
+
182
+ Those options that are in the options hash are the default per-form options. They can be overridden by passing the same
183
+ option to the `form_for` method.
184
+
185
+ ## Testing Your Controllers
186
+
187
+ Because your tests won't include a signature you will get a `ForbiddenAttributes` exception in your tests that do mass
188
+ assignment. SignedForm includes a test helper method, `permit_all_parameters` that works with both TestUnit and RSpec.
189
+
190
+ In your `spec_helper` file or `test_helper` file `require 'signed_form/test_helper'`. Then `include
191
+ SignedForm::TestHelper` in tests where you need it. An example is below.
192
+
193
+ **Caution**: `permit_all_parameters` without a block modifies the singleton class of the controller under test which
194
+ lasts for the duration of the test. If you want `permit_all_parameters` to be limited to a specific part of the test,
195
+ pass it a block and only that block will be affected. Example:
196
+
197
+ ```ruby
198
+ describe CarsController do
199
+ include SignedForm::TestHelper
200
+
201
+ describe "POST create" do
202
+ it "should create a car" do
203
+ permit_all_parameters do
204
+ # This won't raise ForbiddenAttributesError
205
+ post :create, {:car => valid_attributes}, valid_session
206
+ end
207
+
208
+ # This one will raise
209
+ post :create, {:car => valid_attributes}, valid_session
210
+
211
+ # ...
212
+ end
213
+ end
214
+ end
215
+ ```
216
+
217
+ Example without a block:
218
+
219
+ ```ruby
220
+ describe CarsController do
221
+ include SignedForm::TestHelper
222
+
223
+ describe "POST create" do
224
+ before { permit_all_parameters }
225
+
226
+ describe "with valid params" do
227
+ it "assigns a newly created car as @car" do
228
+ post :create, {:car => valid_attributes}, valid_session
229
+
230
+ assigns(:car).should be_a(Car)
231
+ assigns(:car).should be_persisted
232
+ end
233
+
234
+ # ...
235
+ end
236
+ end
237
+ end
238
+ ```
125
239
 
126
- ### Caching
240
+ ## I want to hear from you
127
241
 
128
- Another consideration to be aware of is caching. If you cache a form, and then change the secret key that form will
129
- perpetually submit parameters that fail verification. So if you want to cache the form you should tie the cache key to
130
- something that will be changed whenever the secret key changes.
242
+ If you're using SignedForm, I'd love to hear from you. What do you like? What could be better? I'd love to hear your
243
+ ideas. Join the mailing list on librelist to join the discussion at [signedform@librelist.com](mailto:signedform@librelist.com).
131
244
 
132
245
  ## Contributing
133
246
 
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Form is Expired</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <div class="dialog">
21
+ <h1>Form Expired</h1>
22
+ <p>The form you submitted has expired. Please go back and refresh the form to try again.</p>
23
+ </div>
24
+ </body>
25
+ </html>
data/lib/signed_form.rb CHANGED
@@ -5,5 +5,36 @@ require "signed_form/version"
5
5
  require "signed_form/errors"
6
6
  require "signed_form/form_builder"
7
7
  require "signed_form/hmac"
8
+ require "signed_form/digest_stores"
9
+ require "signed_form/digestor"
8
10
  require "signed_form/action_view/form_helper"
11
+ require "signed_form/gate_keeper"
9
12
  require "signed_form/action_controller/permit_signed_params"
13
+ require "signed_form/engine" if defined?(Rails)
14
+
15
+ module SignedForm
16
+ DEFAULT_OPTIONS = {
17
+ sign_destination: true,
18
+ digest: true,
19
+ digest_grace_period: 300,
20
+ signed: false
21
+ }.freeze
22
+
23
+ class << self
24
+ attr_accessor :secret_key
25
+
26
+ attr_writer :options
27
+ def options
28
+ @options ||= DEFAULT_OPTIONS.dup
29
+ end
30
+
31
+ attr_writer :digest_store
32
+ def digest_store
33
+ @digest_store ||= SignedForm::DigestStores::NullStore.new
34
+ end
35
+
36
+ def config
37
+ yield self
38
+ end
39
+ end
40
+ end
@@ -15,24 +15,17 @@ module SignedForm
15
15
  def permit_signed_form_data
16
16
  return if request.method == 'GET' || params['form_signature'].blank?
17
17
 
18
- data, signature = params['form_signature'].split('--', 2)
18
+ gate_keeper = GateKeeper.new(self)
19
19
 
20
- signature ||= ''
21
-
22
- raise Errors::InvalidSignature, "Form signature is not valid" unless SignedForm::HMAC.verify_hmac signature, data
23
-
24
- allowed_attributes = Marshal.load Base64.strict_decode64(data)
25
- options = allowed_attributes.delete(:__options__)
26
-
27
- if options
28
- raise Errors::InvalidURL if options[:method].to_s.casecmp(request.request_method) != 0
29
-
30
- url = url_for(options[:url])
31
- raise Errors::InvalidURL if url != request.fullpath && url != request.url
20
+ gate_keeper.allowed_attributes.each do |k, v|
21
+ next if params[k].nil? || v.empty?
22
+ params[k] = params[k].permit(*v)
32
23
  end
33
-
34
- allowed_attributes.each do |k, v|
35
- params[k] = params.require(k).permit(*v)
24
+ rescue Errors::ExpiredForm
25
+ if defined?(Rails)
26
+ render 'signed_form/expired_form', status: 500, layout: nil
27
+ else
28
+ raise
36
29
  end
37
30
  end
38
31
  end
@@ -2,13 +2,27 @@ module SignedForm
2
2
  module ActionView
3
3
  module FormHelper
4
4
 
5
- # This is a wrapper around ActionView's form_for helper.
6
- #
7
5
  # @option options :sign_destination [Boolean] Only the URL given/created will be allowed to receive the form.
8
- def signed_form_for(record, options = {}, &block)
9
- options[:builder] ||= SignedForm::FormBuilder
6
+ # @option options :digest [Boolean] Digest and verify the views have not been modified
7
+ # @option options :digest_grace_period [Integer] Time in seconds to allow old forms
8
+ # @option options :wrap_form [Symbol] Method of a form builder to wrap. Default is form_for
9
+ def form_for(record, options = {}, &block)
10
+ if options[:signed].nil? && !SignedForm.options[:signed] || !options[:signed].nil? && !options[:signed]
11
+ return super
12
+ end
13
+
14
+ options = SignedForm.options.merge options
15
+ options[:builder] ||= ::ActionView::Helpers::FormBuilder
16
+
17
+ ancestors = options[:builder].ancestors
18
+
19
+ if !ancestors.include?(::ActionView::Helpers::FormBuilder) && !ancestors.include?(SignedForm::FormBuilder)
20
+ raise "Form signing not supported on builders that don't subclass ActionView::Helpers::FormBuilder or include SignedForm::FormBuilder"
21
+ elsif !ancestors.include?(SignedForm::FormBuilder)
22
+ options[:builder] = SignedForm::FormBuilder::BUILDERS[options[:builder]]
23
+ end
10
24
 
11
- form_for(record, options) do |f|
25
+ super record, options do |f|
12
26
  output = capture(f, &block)
13
27
  f.form_signature_tag + output
14
28
  end