coalescing_panda 5.0.8 → 5.1.3

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
- SHA256:
3
- metadata.gz: b00520a9558a0dbd2699d97b5f1163721dff23d0042be51b2df5cc220675a0a1
4
- data.tar.gz: 7adaf679f268ebcd2e5695994d3b5b498eda1ee3078e3be617b5a47e24a04244
2
+ SHA1:
3
+ metadata.gz: d0d95162b1397f3f63cefeea2307d14bdb446771
4
+ data.tar.gz: 3c81934197e230702d93700cb4c4706cbcbf78fa
5
5
  SHA512:
6
- metadata.gz: 630ea590d568f2fc1839ed5b456eec95a8ab0fa43b8139a7c9c32d5c08eddbb557fbe6cd71b65a88b71e5d72ec61098252033772f18e863368f4475bc807ac96
7
- data.tar.gz: 18622f8b4c058ca1d8d66626d7b5186083114dba571f5d0a2b7c3d12b75abea934a6b02b2e29612541674ac704c018da58c126bbf3ea58dcd06d66f7994c4878
6
+ metadata.gz: c9321fcd178dd6df1990e91b17676ca9e6d2762e8fe0aa6b27e00b26a7951470518eb1e4f847a9e4b73757ee264968a8a5f2ed55f2f7f6d33f8919269e0e4294
7
+ data.tar.gz: 0f96f9df3de844abc278f249ef0ec239537109762815b91b4c0dcdda72481116580b42e7b142b8e790959acb2e7b6add2642411071b1d29353ec8994a40658db
@@ -34,7 +34,7 @@ module CoalescingPanda
34
34
  private
35
35
 
36
36
  def oauth2_protocol
37
- ENV['OAUTH_PROTOCOL'] || 'https'
37
+ ENV['OAUTH_PROTOCOL'] || (Rails.env.development? ? 'http' : 'https')
38
38
  end
39
39
 
40
40
  def retrieve_oauth_state
@@ -1,51 +1,17 @@
1
1
  require 'browser'
2
+ require_relative 'session_replacement'
2
3
 
3
4
  module CoalescingPanda
4
5
  module ControllerHelpers
5
6
  extend ActiveSupport::Concern
6
-
7
- included do
8
- alias_method :rails_session, :session
9
-
10
- helper_method :encrypted_session_key, :current_session_data, :current_session
11
- append_after_action :save_session, if: -> { @current_session && session_changed? }
12
- end
13
-
14
- class_methods do
15
- def use_native_sessions
16
- after_action do
17
- rails_session['persistent_session_key'] = current_session.session_key if @current_session.present?
18
- end
19
- end
20
- end
21
-
22
- def current_session
23
- @current_session ||= (CoalescingPanda::PersistentSession.find_by(session_key: session_key) if session_key)
24
- @current_session ||= (CoalescingPanda::PersistentSession.create_from_launch(params, current_lti_account.id) if current_lti_account.present?)
25
- @current_session
26
- end
7
+ include SessionReplacement
27
8
 
28
9
  def current_lti_account
29
10
  @account ||= (CoalescingPanda::LtiAccount.find_by!(key: organization_key) if organization_key)
30
11
  @account ||= (CoalescingPanda::LtiAccount.find_by(id: organization_id) if organization_id)
31
12
  @account
32
13
  end
33
-
34
- def current_session_data
35
- current_session.data
36
- end
37
-
38
- def encrypted_session_key
39
- msg_encryptor.encrypt_and_sign(current_session.session_key)
40
- end
41
-
42
- def save_session
43
- current_session.try(:save)
44
- end
45
-
46
- def session_changed?
47
- current_session.changed? && current_session.changes[:data].present?
48
- end
14
+ def current_organization; current_lti_account; end
49
15
 
50
16
  def canvas_oauth2(*roles)
51
17
  return if have_session?
@@ -81,9 +47,7 @@ module CoalescingPanda
81
47
  client = Bearcat::Client.new(prefix: uri.prefix)
82
48
  state = SecureRandom.hex(32)
83
49
  OauthState.create! state_key: state, data: { key: params['oauth_consumer_key'], user_id: user_id, api_domain: uri.api_domain }
84
- redirect_path = coalescing_panda.oauth2_redirect_path
85
- redirect_url = [coalescing_panda_url, redirect_path.sub(/^\/lti/, '')].join
86
- @canvas_url = client.auth_redirect_url(client_id, redirect_url, { state: state })
50
+ @canvas_url = client.auth_redirect_url(client_id, resolve_coalescing_panda_url(:oauth2_redirect_url), { state: state })
87
51
 
