coalescing_panda 5.0.6 → 5.1.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
- SHA256:
3
- metadata.gz: 92bed66c0757318a64fada41a45e598216c1daefdd7b43c8adf0a58c9d5259af
4
- data.tar.gz: d99684f401684ffdcb93ae63fe7ea49ba6ae4303db2e68ac43709e934b31ebeb
2
+ SHA1:
3
+ metadata.gz: ca4769293181b5ee47471cc9051ad0b678915f71
4
+ data.tar.gz: 60c8585ff7f5d28057b703a9ae7c5a85ab6dbdb7
5
5
  SHA512:
6
- metadata.gz: 8d1289c6a483a650b1e54a669ad2968cd1d3935e4ef3a67de130775cacfeab78a810ab7a736edb7b5a63b1960633f98fb8d813ceb0e3b6dd1e59d50448fa9697
7
- data.tar.gz: a8b9d22ffc7262a1385e156915e382ef43bc0d3e68d9077032676c92b9e9dd466084289c143079e977ee20d82e7259a05496ed452798aaf3ae3306f9efe006b7
6
+ metadata.gz: 6103b51481915a9d2719a87ff81c042a783d0111108e4c270fc3261a61008ab3683beaae3b2a9490450248fe882f4be75776d360219fe2dadb7377cc301f229f
7
+ data.tar.gz: f434c3a048769d55b72fe0ba190c7033ce0429b0acbf3ec0ec1a2ea2bc920262de49ef9336feb5e236d1767237bd79dd4ccf6bf14765c79358faa24fa329e91c
@@ -12,13 +12,13 @@ module CoalescingPanda
12
12
  @batch.status = 'Queued'
13
13
  @batch.save
14
14
  worker = CoalescingPanda::Workers::CourseMiner.new(@batch.context, @batch.options)
15
- session[:canvas_batch_id] = worker.batch.id
15
+ current_session_data[:canvas_batch_id] = worker.batch.id
16
16
  worker.start(true)
17
17
  redirect_to :back
18
18
  end
19
19
 
20
20
  def clear_batch_session
21
- session[:canvas_batch_id] = nil
21
+ current_session_data[:canvas_batch_id] = nil
22
22
  render nothing: true
23
23
  end
24
24
  end
@@ -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,7 +1,7 @@
1
1
  module CoalescingPanda
2
2
  module CanvasBatchesHelper
3
3
  def current_batch
4
- @current_batch ||= CoalescingPanda::CanvasBatch.find_by_id(session[:canvas_batch_id])
4
+ @current_batch ||= CoalescingPanda::CanvasBatch.find_by_id(current_session_data[:canvas_batch_id])
5
5
  end
6
6
  end
7
7
  end
@@ -1,4 +1,4 @@
1
1
  - if current_batch.present?
2
- - path = CoalescingPanda::Engine.routes.url_helpers.canvas_batch_path(current_batch)
3
- - clear_path = CoalescingPanda::Engine.routes.url_helpers.clear_batch_session_path
4
- #batch-progress{data: {batch: current_batch.try(:to_json), url: path, clear_path: clear_path} }
2
+ - path = CoalescingPanda::Engine.routes.url_helpers.canvas_batch_path(current_batch) + "?encrypted_session_key=#{encrypted_session_key}"
3
+ - clear_path = CoalescingPanda::Engine.routes.url_helpers.clear_batch_session_path + "?encrypted_session_key=#{encrypted_session_key}"
4
+ #batch-progress{data: {batch: current_batch.try(:to_json), url: path, clear_path: clear_path} }
@@ -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,22 @@ 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
+ [
162
+ current_session&.persisted?,
163
+ ].all?
164
+ rescue SessionNonceMismatch
165
+ false
198
166
  end
199
167
 
200
168
  private
201
169
 
202
- def msg_encryptor
203
- @crypt ||= ActiveSupport::MessageEncryptor.new(Rails.application.secrets.secret_key_base[0..31])
170
+ def find_or_create_session(key:)
171
+ if key == :create
172
+ CoalescingPanda::PersistentSession.create_from_launch(params, current_lti_account.id) if current_lti_account.present?
173
+ else
174
+ CoalescingPanda::PersistentSession.find_by(session_key: key)
175
+ end
204
176
  end
205
177
 
206
178
  def organization_key
@@ -211,51 +183,16 @@ module CoalescingPanda
211
183
  params[:organization_id] || (current_session_data[:launch_params][:organization_id] if @current_session)
212
184
  end
213
185
 
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']
186
+ # This is necessitated by a bug in Rails Engines where it isn't resolving the URL correctly
187
+ # when using coalescing_panda.xyz_url (The Engine Prefix is not included)
188
+ # I believe https://github.com/rails/rails/issues/34452 is the same issue
189
+ def resolve_coalescing_panda_url(key)
190
+ key = key.to_s[0...-4] if key.to_s.ends_with?('_url')
191
+ resolved_path = coalescing_panda.send(:"#{key}_path")
192
+ cpurl = coalescing_panda_url
193
+ cppath = URI.parse(cpurl).path
194
+ resolved_path = cppath + resolved_path unless resolved_path.starts_with?(cppath)
195
+ URI.join(cpurl, resolved_path)
219
196
  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
197
  end
