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,7 @@
1
+ require 'signed_form/digest_stores/null_store'
2
+
3
+ module SignedForm
4
+ module DigestStores
5
+ autoload :MemoryStore, 'signed_form/digest_stores/memory_store'
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'active_support'
2
+
3
+ module SignedForm
4
+ module DigestStores
5
+ class MemoryStore < ActiveSupport::Cache::MemoryStore; end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module SignedForm
2
+ module DigestStores
3
+ class NullStore
4
+ def fetch(key)
5
+ yield
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,67 @@
1
+ require 'set'
2
+
3
+ module SignedForm
4
+ class Digestor
5
+ attr_accessor :view_paths
6
+
7
+ def initialize(template)
8
+ @view_paths = Set.new
9
+ @views = Set.new
10
+ self << template
11
+ end
12
+
13
+ def <<(template)
14
+ virtual_path = get_virtual_path(template)
15
+ raise Errors::UnableToDigest, "Unable to get virtual path from template" unless virtual_path
16
+
17
+ @views << virtual_path
18
+ @view_paths += template.view_paths.map(&:to_s)
19
+ @digest = nil
20
+ rescue NoMethodError
21
+ raise Errors::UnableToDigest, "Unable get view paths from template"
22
+ end
23
+
24
+ def marshal_dump
25
+ [@views.to_a, to_s]
26
+ end
27
+
28
+ def marshal_load(input)
29
+ @views, @digest = input
30
+ @view_paths = []
31
+ @digest.taint
32
+ end
33
+
34
+ def to_s
35
+ @digest ||= SignedForm.digest_store.fetch(@views.sort.join(':')) { hash_files(glob_files) }
36
+ end
37
+ alias_method :digest, :to_s
38
+
39
+ def refresh
40
+ @digest = nil
41
+ end
42
+
43
+ private
44
+
45
+ def glob_files
46
+ globbed_files = []
47
+ view_paths.each do |path|
48
+ @views.each { |view| globbed_files += Dir["#{path}/#{view}.*"] }
49
+ end
50
+ globbed_files
51
+ end
52
+
53
+ def hash_files(files)
54
+ raise Errors::UnableToDigest, "No files to digest" if files.empty?
55
+
56
+ md5 = Digest::MD5.new
57
+ files.sort.each do |entry|
58
+ File.open(entry) { |f| md5 << f.read }
59
+ end
60
+ md5.to_s
61
+ end
62
+
63
+ def get_virtual_path(template)
64
+ template.respond_to?(:virtual_path) ? template.virtual_path : template.instance_variable_get(:@virtual_path)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,5 @@
1
+ module SignedForm
2
+ module Rails
3
+ class Engine < ::Rails::Engine; end
4
+ end
5
+ end
@@ -1,7 +1,9 @@
1
1
  module SignedForm
2
2
  module Errors
3
- class NoSecretKey < StandardError; end
3
+ class NoSecretKey < StandardError; end
4
4
  class InvalidSignature < StandardError; end
5
- class InvalidURL < StandardError; end
5
+ class InvalidURL < StandardError; end
6
+ class UnableToDigest < StandardError; end
7
+ class ExpiredForm < StandardError; end
6
8
  end
7
9
  end
@@ -1,85 +1,99 @@
1
1
  module SignedForm
2
- class FormBuilder < ::ActionView::Helpers::FormBuilder
2
+ module FormBuilder
3
+ FIELDS_TO_SIGN = [:select, :collection_select, :grouped_collection_select,
4
+ :time_zone_select, :collection_radio_buttons, :collection_check_boxes,
5
+ :date_select, :datetime_select, :time_select,
6
+ :text_field, :password_field, :hidden_field,
7
+ :file_field, :text_area, :check_box,
8
+ :radio_button, :color_field,
9
+ :telephone_field, :phone_field, :date_field,
10
+ :time_field, :datetime_field, :datetime_local_field,
11
+ :month_field, :week_field, :url_field,
12
+ :email_field, :number_field, :range_field]
3
13
 
