coalescing_panda 5.0.8 → 5.1.3

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