261
198
  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,185 @@
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
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
+
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]) > 15.minutes.ago
42
+ @current_session = matched_session
43
+ end
44
+ end
45
+ raise SessionNonceMismatch, "Session Not Found" unless @current_session.present?
46
+ elsif (session_key = params[:session_key] || session_key_header || flash[:session_key] || session[:session_key]).present?
47
+ @current_session = find_or_create_session(key: session_key)
48
+ end
49
+
50
+ @current_session ||= find_or_create_session(key: :create)
51
+
52
+ @current_session
53
+ end
54
+
55
+ def current_session_data
56
+ current_session.data
57
+ end
58
+
59
+ def session_changed?
60
+ current_session.changed? && current_session.changes[:data].present?
61
+ end
62
+
63
+ def forbid_access_if_lacking_session
64
+ render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session?
65
+ end
66
+
67
+ def verify_authenticity_token
68
+ # No need to check CSRF when no cookies were sent. This fixes CSRF failures in Browsers
69
+ # that restrict Cookie setting within an IFrame.
70
+ return unless request.cookies.keys.length > 0
71
+ super
72
+ end
73
+
74
+ # Redirect with the session key intact. In production,
75
+ # handle this by adding a one-time use encrypted token to the URL.
76
+ # Keeping it in the URL in development means that it plays
77
+ # nicely with webpack-dev-server live reloading (otherwise
78
+ # you get an access error everytime it tries to live reload).
79
+
80
+ def redirect_with_session_to(*args)
81
+ redirect_to url_with_session(*args)
82
+ end
83
+
84
+ def link_with_session_to(*args)
85
+ helpers.link_to url_with_session(*args)
86
+ end
87
+
88
+ def session_url_for(*args)
89
+ url_for(build_session_url_params(*args))
90
+ end
91
+
92
+ def url_with_session(location, *args, route_context: self, **kwargs)
93
+ route_context.send(location, *build_session_url_params(*args, **kwargs))
94
+ end
95
+
96
+ def link_nonce(type: link_nonce_type)
97
+ type = instance_exec(&type) if type.is_a?(Proc)
98
+ type = type.to_s
99
+
100
+ @cached_link_nonces ||= {}
101
+ @cached_link_nonces[type] ||= begin
102
+ payload = {
103
+ token_type: type,
104
+ session_key: current_session.session_key,
105
+ organization_id: current_organization.id,
106
+ }
107
+
108
+ if type == 'nonce'
109
+ current_session_data[:link_nonce] = SecureRandom.hex
110
+ payload.merge!(nonce: current_session_data[:link_nonce])
111
+ elsif type == 'fixed_ip'
112
+ current_session_data[:remote_ip] ||= request.remote_ip
113
+ current_session_data[:last_ip_token_requested] = DateTime.now.iso8601
114
+ else
115
+ raise StandardError, "Unsupported link_nonce_type: '#{type}'"
116
+ end
117
+
118
+ session_cryptor.encrypt_and_sign(payload.to_json)
119
+ end
120
+ end
121
+
122
+ def link_nonce_type
123
+ self.class.link_nonce_type
124
+ end
125
+
126
+ private
127
+
128
+ def session_cryptor
129
+ secret_key_base = Rails.application.try(:secret_key_base) || Rails.application.secrets.secret_key_base
130
+ @session_cryptor ||= ActiveSupport::MessageEncryptor.new(secret_key_base[0..31])
131
+ end
132
+
133
+ def session_key_header
134
+ if match = request.headers['Authorization'].try(:match, /token=(.+)/)
135
+ match[1]
136
+ end
137
+ end
138
+
139
+ def build_session_url_params(*args, nonce_type: link_nonce_type, **kwargs)
140
+ if args[-1].is_a?(Hash)
141
+ args[-1] = args[-1].dup
142
+ else
143
+ args.push({})
144
+ end
145
+
146
+ if Rails.env.development?
147
+ args[-1].merge!(
148
+ session_key: current_session.session_key,
149
+ organization_id: current_organization.id,
150
+ )
151
+ else
152
+ args[-1].merge!(
153
+ session_token: link_nonce(type: nonce_type),
154
+ organization_id: current_organization.id,
155
+ )
156
+ end
157
+
158
+ args[-1].merge!(kwargs)
159
+ args
160
+ end
161
+
162
+ def auto_save_session
163
+ yield if block_given?
164
+ save_session if @current_session && session_changed?
165
+ end
166
+
167
+ def monkeypatch_flash
168
+ if valid_session? && (value = current_session_data['flashes']).present?
169
+ flashes = value["flashes"]
170
+ if discard = value["discard"]
171
+ flashes.except!(*discard)
172
+ end
173
+ flash.replace(flashes)
174
+ flash.discard()
175
+ end
176
+
177
+ yield
178
+
179
+ if @current_session.present?
180
+ current_session_data['flashes'] = flash.to_session_value
181
+ flash.discard()
182
+ end
183
+ end
184
+ end
185
+ end
@@ -1,3 +1,3 @@
1
1
  module CoalescingPanda
2
- VERSION = '5.0.6'
2
+ VERSION = '5.1.0'
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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coalescing_panda
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.6
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Mills
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-08-18 00:00:00.000000000 Z
13
+ date: 2020-09-14 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
@@ -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
@@ -577,7 +578,8 @@ 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
+ rubyforge_project:
582
+ rubygems_version: 2.6.14.4
581
583
  signing_key:
582
584
  specification_version: 4
583
585
  summary: Canvas LTI and OAUTH2 mountable engine