4
- # Base methods for form signing. Include this module in your own builders to get signatures for the base input
5
- # helpers. Add fields to sign with #add_signed_fields
6
- module Methods
7
- FIELDS_TO_SIGN = [:select, :collection_select, :grouped_collection_select,
8
- :time_zone_select, :collection_radio_buttons, :collection_check_boxes,
9
- :date_select, :datetime_select, :time_select,
10
- :text_field, :password_field, :hidden_field,
11
- :file_field, :text_area, :check_box,
12
- :radio_button, :color_field, :search_field,
13
- :telephone_field, :phone_field, :date_field,
14
- :time_field, :datetime_field, :datetime_local_field,
15
- :month_field, :week_field, :url_field,
16
- :email_field, :number_field, :range_field]
14
+ FIELDS_TO_SIGN.delete_if { |e| !::ActionView::Helpers::FormBuilder.instance_methods.include?(e) }
15
+ FIELDS_TO_SIGN.freeze
17
16
 
18
- FIELDS_TO_SIGN.delete_if { |e| !::ActionView::Helpers::FormBuilder.instance_methods.include?(e) }
19
- FIELDS_TO_SIGN.freeze
20
-
21
- FIELDS_TO_SIGN.each do |h|
22
- define_method(h) do |field, *args|
23
- add_signed_fields field
24
- super(field, *args)
25
- end
17
+ FIELDS_TO_SIGN.each do |h|
18
+ define_method(h) do |field, *args|
19
+ add_signed_fields field
20
+ super(field, *args)
26
21
  end
22
+ end
27
23
 
28
- def initialize(*)
29
- super
30
- if options[:signed_attributes_object]
31
- self.signed_attributes_object = options[:signed_attributes_object]
32
- else
33
- self.signed_attributes = { object_name => [] }
34
- self.signed_attributes_object = signed_attributes[object_name]
35
- end
24
+ BUILDERS = Hash.new do |h,k|
25
+ h[k] = Class.new(k) do
26
+ include FormBuilder
36
27
  end
28
+ end
37
29
 
38
- def form_signature_tag
39
- signed_attributes.each { |k,v| v.uniq! if v.is_a?(Array) }
40
- signed_attributes[:__options__] = { method: options[:html][:method], url: options[:url] } if options[:sign_destination]
41
- encoded_data = Base64.strict_encode64 Marshal.dump(signed_attributes)
42
- signature = SignedForm::HMAC.create_hmac(encoded_data)
43
- token = "#{encoded_data}--#{signature}"
44
- %(<input type="hidden" name="form_signature" value="#{token}" />\n).html_safe
30
+ def initialize(*)
31
+ super
32
+ if options[:signed_attributes_context]
33
+ @signed_attributes_context = options[:signed_attributes_context]
34
+ else
35
+ @signed_attributes = { object_name => [] }
36
+ @signed_attributes_context = @signed_attributes[object_name]
37
+ prepare_signed_attributes_hash
45
38
  end
39
+ end
46
40
 
47
- # Wrapper for Rails fields_for
48
- #
49
- # @see http://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-fields_for
50
- def fields_for(record_name, record_object = nil, fields_options = {}, &block)
51
- hash = {}
52
- array = []
41
+ def form_signature_tag
42
+ @signed_attributes.each { |k,v| v.uniq! if v.is_a?(Array) }
43
+ encoded_data = Base64.strict_encode64 Marshal.dump(@signed_attributes)
53
44
 
54
- if nested_attributes_association?(record_name)
55
- hash["#{record_name}_attributes"] = fields_options[:signed_attributes_object] = array
56
- else
57
- hash[record_name] = fields_options[:signed_attributes_object] = array
58
- end
45
+ hmac = SignedForm::HMAC.new(secret_key: SignedForm.secret_key)
46
+ signature = hmac.create(encoded_data)
47
+ token = "#{encoded_data}--#{signature}"
48
+ %(<input type="hidden" name="form_signature" value="#{token}" />\n).html_safe
49
+ end
59
50
 
60
- add_signed_fields hash
51
+ # Wrapper for Rails fields_for
52
+ #
53
+ # @see http://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-fields_for
54
+ def fields_for(record_name, record_object = nil, fields_options = {}, &block)
55
+ hash = {}
56
+ array = []
61
57
 
62
- content = super
63
- array.uniq!
64
- content
58
+ if nested_attributes_association?(record_name)
59
+ hash["#{record_name}_attributes"] = fields_options[:signed_attributes_context] = array
60
+ else
61
+ hash[record_name] = fields_options[:signed_attributes_context] = array
65
62
  end
66
63
 
