wix-apps 0.0.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a314236e1cdbd6379c6bf7b4a9731508f1f29751
4
+ data.tar.gz: a8e6f86a69f82c7c36deef8aa4efb8d2a923d4d8
5
+ SHA512:
6
+ metadata.gz: 222b61a2e958fecd8e2e0eb685a4a17e15601b476c067311f436251292aa876ba5f134eab6509f1ef015e2b45bb3008f5612d18419a5bebfe1f8c18ec4a7ed88
7
+ data.tar.gz: 585213545bda5d1786852813c2a55d8c9758effbcb7f91210d84c9ee413eba214ca1be50d0ea277a2b3c667cb3bcac35d395c5c6e9635a73d213ee38ae81d80a
data/.rspec CHANGED
@@ -1,2 +1,2 @@
1
1
  --color
2
- --format documentation
2
+ --format documentation
@@ -0,0 +1 @@
1
+ wix-apps
@@ -0,0 +1 @@
1
+ 2.0.0
@@ -1,12 +1,16 @@
1
+ before_script:
2
+ - uname -a
3
+ before_install:
4
+ - gem install bundler
1
5
  language: ruby
2
6
  rvm:
3
- - 1.9.3
4
- - 1.9.2
5
- - jruby-19mode
6
- - rbx-19mode
7
+ - 2.0
8
+ - 2.1
9
+ - 2.2
10
+ - 2.3
7
11
  - ruby-head
8
12
  - jruby-head
9
- bundler_args: --without development
10
13
  notifications:
11
14
  email:
12
- - gregory@wix.com
15
+ - gregory@wix.com
16
+ - niklas@bichinger.de
data/Gemfile CHANGED
@@ -1,11 +1,9 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
- gemspec :development_group => :gem_dev
5
4
 
6
5
  group :development do
7
6
  gem 'ruby-debug', :platforms => :mri_18
8
7
  gem 'debugger', :platforms => :mri_19
9
8
  gem 'guard-rspec'
10
9
  end
11
-
data/Guardfile CHANGED
@@ -1,5 +1,5 @@
1
1
  guard 'rspec' do
2
2
  watch(%r{^spec/.+_spec\.rb$})
3
3
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
- watch('spec/spec_helper.rb') { "spec" }
4
+ watch('spec/spec_helper.rb') { 'spec' }
5
5
  end