88
52
  #delete the added params so the original oauth sig still works
89
53
  @lti_params = params.to_hash
@@ -94,7 +58,7 @@ module CoalescingPanda
94
58
 
95
59
  def refresh_token(uri, api_auth)
96
60
  refresh_client = Bearcat::Client.new(prefix: uri.prefix)
97
- refresh_body = refresh_client.retrieve_token(@lti_account.oauth2_client_id, coalescing_panda.oauth2_redirect_url,
61
+ refresh_body = refresh_client.retrieve_token(@lti_account.oauth2_client_id, resolve_coalescing_panda_url(:oauth2_redirect_url),
98
62
  @lti_account.oauth2_client_key, api_auth.refresh_token, 'refresh_token')
99
63
  api_auth.update({ api_token: refresh_body['access_token'], expires_at: (Time.now + refresh_body['expires_in']) })
100
64
  end
@@ -193,14 +157,21 @@ module CoalescingPanda
193
157
  end
194
158
  end
195
159
 
196
- def session_check
197
- logger.warn 'session_check is deprecated. Functionality moved to lti_authorize.'
160
+ def valid_session?
161
+ return false unless current_session(create_missing: false)&.persisted?
162
+ true
163
+ rescue SessionNonceMismatch
164
+ false
198
165
  end
199
166
 
200
167
  private
201
168
 
202
- def msg_encryptor
203
- @crypt ||= ActiveSupport::MessageEncryptor.new(Rails.application.secrets.secret_key_base[0..31])
169
+ def find_or_create_session(key:)
170
+ if key == :create
171
+ CoalescingPanda::PersistentSession.create_from_launch(params, current_lti_account.id) if current_lti_account.present?
172
+ else
173
+ CoalescingPanda::PersistentSession.find_by(session_key: key)
174
+ end
204
175
  end
205
176
 
206
177
  def organization_key
@@ -211,51 +182,16 @@ module CoalescingPanda
211
182
  params[:organization_id] || (current_session_data[:launch_params][:organization_id] if @current_session)
212
183
  end
213
184
 
214
- def session_key
215
- if params[:encrypted_session_key]
216
- return msg_encryptor.decrypt_and_verify(params[:encrypted_session_key])
217
- end
218
- params[:session_key] || session_key_header || rails_session['persistent_session_key']
185
+ # This is necessitated by a bug in Rails Engines where it isn't resolving the URL correctly
186
+ # when using coalescing_panda.xyz_url (The Engine Prefix is not included)
187
+ # I believe https://github.com/rails/rails/issues/34452 is the same issue
188
+ def resolve_coalescing_panda_url(key)
189
+ key = key.to_s[0...-4] if key.to_s.ends_with?('_url')
190
+ resolved_path = coalescing_panda.send(:"#{key}_path")
191
+ cpurl = coalescing_panda_url
192
+ cppath = URI.parse(cpurl).path
193
+ resolved_path = cppath + resolved_path unless resolved_path.starts_with?(cppath)
194
+ URI.join(cpurl, resolved_path)
219
195
  end
220
-
221
- def session_key_header
222
- if (match = request.headers['Authorization'].try(:match, /crypted_token=(.+)/))
223
- msg_encryptor.decrypt_and_verify(match[1])
224
- elsif (match = request.headers['Authorization'].try(:match, /token=(.+)/))
225
- match[1]
226
- end
227
- end
228
-
229
- # Redirect with the session key intact. In production,
230
- # handle this by encrypting the session key. That way if the
231
- # url is logged anywhere, it will all be encrypted data. In dev,
232
- # just put it in the URL. Putting it in the URL
233
- # is insecure, but is fine in development.
234
- # Keeping it in the URL in development means that it plays
235
- # nicely with webpack-dev-server live reloading (otherwise
236
- # you get an access error every time it tries to live reload).
237
-
238
- def redirect_with_session_to(path, id_or_resource = nil, redirect_params = {})
239
- if Rails.env.development? || Rails.env.test?
240
- redirect_development_mode(path, id_or_resource, redirect_params)
241
- else
242
- redirect_production_mode(path, id_or_resource, redirect_params)
243
- end
244
- end
245
-
246
- def redirect_development_mode(path, id_or_resource = nil, redirect_params)
247
- redirect_to send(path, id_or_resource, {
248
- session_key: current_session.session_key,
249
- organization_id: current_lti_account.id
250
- }.merge(redirect_params))
251
- end
252
-
253
- def redirect_production_mode(path, id_or_resource = nil, redirect_params)
254
- redirect_to send(path, id_or_resource, {
255
- encrypted_session_key: encrypted_session_key,
256
- organization_id: current_lti_account.id
257
- }.merge(redirect_params))
258
- end
259
-
260
196
  end
261
197
  end
@@ -4,9 +4,9 @@ module CoalescingPanda
4
4
 
5
5
  def self.to_boolean(v)
6
6
  if Rails.version < '5.0'
7
- ActiveRecord::Type::Boolean.new.type_cast_from_user("0")
7
+ ActiveRecord::Type::Boolean.new.type_cast_from_user(v)
8
8
  else
9
- ActiveRecord::Type::Boolean.new.deserialize('0')
9
+ ActiveRecord::Type::Boolean.new.deserialize(v)
10
10
  end
11
11
  end
12
12
  end
@@ -48,11 +48,6 @@ module CoalescingPanda
48
48
  end
49
49
  end
50
50
 
51
- if CoalescingPanda.lti_options.has_key?(:allow_unsafe_eval) && CoalescingPanda.lti_options[:allow_unsafe_eval] == true
52
- # For when code is returned from server and injected into dom. Need to have unsafe-eval or it won't work.
53
- csp_entry(:script_src, "'unsafe-eval'")
54
- end
55
-
56
51
  # Detect and permit Sentry
57
52
  if defined?(Raven) && Raven.configuration.server.present?
58
53
  csp_entry(:connect_src, Raven.configuration.server)
@@ -0,0 +1,189 @@
1
+ module CoalescingPanda
2
+ class SessionNonceMismatch < StandardError; end
3
+
4
+ module SessionReplacement
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ helper_method :link_nonce, :current_session, :current_session_data
9
+ helper_method :link_with_session_to, :url_with_session, :session_url_for
10
+
11
+ prepend_around_action :monkeypatch_flash
12
+ prepend_around_action :auto_save_session
13
+ end
14
+
15
+ class_methods do
16
+ def link_nonce_type(value = :not_given)
17
+ if value == :not_given
18
+ @link_nonce_type || superclass.try(:link_nonce_type) || :nonce
19
+ else
20
+ @link_nonce_type = value
21
+ end
22
+ end
23
+ end
24
+
25
+ def save_session
26
+ current_session.try(:save)
27
+ end
28
+
29
+ def current_session(create_missing: true)
30
+ return @current_session if @current_session.present?
31
+
32
+ if params[:session_token]
33
+ payload = JSON.parse(session_cryptor.decrypt_and_verify(params[:session_token])).with_indifferent_access
34
+ matched_session = find_or_create_session(key: payload[:session_key])
35
+ session_expiration_period_minutes = superclass.try(:session_expiration_period_minutes) || 15
36
+ if matched_session.present?
37
+ if payload[:token_type] == 'nonce' && matched_session.data[:link_nonce] == payload[:nonce]
38
+ @current_session = matched_session
39
+ @current_session.data[:link_nonce] = nil
40
+ elsif payload[:token_type] == 'fixed_ip' && matched_session.data[:remote_ip] == request.remote_ip &&
41
+ DateTime.parse(matched_session.data[:last_ip_token_requested]) > session_expiration_period_minutes.minutes.ago
42
+ @current_session = matched_session
43
+ elsif payload[:token_type] == 'expiring' && DateTime.parse(matched_session.data[:last_token_requested]) > session_expiration_period_minutes.minutes.ago
44
+ @current_session = matched_session
45
+ end
46
+ end
47
+ raise SessionNonceMismatch, "Session Not Found" unless @current_session.present?
48
+ elsif (session_key = params[:session_key] || session_key_header || flash[:session_key] || session[:session_key]).present?
49
+ @current_session = find_or_create_session(key: session_key)
50
+ end
51
+
52
+ @current_session ||= find_or_create_session(key: :create) if create_missing
53
+
54
+ @current_session
55
+ end
56
+
57
+ def current_session_data
58
+ current_session.data
59
+ end
60
+
61
+ def session_changed?
62
+ current_session.changed? && current_session.changes[:data].present?
63
+ end
64
+
65
+ def forbid_access_if_lacking_session
66
+ render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session?
67
+ end
68
+
69
+ def verify_authenticity_token
70
+ # No need to check CSRF when no cookies were sent. This fixes CSRF failures in Browsers
71
+ # that restrict Cookie setting within an IFrame.
72
+ return unless request.cookies.keys.length > 0
73
+ super
74
+ end
75
+
76
+ # Redirect with the session key intact. In production,
77
+ # handle this by adding a one-time use encrypted token to the URL.
78
+ # Keeping it in the URL in development means that it plays
79
+ # nicely with webpack-dev-server live reloading (otherwise
80
+ # you get an access error everytime it tries to live reload).
81
+
82
+ def redirect_with_session_to(*args)
83
+ redirect_to url_with_session(*args)
84
+ end
85
+
86
+ def link_with_session_to(*args)
87
+ helpers.link_to url_with_session(*args)
88
+ end
89
+
90
+ def session_url_for(*args)
91
+ url_for(build_session_url_params(*args))
92
+ end
93
+
94
+ def url_with_session(location, *args, route_context: self, **kwargs)
95
+ route_context.send(location, *build_session_url_params(*args, **kwargs))
96
+ end
97
+
98
+ def link_nonce(type: link_nonce_type)
99
+ type = instance_exec(&type) if type.is_a?(Proc)
100
+ type = type.to_s
101
+
102
+ @cached_link_nonces ||= {}
103
+ @cached_link_nonces[type] ||= begin
104
+ payload = {
105
+ token_type: type,
106
+ session_key: current_session.session_key,
107
+ organization_id: current_organization.id,
108
+ }
109
+
110
+ if type == 'nonce'
111
+ current_session_data[:link_nonce] = SecureRandom.hex
112
+ payload.merge!(nonce: current_session_data[:link_nonce])
113
+ elsif type == 'fixed_ip'
114
+ current_session_data[:remote_ip] ||= request.remote_ip
115
+ current_session_data[:last_ip_token_requested] = DateTime.now.iso8601
116
+ elsif type == 'expiring'
117
+ current_session_data[:last_token_requested] = DateTime.now.iso8601
118
+ else
119
+ raise StandardError, "Unsupported link_nonce_type: '#{type}'"
120
+ end
121
+
122
+ session_cryptor.encrypt_and_sign(payload.to_json)
123
+ end
124
+ end
125
+
126
+ def link_nonce_type
127
+ self.class.link_nonce_type
128
+ end
129
+
130
+ private
131
+
132
+ def session_cryptor
133
+ secret_key_base = Rails.application.try(:secret_key_base) || Rails.application.secrets.secret_key_base
134
+ @session_cryptor ||= ActiveSupport::MessageEncryptor.new(secret_key_base[0..31])
135
+ end
136
+
137
+ def session_key_header
138
+ if match = request.headers['Authorization'].try(:match, /token=(.+)/)
139
+ match[1]
140
+ end
141
+ end
142
+
143
+ def build_session_url_params(*args, nonce_type: link_nonce_type, **kwargs)
144
+ if args[-1].is_a?(Hash)
145
+ args[-1] = args[-1].dup
146
+ else
147
+ args.push({})
148
+ end
149
+
150
+ if Rails.env.development?
151
+ args[-1].merge!(
152
+ session_key: current_session.session_key,
153
+ organization_id: current_organization.id,
154
+ )
155
+ else
156
+ args[-1].merge!(
157
+ session_token: link_nonce(type: nonce_type),
158
+ organization_id: current_organization.id,
159
+ )
160
+ end
161
+
162
+ args[-1].merge!(kwargs)
163
+ args
164
+ end
165
+
166
+ def auto_save_session
167
+ yield if block_given?
168
+ save_session if @current_session && session_changed?
169
+ end
170
+
171
+ def monkeypatch_flash
172
+ if valid_session? && (value = current_session_data['flashes']).present?
173
+ flashes = value["flashes"]
174
+ if discard = value["discard"]
175
+ flashes.except!(*discard)
176
+ end
177
+ flash.replace(flashes)
178
+ flash.discard()
179
+ end
180
+
181
+ yield
182
+
183
+ if @current_session.present?
184
+ current_session_data['flashes'] = flash.to_session_value
185
+ flash.discard()
186
+ end
187
+ end
188
+ end
189
+ end
@@ -1,3 +1,3 @@
1
1
  module CoalescingPanda
2
- VERSION = '5.0.8'
2
+ VERSION = '5.1.3'
3
3
  end
@@ -11,7 +11,7 @@ describe CoalescingPanda::Oauth2Controller, :type => :controller do
11
11
  Bearcat::Client.any_instance.stub(retrieve_token: { 'access_token' => 'token', 'refresh_token' => 'token', 'expires_in' => 3600 })
12
12
  session[:state] = 'test'
13
13
  CoalescingPanda::OauthState.create!(state_key: session[:state], data: { key: account.key, user_id: user.id, api_domain: 'foo.com' })
14
- get :redirect, {user_id: user.id, api_domain: 'foo.com', code: 'bar', key: account.key, state: 'test'}
14
+ get :redirect, params: {user_id: user.id, api_domain: 'foo.com', code: 'bar', key: account.key, state: 'test'}
15
15
  auth = CoalescingPanda::CanvasApiAuth.find_by_user_id_and_api_domain(user.id, 'foo.com')
16
16
  auth.should_not == nil
17
17
  expect(auth.api_token).to eql 'token'
@@ -20,7 +20,7 @@ describe CoalescingPanda::Oauth2Controller, :type => :controller do
20
20
  end
21
21
 
22
22
  it "doesn't create a token in the db" do
23
- get :redirect, {error: 'your face'}
23
+ get :redirect, params: {error: 'your face'}
24
24
  CoalescingPanda::CanvasApiAuth.all.count.should == 0
25
25
  end
26
26
  end
@@ -1,10 +1,10 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe CoalescingPanda::CanvasApiAuth do
3
+ describe CoalescingPanda::CanvasApiAuth, type: :model do
4
4
 
5
5
  it { should validate_uniqueness_of(:user_id).scoped_to(:api_domain)}
6
6
  it { should validate_presence_of(:user_id)}
7
- it {should validate_presence_of(:api_domain)}
7
+ it { should validate_presence_of(:api_domain)}
8
8
 
9
9
  describe '#expired?' do
10
10
  let(:auth) { FactoryGirl.create :canvas_api_auth }
@@ -24,6 +24,13 @@ SimpleCov.start
24
24
 
25
25
  ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)