67
- # This method is used to add additional fields to sign. A usecase for this may be if you want to add fields later with javascript.
68
- #
69
- # @example
70
- # <%= signed_form_for(@user) do |f| %>
71
- # <% f.add_signed_fields :name, :address
72
- # <% end %>
73
- #
74
- def add_signed_fields(*fields)
75
- signed_attributes_object.push(*fields)
76
- end
64
+ add_signed_fields hash
77
65
 
78
- private
66
+ content = super
67
+ array.uniq!
68
+ content
69
+ end
79
70
 
80
- attr_accessor :signed_attributes, :signed_attributes_object
71
+ # This method is used to add additional fields to sign. A usecase for this may be if you want to add fields later with javascript.
72
+ #
73
+ # @example
74
+ # <%= signed_form_for(@user) do |f| %>
75
+ # <% f.add_signed_fields :name, :address
76
+ # <% end %>
77
+ #
78
+ def add_signed_fields(*fields)
79
+ @signed_attributes_context.push(*fields)
80
+ options[:digest] << @template if options[:digest]
81
81
  end
82
82
 
83
- include Methods
83
+ private
84
+
85
+ def prepare_signed_attributes_hash
86
+ @signed_attributes[:_options_] = {}
87
+
88
+ if options[:sign_destination]
89
+ @signed_attributes[:_options_][:method] = options[:html][:method]
90
+ @signed_attributes[:_options_][:url] = options[:url]
91
+ end
92
+
93
+ if options[:digest]
94
+ @signed_attributes[:_options_][:digest] = options[:digest] = Digestor.new(@template)
95
+ @signed_attributes[:_options_][:digest_expiration] = Time.now + options[:digest_grace_period] if options[:digest_grace_period]
96
+ end
97
+ end
84
98
  end
85
99
  end
@@ -0,0 +1,50 @@
1
+ module SignedForm
2
+ class GateKeeper
3
+ attr_reader :allowed_attributes
4
+
5
+ def initialize(controller)
6
+ @controller = controller
7
+ @params = controller.params
8
+ @request = controller.request
9
+
10
+ extract_and_verify_form_signature
11
+ verify_destination
12
+ verify_digest
13
+ end
14
+
15
+ def options
16
+ @options ||= {}
17
+ end
18
+
19
+ def extract_and_verify_form_signature
20
+ data, signature = @params['form_signature'].split('--', 2)
21
+ hmac = SignedForm::HMAC.new secret_key: SignedForm.secret_key
22
+
23
+ signature ||= ''
24
+
25
+ raise Errors::InvalidSignature, "Form signature is not valid" unless hmac.verify signature, data
26
+
27
+ @allowed_attributes = Marshal.load Base64.strict_decode64(data)
28
+ @options = allowed_attributes.delete(:_options_)
29
+ end
30
+
31
+ def verify_destination
32
+ return unless options[:method] && options[:url]
33
+ raise Errors::InvalidURL if options[:method].to_s.casecmp(@request.request_method) != 0
34
+ url = @controller.url_for(options[:url])
35
+ raise Errors::InvalidURL if url != @request.fullpath && url != @request.url
36
+ end
37
+
38
+ def verify_digest
39
+ return unless options[:digest]
40
+
41
+ return if options[:digest_expiration] && Time.now < options[:digest_expiration]
42
+
43
+ digestor = options[:digest]
44
+ given_digest = digestor.to_s
45
+ digestor.view_paths = @controller.view_paths.map(&:to_s)
46
+ digestor.refresh
47
+ raise Errors::ExpiredForm unless given_digest == digestor.to_s
48
+ end
49
+ end
50
+ end
@@ -1,24 +1,22 @@
1
- require 'openssl'
1
+ require "openssl"
2
2
 
3
3
  module SignedForm
4
- module HMAC
5
- extend self
6
-
4
+ class HMAC
7
5
  attr_accessor :secret_key
8
6
 
9
- def create_hmac(data)
7
+ def initialize(options = {})
8
+ self.secret_key = options[:secret_key]
9
+
10
10
  if secret_key.nil? || secret_key.empty?
11
11
  raise Errors::NoSecretKey, "Please consult the README for instructions on creating a secret key"
12
12
  end
13
+ end
13
14
 
15
+ def create(data)
14
16
  OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA1.new, secret_key, data
15
17
  end
16
18
 
17
- def verify_hmac(signature, data)
18
- if secret_key.nil? || secret_key.empty?
19
- raise Errors::NoSecretKey, "Please consult the README for instructions on creating a secret key"
20
- end
21
-
19
+ def verify(signature, data)
22
20
  secure_compare OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret_key, data), signature
