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.
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