26
26
 
27
+ Shoulda::Matchers.configure do |config|
28
+ config.integrate do |with|
29
+ with.test_framework :rspec
30
+ with.library :rails
31
+ end
32
+ end
33
+
27
34
  # This file was generated by the `rails generate rspec:install` command. Conventionally, all
28
35
  # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
29
36
  # The generated `.rspec` file contains `--require spec_helper` which will cause this
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coalescing_panda
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.8
4
+ version: 5.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Mills
8
8
  - Cody Tanner
9
9
  - Jake Sorce
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-08-26 00:00:00.000000000 Z
13
+ date: 2020-10-02 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
@@ -396,7 +396,7 @@ dependencies:
396
396
  - - ">="
397
397
  - !ruby/object:Gem::Version
398
398
  version: '0'
399
- description:
399
+ description:
400
400
  email:
401
401
  - nathanm@instructure.com
402
402
  - ctanner@instructure.com
@@ -490,6 +490,7 @@ files:
490
490
  - lib/coalescing_panda/misc_helper.rb
491
491
  - lib/coalescing_panda/route_helpers.rb
492
492
  - lib/coalescing_panda/secure_headers.rb
493
+ - lib/coalescing_panda/session_replacement.rb
493
494
  - lib/coalescing_panda/version.rb
494
495
  - lib/tasks/coalescing_panda_tasks.rake
495
496
  - spec/controllers/coalescing_panda/canvas_batches_controller_spec.rb
@@ -562,7 +563,7 @@ files:
562
563
  homepage: http://www.instructure.com
563
564
  licenses: []
564
565
  metadata: {}
565
- post_install_message:
566
+ post_install_message:
566
567
  rdoc_options: []
567
568
  require_paths:
568
569
  - lib
@@ -577,8 +578,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
577
578
  - !ruby/object:Gem::Version
578
579
  version: '0'
579
580
  requirements: []
580
- rubygems_version: 3.1.2
581
- signing_key:
581
+ rubyforge_project:
582
+ rubygems_version: 2.6.14.4
583
+ signing_key:
582
584
  specification_version: 4
583
585
  summary: Canvas LTI and OAUTH2 mountable engine
584
586
  test_files: