panda_pal 5.2.3 → 5.3.4

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
2
  SHA256:
3
- metadata.gz: f45c2c850cfd4b5551aab3608bc23a315c220c4e29f808e7cfc7889858b3b0cf
4
- data.tar.gz: 4b53ce7fb3a47bcda9f9720696efd76d0c92c66cecd9abb3f8b03e318968187c
3
+ metadata.gz: 6c598919f88d72cc23c02724fe9689b051ee82d553b8328e528253ec8e4f6622
4
+ data.tar.gz: 24b76f1904796d1672cb060482f63fc76fa56d34e8b248aad9480ac01e21fa3c
5
5
  SHA512:
6
- metadata.gz: 7139f7d41bd4dbef2d955db4986aeb19a5ae19794a1677efb30621d9c3e6326e675b7128e46d6d609131c8c31861a3bb2cdce1ee7fca0282ed66c1a5a00b0dd3
7
- data.tar.gz: 53df5ac963b3b52bbe02f3fe296a79fa57fb73460cafd3a5261ca7092bdc80f5bce95011522c71cef41dd0635398878f4562450b8010db1e3011a9742c1c76d0
6
+ metadata.gz: 75ea92798635fa208e51fc29d492c37fc8b75429b97a8e9e814e07b95e393d26aa03fe1599c42ed4a5dcbd1cb85b2d438a4d8a99ae17d885b7791c0458c4c749
7
+ data.tar.gz: 2cabc475ece25dadd88da19d3667e80a0f2d94d875d85cf05b244d692b0438771140cf4b9cc9d355f46c46aeca98583cb96323d7ee58c81eed8074cc7a7b87b8
data/README.md CHANGED
@@ -369,7 +369,11 @@ You will want to watch out for a few scenarios:
369
369
  3) If you use `link_to` and navigate in your LTI (apps that are not single page)
370
370
  make sure you include the `link_nonce` like so:
371
371
  ```ruby
372
- link_to "Link Name", somewhere_else_path(session_token: link_nonce)
372
+ link_to "Link Name", somewhere_else_path(arg, session_token: link_nonce)
373
+ ```
374
+ NB: As of PandaPal 5.2.6, you can instead use
375
+ ```ruby
376
+ link_to "Name", url_with_session(:somewhere_else_path, arg, kwarg: 1)
373
377
  ```
374
378
 
375
379
  ### Previous Safari Instructions
@@ -2,8 +2,6 @@ require_dependency "panda_pal/application_controller"
2
2
 
3
3
  module PandaPal
4
4
  class LtiV1P3Controller < ApplicationController
5
- skip_before_action :verify_authenticity_token
6
-
7
5
  before_action :validate_launch!, only: [:resource_link_request]
8
6
  around_action :switch_tenant, only: [:resource_link_request]
9
7
 
@@ -37,6 +35,12 @@ module PandaPal
37
35
 
38
36
  redirect_with_session_to(:"#{LaunchUrlHelpers.launch_route(params[:launch_type])}_url", route_context: main_app)
39
37
  end
38
+ # render json: {
39
+ # launch_type: params[:launch_type],
40
+ # final_url: LaunchUrlHelpers.launch_url(params[:launch_type]),
41
+ # final_route: LaunchUrlHelpers.launch_route(params[:launch_type]),
42
+ # decoded_jwt: @decoded_lti_jwt,
43
+ # }
40
44
  end
41
45
 
42
46
  def tool_config
@@ -4,9 +4,9 @@ module PandaPal
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
@@ -1,7 +1,8 @@
1
1
  require 'apartment/elevators/generic'
2
2
 
3
3
  Apartment.configure do |config|
4
- config.excluded_models = ['PandaPal::Organization', 'PandaPal::Session']
4
+ config.excluded_models ||= []
5
+ config.excluded_models |= ['PandaPal::Organization', 'PandaPal::Session']
5
6
 
