panda_pal 5.2.1 → 5.3.0

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: 88fe8cce75e1f511b08e025ca0f00ab9cb224e9ab9c91f4a6f36fbbef6031f26
4
- data.tar.gz: 9b23a25becc266908e081985943a9b51a5b856b7b52c8c948e007ad638ce90a9
2
+ SHA1:
3
+ metadata.gz: 42ed3ad8440c5f79c6ccbd20908ffc3e3458b342
4
+ data.tar.gz: 421fcb272ca0fb0d7c01b191ab4d47da9789d1c4
5
5
  SHA512:
6
- metadata.gz: 5b82a6629e5d87d9420263eff1a57c477c9e1786fd91312354d8de41c0eba1e83968e4bf954691cb17eb09e6e5fc1f73284f08c76574ec459329ec5c5093d9c2
7
- data.tar.gz: 86c997d27d7de28fd3be87bd9343c06ec5cfbf35a1559b2f131f068315b883449b58049b23ccba31e99524a32cef3132ff12ac50777a098821c17bbbe445a2e6
6
+ metadata.gz: 7a32296d2975f108022334ec1f32b23a461ee7e37966c639680779198a1941b683ef6bc02fe020a61d8ffa683132f7dc05199d43d8fb2c50ed877082a6129b66
7
+ data.tar.gz: 8d9f225f68a4b3b4d632f48a3975eab2a61ad61575c9278e06e1f21f41b7df6950750f8b429da47ab45c3e0d8aaf032e32da7b98b01c3a0d1151e049971b5857
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
@@ -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,224 +1,140 @@
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] == params[:session_token]
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)
170
- if Rails.env.development?
171
- redirect_to route_context.send(location, {
172
- session_key: current_session.session_key,
173
- organization_id: current_organization.id,
174
- }.merge(params))
175
- else
176
- redirect_to route_context.send(location, {
177
- session_token: link_nonce,
178
- organization_id: current_organization.id,
179
- }.merge(params))
94
+ Apartment::Tenant.switch(organization.name) do
95
+ yield
96
+ end
180
97
  end
181
- end
182
-
183
- def link_nonce
184
- @link_nonce ||= begin
185
- current_session_data[:link_nonce] = SecureRandom.hex
186
98
 
187
- payload = {
188
- session_key: current_session.session_key,
189
- organization_id: current_organization.id,
190
- nonce: current_session_data[:link_nonce],
191
- }
99
+ def forbid_access_if_lacking_session
100
+ super
101
+ safari_override
102
+ end
192
103
 
193
- panda_pal_cryptor.encrypt_and_sign(payload.to_json)
104
+ def valid_session?
105
+ [
106
+ current_session&.persisted?,
107
+ current_organization,
108
+ current_session&.panda_pal_organization_id == current_organization.id,
109
+ Apartment::Tenant.current == current_organization.name
110
+ ].all?
111
+ rescue SessionNonceMismatch
112
+ false
194
113
  end
195
- end
196
114
 
197
- private
115
+ def safari_override
116
+ use_secure_headers_override(:safari_override) if browser.safari?
117
+ end
198
118
 
199
- def organization_key
200
- org_key ||= params[:oauth_consumer_key]
201
- org_key ||= "#{params[:client_id]}/#{params[:deployment_id]}" if params[:client_id].present?
202
- org_key ||= session[:organization_key]
203
- org_key
204
- end
119
+ private
205
120
 
206
- def organization_id
207
- params[:organization_id]
208
- end
209
-
210
- def session_key_header
211
- if match = request.headers['Authorization'].try(:match, /token=(.+)/)
212
- match[1]
121
+ def find_or_create_session(key:)
122
+ if key == :create
123
+ PandaPal::Session.new(panda_pal_organization_id: current_organization.id)
124
+ else
125
+ PandaPal::Session.find_by(session_key: key)
126
+ end
213
127
  end
214
- end
215
128
 
216
- def panda_pal_cryptor
217
- @panda_pal_cryptor ||= ActiveSupport::MessageEncryptor.new(Rails.application.secret_key_base[0..31])
218
- end
129
+ def organization_key
130
+ org_key ||= params[:oauth_consumer_key]
131
+ org_key ||= "#{params[:client_id]}/#{params[:deployment_id]}" if params[:client_id].present?
132
+ org_key ||= session[:organization_key]
133
+ org_key
134
+ end
219
135
 
220
- def auto_save_session
221
- yield if block_given?
222
- save_session if @current_session && session_changed?
136
+ def organization_id
137
+ params[:organization_id]
138
+ end
223
139
  end
224
140
  end