data/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # Wix::Apps
2
2
  [![Build Status](https://secure.travis-ci.org/wix/wix-apps-ruby.png?branch=master)](http://travis-ci.org/wix/wix-apps-ruby)
3
3
 
4
- Rack middleware use with "Third Party Applications".
5
- It checks signature and passes the parsed_instance param to you application
4
+ Rack middleware for use with "Third Party Applications" on the Wix platform.
5
+ It handles the instance param passed by Wix requests for configurable paths. It checks the signature, parses the contents and passes a convenient Object to your application via env.
6
6
 
7
7
  ## Installation
8
8
 
9
+ Important note: if you're using a pre-1.0.0-version of this gem, please be aware the API changed significantly. You will have to update some of your code. However, in versions >= 1.0.0 should be a corresponding method for anything from version 0.0.x.
10
+
9
11
  Add this line to your application's Gemfile:
10
12
 
11
- gem 'wix-apps'
13
+ gem 'wix-apps', '~> 1.0.0'
12
14
 
13
15
  And then execute:
14
16
 
@@ -21,17 +23,71 @@ Or install it yourself as:
21
23
  ## Usage
22
24
 
23
25
  ### Any Rack Application
24
- Add Wix::Apps::SignedInstanceMiddleware as any other middleware.
26
+ Add `Wix::Apps::SignedInstanceMiddleware` like any other middleware.
25
27
  ```ruby
26
- use Wix::Apps::SignedInstanceMiddleware, secured_paths: ['/yours', '/paths'], secret_key: 'secret_key'
28
+ use Wix::Apps::SignedInstanceMiddleware, secret_key: 'secret_key',
29
+ secured_paths: ['/your', '/paths', %r{\A/wix/(auth|update)\z}]
27
30
  ```
28
31
  ### Rails
29
- In application.rb, add:
32
+ In `application.rb`, add:
33
+ ```ruby
34
+ config.middleware.use Wix::Apps::SignedInstanceMiddleware, secret_key: 'your-secret-key',
35
+ secured_paths: ['/wix']
36
+ ```
37
+
38
+ In your controller handling a configured secured path, access the signed instance:
30
39
  ```ruby
31
- config.middleware.use Wix::Apps::SignedInstanceMiddleware,
32
- secured_paths: ['/yours', '/paths'], secret_key: 'your-secret-key'
40
+ class WixController < ApplicationController
41
+ def index
42
+ # retrieve Wix::Apps::SignedInstance object
43
+ instance = request.env['wix.instance']
44
+
45
+ if instance.owner_permissions?
46
+ ...
33
47
  ```
34
48
 
49
+ ### env['wix.instance']
50
+ This WixApps middleware passes on a `Wix::Apps::SignedInstance` object through env['wix.instance']. The object has the following methods:
51
+
52
+ Object method | Type | Description
53
+ ------------- | ---- | -----------
54
+ instance_id | String | Required Wix instance property `instanceId`
55
+ sign_date | DateTime | Required Wix instance property `signDate`
56
+ uid | String | Optional Wix instance property `uid`
57
+ permissions | String | Required Wix instance property `permissions`
58
+ ip_and_port | String | Required Wix instance property `ipAndPort`
59
+ vendor_product_id | String | Required Wix instance property `vendorProductId`
60
+ aid | String | Required Wix instance property `aid`
61
+ origin_instance_id | String | Optional Wix instance property `originInstanceId`
62
+ site_owner_id | String | Required Wix instance property `siteOwnerId`
63
+ owner_permissions? | Boolean | Indicates if instance user has owner (includes site administrators) permissions
64
+ owner_logged_in? | Boolean | Indicates if instance user is the single owner of the site
65
+
66
+ ### Options
67
+
68
+ #### secured_paths
69
+ An Array of String- and Regexp-objects describing the paths to be secured by checking the Wix signed instance. On request, the path will be checked against every entry in this list. Strings are checked for equality, Regexps are matched (without additional conditions, so use \A and \z for start and end markers if you need them!). Keep in mind rack's paths always begin with a '/'.
70
+
71
+ Example (secures '/auth_path', every path containing the string 'wix' and every path starting with '/wox/'):
72
+
73
+ secured_paths: ['/auth_path', /wix/, %r{\A/wox/}]
74
+
75
+ #### paths
76
+ Just like secured_paths, except these paths may or may not receive a Wix signed instance. If it does, the instance has to be valid. Calling `request.env('wix.instance')` in the controller may return nil (although the env key `'wix.instance'` exists) if there was no instance passed.
77
+
78
+ #### secret_key
79
+ A String containing your Wix app's secret key.
80
+
81
+ Example:
82
+
83
+ secret_key: 'd245bbf8-57eb-49d6-aeff-beff6d82cd39'
84
+
85
+ #### (optional) strict_properties
86
+ A Boolean indicating if the instance properties of the Wix signed instance should be checked for completeness (true, default) or not (false). You should never need to set it to false and it's meant for debugging purposes only.
87
+
88
+ Example:
89
+
90
+ strict_properties: true
35
91
 
36
92
  ## Contributing
37
93
 
data/Rakefile CHANGED
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env rake
2
- require "bundler/gem_tasks"
2
+ require 'bundler/gem_tasks'
3
3
  require 'rspec/core/rake_task'
4
4
 
5
5
  desc 'Default: run specs.'
6
6
  task :default => :spec
7
7
 
8
- desc "Run specs"
9
- RSpec::Core::RakeTask.new
8
+ desc 'Run specs'
9
+ RSpec::Core::RakeTask.new
10
+
11
+ task :console do
12
+ exec 'irb -r wix-apps.rb -I ./lib'
13
+ end
@@ -1,9 +1,8 @@
1
- require "wix-apps/version"
1
+ require 'wix-apps/version'
2
2
  require 'wix-apps/signed_instance'
3
3
  require 'wix-apps/signed_instance_middleware'
4
4
 
5
5
  module Wix
6
6
  module Apps
7
-
8
7
  end
9
8
  end
@@ -4,62 +4,113 @@ require 'openssl'
4
4
 
5
5
  module Wix
6
6
  module Apps
7
- class SignedInstanceParseError < Exception;end
8
- class SignedInstanceNoSecretKey < Exception;end
9
- # This class deal with Wix Signed Instance
10
- # (http://dev.wix.com/display/wixdevelopersapi/The+Signed+Instance)
7
+
8
+ class SignedInstanceParseError < Exception
9
+ end
10
+
11
+ class SignedInstanceNoSecretKey < Exception
12
+ end
13
+
14
+ # This class deals with Wix Signed Instance
15
+ # (http://dev.wix.com/docs/display/DRAF/Using+the+Signed+App+Instance)
11
16
  #
12
17
  # Example:
13
18
  # si = SignedInstance.new('vrinSv2HB9tqbnJ....')
14
19
  class SignedInstance
15
- attr_reader :raw_signed_instance, :instance_id, :sign_date, :uid,
16
- :permissions
17
20
 
18
- def initialize(raw_signed_instance, options = {})
19
- @raw_signed_instance = raw_signed_instance
20
- @secret = options[:secret]
21
+ # maps required instance properties to object attributes
22
+ REQUIRED_PROPERTIES = {
23
+ 'instanceId' => :instance_id,
24
+ 'signDate' => :sign_date,
25
+ 'permissions' => :permissions,
26
+ 'ipAndPort' => :ip_and_port,
27
+ 'vendorProductId' => :vendor_product_id,
28
+ 'aid' => :aid,
29
+ 'siteOwnerId' => :site_owner_id,
30
+ }
21
31
 
22
- parse_signed_instance_data
23
- end
32
+ # maps optional instance properties to object attributes
33
+ OPTIONAL_PROPERTIES = {
34
+ 'uid' => :uid,
35
+ 'originInstanceId' => :origin_instance_id,
36
+ }
24
37
 
25
- # validates signature
26
- def valid?
27
- raise SignedInstanceNoSecretKey.new('Please provide secret key') if @secret.nil?
28
- digest = OpenSSL::Digest::Digest.new('sha256')
29
- hmac_digest = OpenSSL::HMAC.digest(digest, @secret, @encoded_json)
30
- my_signature = Base64.urlsafe_encode64(hmac_digest).gsub('=','')
38
+ PERMISSIONS_OWNER = 'OWNER'
39
+
40
+ attr_reader :raw_signed_instance, :strict_properties
41
+ attr_reader *(REQUIRED_PROPERTIES.values + OPTIONAL_PROPERTIES.values)
42
+
43
+ # @param [String] raw_signed_instance The "instance" parameter Wix sends with the request
44
+ # @param [Hash] options Options for
45
+ def initialize(raw_signed_instance, options={})
46
+ self.strict_properties = options[:strict_properties].nil? ? true : !!options[:strict_properties]
47
+ self.secret_key = options[:secret_key] || options[:secret] # :secret for backwards compatibility
48
+ raise SignedInstanceNoSecretKey.new('secret key must be provided') if secret_key.nil?
49
+ self.raw_signed_instance = raw_signed_instance
50
+ raise SignedInstanceParseError.new('invalid instance signature') unless instance_signature_valid?
31
51
 
32
- return my_signature == @signature
52
+ initialize_from_signed_instance
33
53
  end
34
54
 
35
- #Owner mode on?
36
- def owner?
37
- permissions == 'OWNER'
55
+ # owner or site collaborator visiting?
56
+ def owner_permissions?
57
+ permissions == PERMISSIONS_OWNER
58
+ end
59
+
60
+ # did the one single site owner log in?
61
+ def owner_logged_in?
62
+ # note: site owner id is required so we wouldn't have to check for nil,
63
+ # but this method's output can be very important and I'm paranoid. ;)
64
+ !site_owner_id.nil? && site_owner_id == uid
38
65
  end
39
66
 
40
67
  private
41
- def parse_signed_instance_data
42
- @signature, @encoded_json = raw_signed_instance.split('.', 2)
43
- raise SignedInstanceParseError if @signature.nil? || @encoded_json.nil?
68
+
69
+ attr_accessor :secret_key
70
+ attr_writer :raw_signed_instance, :strict_properties
71
+ attr_writer *(REQUIRED_PROPERTIES.values + OPTIONAL_PROPERTIES.values)
72
+
73
+ # validates signature
74
+ def instance_signature_valid?
75
+ signature, encoded_json = (raw_signed_instance || '').split('.', 2)
76
+ return false if signature.nil? || encoded_json.nil?
77
+
78
+ digest = OpenSSL::Digest.new('sha256')
79
+ hmac_digest = OpenSSL::HMAC.digest(digest, secret_key, encoded_json)
80
+ my_signature = Base64.urlsafe_encode64(hmac_digest).gsub('=', '')
81
+
82
+ my_signature == signature
83
+ end
84
+
85
+ # initializes object attributes from parsed instance
86
+ def initialize_from_signed_instance
87
+ encoded_json = raw_signed_instance.split('.', 2).last
44
88
 
45
89
  # Need to add Base64 padding.
46
90
  # (http://stackoverflow.com/questions/4987772/decoding-facebooks-signed-request-in-ruby-sinatra)
47
- padded_json = @encoded_json
48
- padded_json += ('=' * (4 - @encoded_json.length % 4)) if padded_json.length % 4 != 0
91
+ padded_json = encoded_json
92
+ padded_json += ('=' * (4 - encoded_json.length % 4)) if padded_json.length % 4 != 0
49
93
 
50
94
  begin
51
- @json = Base64.urlsafe_decode64(padded_json)
52
- signed_instance = MultiJson.load(@json)
53
- rescue ArgumentError, MultiJson::DecodeError => e
95
+ json = Base64.urlsafe_decode64(padded_json)
96
+ signed_instance = MultiJson.load(json)
97
+ rescue ArgumentError, MultiJson::ParseError => e
54
98
  raise SignedInstanceParseError.new(e.message)
55
99
  end
56
100
 
57
- @instance_id = signed_instance['instanceId']
58
- @sign_date = DateTime.parse(signed_instance['signDate'])
59
- raise SignedInstanceParseError if @instance_id.nil? || @sign_date.nil?
101
+ # set all required instance properties
102
+ REQUIRED_PROPERTIES.each { |instance_key, attribute|
103
+ instance_value = signed_instance[instance_key]
104
+ raise SignedInstanceParseError.new("missing instance property: #{instance_key}") if strict_properties && instance_value.nil?
105
+ send "#{attribute}=", instance_value
106
+ }
107
+ # overwrite sign date with real DateTime object
108
+ self.sign_date = DateTime.parse(sign_date) if strict_properties || sign_date
60
109
 
61
- @uid = signed_instance['uid']
62
- @permissions = signed_instance['permissions']
110
+ # set all optional instance properties (if set)
111
+ OPTIONAL_PROPERTIES.each { |instance_key, attribute|
112
+ send "#{attribute}=", signed_instance[instance_key] if signed_instance.has_key? instance_key
113
+ }
63
114
  end
64
115
  end
65
116
  end
@@ -1,65 +1,71 @@
1
1
  module Wix
2
2
  module Apps
3
- class SignedInstanceMiddleware < Struct.new :app, :options
3
+ class SignedInstanceMiddleware < Struct.new :app, :secret_key, :secured_paths, :paths
4
+
5
+ # Initializes the middleware to secure a given set of paths.
6
+ # Options:
7
+ # :secret_key - the Wix secret key as String
8
+ # :secured_paths (optional) - an Array of String and Regexp objects which every request's path is matched against. Matching paths are required to pass a Wix signed instance.
4
9
  def initialize(app, options={})
5
- @app = app
6
- initialize_options options
10
+ self.app = app
11
+ self.secret_key = options[:secret_key]
12
+ self.secured_paths = options[:secured_paths] || []
13
+ self.paths = options[:paths] || []
7
14
  end
8
15
 
16
+ # Checks current URL path for Wix instance requirement, parses given signed instance and adds GET param 'parsed_instance' with the instance's parsed properties.
17
+ # @param [Hash] env The current environment hash
18
+ # @return [Array] The typical [<code>, <headers>, <body>] rack response Array
9
19
  def call(env)
10
- @env = env
20
+ path = env['PATH_INFO']
11
21
 
12
- if secured_path?
13
- @request = Rack::Request.new(env)
14
- if have_instance?
15
- begin
16
- @instance = Wix::Apps::SignedInstance.new(@request.params['instance'],
17
- secret: options[:secret_key])
18
- rescue Wix::Apps::SignedInstanceParseError => e
19
- return [403, {}, ['Invalid wix instance']]
20
- end
22
+ secured_path = secured_path? path
23
+ if secured_path || path?(path)
24
+ # path must be handled (instance is either required or optional)
25
+ request = Rack::Request.new(env)
21
26
 
22
- if @instance.valid?
23
- parse_instance!
24
- @app.call(env)
25
- else
26
- [403, {}, ['Invalid wix instance']]
27
+ env['wix.instance'] = nil
28
+ if request.params.has_key? 'instance'
29
+ # Wix' "instance" parameter was supplied so it must be parseable. parse and set it into env.
30
+ begin
31
+ env['wix.instance'] = Wix::Apps::SignedInstance.new(request.params['instance'], secret: secret_key)
32
+ rescue Wix::Apps::SignedInstanceParseError
33
+ # 403 Forbidden
34
+ return [403, {}, ['Invalid Wix instance']]
27
35
  end
28
- else
29
- [401, {}, ['Unauthorized']]
36
+ elsif secured_path
37
+ # instance is required but Wix' "instance" parameter is missing
38
+ # 401 Unauthorized
39
+ return [401, {}, ['Unauthorized']]
30
40
  end
31
- else
32
- @app.call(env)
33
41
  end
34
-
42
+ app.call(env)
35
43
  end
36
44
 
37
45
  private
38
- def initialize_options(options={})
39
- self.options = {
40
- :secret_key => nil,
41
- :secured_paths => []
42
- }.merge(options)
43
- end
44
46
 
45
- def secured_path?
46
- options[:secured_paths].include? @env['PATH_INFO']
47
+ # Check if a request URL's path should be required to pass a Wix signed instance or not. Checks the path against the secured_paths option.
48
+ # @param [String] path URL path (=directory part) to check
49
+ # @return [Boolean] Indicates if given path should be secured or not
50
+ def secured_path?(path)
51
+ path_match? secured_paths, path
47
52
  end
48
53
 
49
- def have_instance?
50
- @request.params.keys.include? 'instance'
54
+ # Check if a request URL's path should be checked for a Wix signed instance or not. Checks the path against the paths option.
55
+ # @param [String] path URL path (=directory part) to check
56
+ # @return [Boolean] Indicates if given path should be checked or not
57
+ def path?(path)
58
+ path_match? paths, path
51
59
  end
52
60
 
53
- def parse_instance!
54
- parsed_instance = {
55
- 'instance_id' => @instance.instance_id,
56
- 'sign_date' => @instance.sign_date,
57
- 'user_id' => @instance.uid,
58
- 'permissions' => @instance.permissions
61
+ # @param [Array<String,Regexp>] match_paths List of Strings (match exact path) amd Regexps (match whatever the regexp says)
62
+ # @param [String] path The path to match against
63
+ def path_match?(match_paths, path)
64
+ match_paths.any? { |match_path|
65
+ match_path === path
59
66
  }
60
- @request.GET['parsed_instance'] = parsed_instance
61
-
62
67
  end
68
+
63
69
  end
64
70
  end
65
- end
71
+ end