23
21
  end
24
22
 
@@ -0,0 +1,17 @@
1
+ module SignedForm
2
+ module TestHelper
3
+ def permit_all_parameters
4
+ @controller.instance_eval do
5
+ def params
6
+ @permit_all_parameters ||= super.permit!
7
+ end
8
+ end
9
+
10
+ if block_given?
11
+ yield
12
+ @controller.remove_instance_variable :@permit_all_parameters
13
+ @controller.singleton_class.send :remove_method, :params
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,7 +1,7 @@
1
1
  module SignedForm
2
2
  MAJOR = 0
3
- MINOR = 1
4
- PATCH = 2
3
+ MINOR = 2
4
+ PATCH = 0
5
5
  PRE = nil
6
6
 
7
7
  VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join '.'
data/signed_form.gemspec CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.add_development_dependency "rake"
23
23
  spec.add_development_dependency "rspec", "~> 2.13"
24
24
  spec.add_development_dependency "activemodel", ">= 3.1"
25
+ spec.add_development_dependency "coveralls"
25
26
 
26
27
  spec.add_dependency "actionpack", ">= 3.1"
27
28
 
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ describe SignedForm::Digestor do
4
+ let(:view_paths) { [File.join(File.dirname(__FILE__), 'fixtures', 'views')] }
5
+ let(:template) { double(view_paths: view_paths) }
6
+
7
+ before do
8
+ def template.virtual_path=(path)
9
+ @virtual_path = path
10
+ end
11
+
12
+ template.virtual_path = 'form'
13
+ end
14
+
15
+ it "should raise if template doesn't have view_paths" do
16
+ template = double(view_paths: -> { raise NoMethodError })
17
+ expect { SignedForm::Digestor.new(template) }.to raise_error(SignedForm::Errors::UnableToDigest)
18
+ end
19
+
20
+ it "should raise if the template doesn't have a virtual path" do
21
+ template = double(view_paths: view_paths)
22
+ expect { SignedForm::Digestor.new(template) }.to raise_error(SignedForm::Errors::UnableToDigest)
23
+
24
+ template.instance_variable_set(:@virtual_path, 'form')
25
+ digestor = SignedForm::Digestor.new(template)
26
+ template.instance_variable_set(:@virtual_path, nil)
27
+ expect { digestor << template }.to raise_error(SignedForm::Errors::UnableToDigest)
28
+ end
29
+
30
+ it "should not marshal view paths" do
31
+ digestor = SignedForm::Digestor.new(template)
32
+
33
+ digestor.view_paths.should_not be_empty
34
+
35
+ data = Marshal.dump digestor
36
+ digestor = Marshal.load data
37
+
38
+ digestor.view_paths.should be_empty
39
+ end
40
+
41
+ specify "#to_s should return the correct MD5 digest" do
42
+ digestor = SignedForm::Digestor.new(template)
43
+ digestor.to_s.should == "7a956713f33cabd57357c70025109e69"
44
+ template.virtual_path = "_fields"
45
+ digestor << template
46
+ digestor.to_s.should == "6a161ab9978322e8251d809b3558ab1a"
47
+ end
48
+
49
+ specify "The view order should not affect the digest" do
50
+ digestor = SignedForm::Digestor.new(template)
51
+ template.virtual_path = '_fields'
52
+ digestor << template
53
+
54
+ digestor2 = SignedForm::Digestor.new(template)
55
+ template.virtual_path = 'form'
56
+ digestor2 << template
57
+ digestor.to_s.should == digestor2.to_s
58
+ end
59
+
60
+ it "should marshal and taint the digest" do
61
+ digestor = SignedForm::Digestor.new(template)
62
+ data = Marshal.dump digestor
63
+ digestor = Marshal.load data
64
+ digestor.to_s.should be_tainted
65
+ end
66
+
67
+ it "should reset the digest if a template is added" do
68
+ digestor = SignedForm::Digestor.new(template)
69
+ first_digest = digestor.to_s
70
+ template.virtual_path = '_fields'
71
+ digestor << template
72
+ digestor.to_s.should_not == first_digest
73
+ end
74
+
75
+ it "should be idempotent" do
76
+ digestor = SignedForm::Digestor.new(template)
77
+ first_digest = digestor.to_s
78
+ digestor << template
79
+ digestor.to_s.should == first_digest
80
+ end
81
+ end