6
7
  config.tenant_names = lambda {
7
8
  PandaPal::Organization.pluck(:name)
@@ -14,6 +15,21 @@ Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda {
14
15
  end
15
16
  }
16
17
 
18
+ module PandaPal::Plugins::ApartmentCache
19
+ private
20
+
21
+ if Rails.version >= '5.0'
22
+ def normalize_key(key, options)
23
+ "tenant:#{Apartment::Tenant.current}/#{super}"
24
+ end
25
+ else
26
+ def namespaced_key(*args)
27
+ "tenant:#{Apartment::Tenant.current}/#{super}"
28
+ end
29
+ end
30
+ end
31
+ ActiveSupport::Cache::Store.send(:prepend, PandaPal::Plugins::ApartmentCache)
32
+
17
33
  if defined?(ActionCable)
18
34
  module ActionCable
19
35
  module Channel
@@ -38,7 +54,7 @@ if defined?(ActionCable)
38
54
  end
39
55
  end
40
56
 
41
- module ActionCableApartment
57
+ module PandaPal::Plugins::ActionCableApartment
42
58
  module Connection
43
59
  def tenant=(name)
44
60
  @tenant = name
@@ -56,18 +72,50 @@ if defined?(ActionCable)
56
72
  end
57
73
  end
58
74
 
59
- ActionCable::Connection::Base.prepend(ActionCableApartment::Connection)
75
+ ActionCable::Connection::Base.prepend(PandaPal::Plugins::ActionCableApartment::Connection)
60
76
  end
61
77
 
62
- module ApartmentCache
63
- private
78
+ if defined?(Delayed)
79
+ module PandaPal::Plugins
80
+ class ApartmentDelayedJobsPlugin < ::Delayed::Plugin
81
+ callbacks do |lifecycle|
82
+ lifecycle.around(:enqueue) do |job, *args, &block|
83
+ current_tenant = Apartment::Tenant.current
64
84
 
65
- def normalize_key(key, options)
66
- "tenant:#{Apartment::Tenant.current}/#{super}"
67
- end
85
+ #make sure enqueue on public tenant unless we are testing since delayed job is set to run immediately
86
+ Apartment::Tenant.switch!('public') unless Rails.env.test?
87
+ job.tenant = current_tenant
88
+ begin
89
+ block.call(job, *args)
90
+ rescue Exception => e
91
+ Rails.logger.error("Error enqueing job #{job.to_s} - #{e.backtrace}")
92
+ ensure
93
+ #switch back to prev tenant
94
+ Apartment::Tenant.switch!(current_tenant)
95
+ end
96
+ end
97
+
98
+ lifecycle.before(:perform) do |worker, *args, &block|
99
+ tenant = args.first.tenant
100
+ Apartment::Tenant.switch!(tenant) if tenant.present?
101
+ Rails.logger.debug("Running job with tenant #{Apartment::Tenant.current}")
102
+ end
103
+
104
+ lifecycle.around(:invoke_job) do |job, *args, &block|
105
+ begin
106
+ block.call(job, *args)
107
+ ensure
108
+ Apartment::Tenant.switch!('public')
109
+ Rails.logger.debug("Resetting Tenant back to: #{Apartment::Tenant.current}")
110
+ end
111
+ end
68
112
 
69
- def namespaced_key(*args)
70
- normalize_key(*args)
113
+ lifecycle.after(:failure) do |job, *args|
114
+ Rails.logger.error("Job failed on tenant: #{Apartment::Tenant.current}")
115
+ end
116
+ end
117
+ end
71
118
  end
119
+
120
+ Delayed::Worker.plugins << PandaPal::Plugins::ApartmentDelayedJobsPlugin
72
121
  end
73
- ActiveSupport::Cache::Store.send :prepend, ApartmentCache
@@ -1,225 +1,139 @@
1
1
  require 'browser'
2
+ require_relative 'session_replacement'
2
3
 
3
- module PandaPal::Helpers::ControllerHelper
4
- extend ActiveSupport::Concern
4
+ module PandaPal::Helpers
5
+ module ControllerHelper
6
+ extend ActiveSupport::Concern
7
+ include SessionReplacement
5
8
 
6
- class SessionNotFound < StandardError; end
7
-
8
- included do
9
- helper_method :link_nonce, :current_session
10
-
11
- after_action :auto_save_session
12
- end
13
-
14
- def save_session
15
- current_session.try(:save)
16
- end
17
-
18
- def current_session
19
- return @current_session if @current_session.present?
20
-
21
- if params[:session_token]
22
- payload = JSON.parse(panda_pal_cryptor.decrypt_and_verify(params[:session_token])).with_indifferent_access
23
- matched_session = PandaPal::Session.find_by(session_key: payload[:session_key])
9
+ def current_organization
10
+ @organization ||= PandaPal::Organization.find_by!(key: organization_key) if organization_key
11
+ @organization ||= PandaPal::Organization.find_by(id: organization_id) if organization_id
12
+ @organization ||= PandaPal::Organization.find_by_name(Apartment::Tenant.current)
13
+ end
24
14
 
25
- if matched_session.present? && matched_session.data[:link_nonce] == payload[:nonce]
26
- @current_session = matched_session
27
- @current_session.data[:link_nonce] = nil
15
+ def current_lti_platform
16
+ return @current_lti_platform if @current_lti_platform.present?
17
+ # TODO: (Future) This could be expanded more to take better advantage of the LTI 1.3 Multi-Tenancy model.
18
+ if (canvas_url = current_organization&.settings&.dig(:canvas, :base_url)).present?
19
+ @current_lti_platform ||= PandaPal::Platform::Canvas.new(canvas_url)
28
20
  end
29
- elsif (session_key = params[:session_key] || session_key_header || flash[:session_key] || session[:session_key]).present?
30
- @current_session = PandaPal::Session.find_by(session_key: session_key) if session_key.present?
31
- else
32
- @current_session = PandaPal::Session.new(panda_pal_organization_id: current_organization.id)
21
+ @current_lti_platform ||= PandaPal::Platform::Canvas.new('http://localhost:3000') if Rails.env.development?
22
+ @current_lti_platform ||= PandaPal::Platform::CANVAS
23
+ @current_lti_platform
33
24
  end
34
25
 
35
- raise SessionNotFound, "Session Not Found" unless @current_session.present?
36
-
37
- @current_session
38
- end
39
-
40
- def current_organization
41
- @organization ||= PandaPal::Organization.find_by!(key: organization_key) if organization_key
42
- @organization ||= PandaPal::Organization.find_by(id: organization_id) if organization_id
43
- @organization ||= PandaPal::Organization.find_by_name(Apartment::Tenant.current)
44
- end
45
-
46
- def current_lti_platform
47
- return @current_lti_platform if @current_lti_platform.present?
48
- # TODO: (Future) This could be expanded more to take better advantage of the LTI 1.3 Multi-Tenancy model.
49
- if (canvas_url = current_organization&.settings&.dig(:canvas, :base_url)).present?
50
- @current_lti_platform ||= PandaPal::Platform::Canvas.new(canvas_url)
26
+ def lti_launch_params
27
+ current_session_data[:launch_params]
51
28
  end
52
- @current_lti_platform ||= PandaPal::Platform::Canvas.new('http://localhost:3000') if Rails.env.development?
53
- @current_lti_platform ||= PandaPal::Platform::CANVAS
54
- @current_lti_platform
55
- end
56
-
57
- def current_session_data
58
- current_session.data
59
- end
60
-
61
- def lti_launch_params
62
- current_session_data[:launch_params]
63
- end
64
-
65
- def session_changed?
66
- current_session.changed? && current_session.changes[:data].present?
67
- end
68
29
 
69
- def validate_launch!
70
- safari_override
30
+ def validate_launch!
31
+ safari_override
71
32
 
72
- if params[:id_token].present?
73
- validate_v1p3_launch
74
- elsif params[:oauth_consumer_key].present?
75
- validate_v1p0_launch
76
- end
77
- end
78
-
79
- def validate_v1p0_launch
80
- authorized = false
81
- if @organization = params['oauth_consumer_key'] && PandaPal::Organization.find_by_key(params['oauth_consumer_key'])
82
- sanitized_params = request.request_parameters
83
- # These params come over with a safari-workaround launch. The authenticator doesn't like them, so clean them out.
84
- safe_unexpected_params = ["full_win_launch_requested", "platform_redirect_url", "dummy_param"]
85
- safe_unexpected_params.each do |p|
86
- sanitized_params.delete(p)
33
+ if params[:id_token].present?
34
+ validate_v1p3_launch
35
+ elsif params[:oauth_consumer_key].present?
36
+ validate_v1p0_launch
87
37
  end
88
- authenticator = IMS::LTI::Services::MessageAuthenticator.new(request.original_url, sanitized_params, @organization.secret)
89
- authorized = authenticator.valid_signature?
90
38
  end
91
39
 
92
- if !authorized
93
- render plain: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
94
- end
95
-
96
- authorized
97
- end
40
+ def validate_v1p0_launch
41
+ authorized = false
42
+ if @organization = params['oauth_consumer_key'] && PandaPal::Organization.find_by_key(params['oauth_consumer_key'])
43
+ sanitized_params = request.request_parameters
44
+ # These params come over with a safari-workaround launch. The authenticator doesn't like them, so clean them out.
45
+ safe_unexpected_params = ["full_win_launch_requested", "platform_redirect_url", "dummy_param"]
46
+ safe_unexpected_params.each do |p|
47
+ sanitized_params.delete(p)
48
+ end
49
+ authenticator = IMS::LTI::Services::MessageAuthenticator.new(request.original_url, sanitized_params, @organization.secret)
50
+ authorized = authenticator.valid_signature?
51
+ end
98
52
 
99
- def validate_v1p3_launch
100
- decoded_jwt = JSON::JWT.decode(params.require(:id_token), :skip_verification)
101
- raise JSON::JWT::VerificationFailed, 'error decoding id_token' if decoded_jwt.blank?
53
+ if !authorized
54
+ render plain: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
55
+ end
102
56
 
103
- client_id = decoded_jwt['aud']
104
- @organization = PandaPal::Organization.find_by!(key: 'PandaPal') # client_id)
105
- raise JSON::JWT::VerificationFailed, 'Unrecognized Organization' unless @organization.present?
57
+ authorized
58
+ end
106
59
 
107
- decoded_jwt.verify!(current_lti_platform.public_jwks)
60
+ def validate_v1p3_launch
61
+ decoded_jwt = JSON::JWT.decode(params.require(:id_token), :skip_verification)
62
+ raise JSON::JWT::VerificationFailed, 'error decoding id_token' if decoded_jwt.blank?
108
63
 
109
- params[:session_key] = params[:state]
110
- raise JSON::JWT::VerificationFailed, 'State is invalid' unless current_session_data[:lti_oauth_nonce] == decoded_jwt['nonce']
64
+ client_id = decoded_jwt['aud']
65
+ @organization = PandaPal::Organization.find_by!(key: 'PandaPal') # client_id)
66
+ raise JSON::JWT::VerificationFailed, 'Unrecognized Organization' unless @organization.present?
111
67
 
112
- jwt_verifier = PandaPal::LtiJwtValidator.new(decoded_jwt, client_id)
113
- raise JSON::JWT::VerificationFailed, jwt_verifier.errors unless jwt_verifier.valid?
68
+ decoded_jwt.verify!(current_lti_platform.public_jwks)
114
69
 
115
- @decoded_lti_jwt = decoded_jwt
116
- rescue JSON::JWT::VerificationFailed => e
117
- payload = Array(e.message)
70
+ params[:session_key] = params[:state]
71
+ raise JSON::JWT::VerificationFailed, 'State is invalid' unless current_session_data[:lti_oauth_nonce] == decoded_jwt['nonce']
118
72
 
119
- render json: {
120
- message: [
121
- { errors: payload },
122
- { id_token: params.require(:id_token) },
123
- ],
124
- }, status: :unauthorized
73
+ jwt_verifier = PandaPal::LtiJwtValidator.new(decoded_jwt, client_id)
74
+ raise JSON::JWT::VerificationFailed, jwt_verifier.errors unless jwt_verifier.valid?
125
75
 
126
- false
127
- end
76
+ @decoded_lti_jwt = decoded_jwt
77
+ rescue JSON::JWT::VerificationFailed => e
78
+ payload = Array(e.message)
128
79
 
129
- def switch_tenant(organization = current_organization, &block)
130
- return unless organization
131
- raise 'This method should be called in an around_action callback' unless block_given?
80
+ render json: {
81
+ message: [
82
+ { errors: payload },
83
+ { id_token: params.require(:id_token) },
84
+ ],
85
+ }, status: :unauthorized
132
86
 
133
- Apartment::Tenant.switch(organization.name) do
134
- yield
87
+ false
135
88
  end
136
- end
137
89
 
138
- def forbid_access_if_lacking_session
139
- render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session?
140
- safari_override
141
- end
90
+ def switch_tenant(organization = current_organization, &block)
91
+ return unless organization
92
+ raise 'This method should be called in an around_action callback' unless block_given?
142
93
 
143
- def verify_authenticity_token
144
- # No need to check CSRF when no cookies were sent. This fixes CSRF failures in Browsers
145
- # that restrict Cookie setting within an IFrame.
146
- return unless request.cookies.keys.length > 0
147
- super
148
- end
149
-
150
- def valid_session?
151
- [
152
- current_session.persisted?,
153
- current_organization,
154
- current_session.panda_pal_organization_id == current_organization.id,
155
- Apartment::Tenant.current == current_organization.name
156
- ].all?
157
- end
158
-
159
- def safari_override
160
- use_secure_headers_override(:safari_override) if browser.safari?
161
- end
162
-
163
- # Redirect with the session key intact. In production,
164
- # handle this by adding a one-time use encrypted token to the URL.
165
- # Keeping it in the URL in development means that it plays
166
- # nicely with webpack-dev-server live reloading (otherwise
167
- # you get an access error everytime it tries to live reload).
168
-
169
- def redirect_with_session_to(location, params = {}, route_context: self, **rest)
170
- params.merge!(rest)
171
- if Rails.env.development?
172
- redirect_to route_context.send(location, {
173
- session_key: current_session.session_key,
174
- organization_id: current_organization.id,
175
- }.merge(params))
176
- else
177
- redirect_to route_context.send(location, {
178
- session_token: link_nonce,
179
- organization_id: current_organization.id,
180
- }.merge(params))
94
+ Apartment::Tenant.switch(organization.name) do
95
+ yield
96
+ end
181
97
  end
182
- end
183
-
184
- def link_nonce
185
- @link_nonce ||= begin
186
- current_session_data[:link_nonce] = SecureRandom.hex
187
98
 
188
- payload = {
189
- session_key: current_session.session_key,
190
- organization_id: current_organization.id,
191
- nonce: current_session_data[:link_nonce],
192
- }
99
+ def forbid_access_if_lacking_session
100
+ super
101
+ safari_override
102
+ end
193
103
 
194
- panda_pal_cryptor.encrypt_and_sign(payload.to_json)
104
+ def valid_session?
105
+ return false unless current_session(create_missing: false)&.persisted?
106
+ return false unless current_organization
107
+ return false unless current_session.panda_pal_organization_id == current_organization.id
108
+ return false unless Apartment::Tenant.current == current_organization.name
109
+ true
110
+ rescue SessionNonceMismatch
111
+ false
195
112
  end
196
- end
197
113
 
198
- private
114
+ def safari_override
115
+ use_secure_headers_override(:safari_override) if browser.safari?
116
+ end
199
117
 
200
- def organization_key
201
- org_key ||= params[:oauth_consumer_key]
202
- org_key ||= "#{params[:client_id]}/#{params[:deployment_id]}" if params[:client_id].present?
203
- org_key ||= session[:organization_key]
204
- org_key
205
- end
118
+ private
206
119
 
207
- def organization_id
208
- params[:organization_id]
209
- end
210
-
211
- def session_key_header
212
- if match = request.headers['Authorization'].try(:match, /token=(.+)/)
213
- match[1]
120
+ def find_or_create_session(key:)
121
+ if key == :create
122
+ PandaPal::Session.new(panda_pal_organization_id: current_organization.id)
123
+ else
124
+ PandaPal::Session.find_by(session_key: key)
125
+ end
214
126
  end
215
- end
216
127
 
217
- def panda_pal_cryptor
218
- @panda_pal_cryptor ||= ActiveSupport::MessageEncryptor.new(Rails.application.secret_key_base[0..31])
219
- end
128
+ def organization_key
129
+ org_key ||= params[:oauth_consumer_key]
130
+ org_key ||= "#{params[:client_id]}/#{params[:deployment_id]}" if params[:client_id].present?
131
+ org_key ||= session[:organization_key]
132
+ org_key
133
+ end
220
134
 
221
- def auto_save_session
222
- yield if block_given?
223
- save_session if @current_session && session_changed?
135
+ def organization_id
136
+ params[:organization_id]
137
+ end
224
138
  end
225
139
  end
@@ -0,0 +1,185 @@
1
+ module PandaPal::Helpers
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
+
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) if create_missing
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 PandaPal
2
- VERSION = "5.2.3"
2
+ VERSION = "5.3.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: panda_pal
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.2.3
4
+ version: 5.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure ProServe
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-19 00:00:00.000000000 Z
11
+ date: 2020-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -184,7 +184,7 @@ dependencies:
184
184
  - - ">="
185
185
  - !ruby/object:Gem::Version
186
186
  version: '0'
187
- description:
187
+ description:
188
188
  email:
189
189
  - pseng@instructure.com
190
190
  executables: []
@@ -237,6 +237,7 @@ files:
237
237
  - lib/panda_pal/helpers/controller_helper.rb
238
238
  - lib/panda_pal/helpers/route_helper.rb
239
239
  - lib/panda_pal/helpers/secure_headers.rb
240
+ - lib/panda_pal/helpers/session_replacement.rb
240
241
  - lib/panda_pal/plugins.rb
241
242
  - lib/panda_pal/version.rb
242
243
  - lib/tasks/panda_pal_tasks.rake
@@ -291,7 +292,7 @@ homepage: http://instructure.com
291
292
  licenses:
292
293
  - MIT
293
294
  metadata: {}
294
- post_install_message:
295
+ post_install_message:
295
296
  rdoc_options: []
296
297
  require_paths:
297
298
  - lib
@@ -307,7 +308,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
307
308
  version: '0'
308
309
  requirements: []
309
310
  rubygems_version: 3.1.2
310
- signing_key:
311
+ signing_key:
311
312
  specification_version: 4
312
313
  summary: LTI mountable engine
313
314
  test_files: