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 +5 -5
- data/README.md +5 -1
- data/config/initializers/apartment.rb +59 -11
- data/lib/panda_pal/helpers/controller_helper.rb +102 -186
- data/lib/panda_pal/helpers/session_replacement.rb +185 -0
- data/lib/panda_pal/version.rb +1 -1
- metadata +5 -15
- data/db/618eef7c0380ba654ad16f867a919e72.sqlite3 +0 -0
- data/db/9ff93d4f7e0e9dc80a43f68997caf4a1.sqlite3 +0 -0
- data/db/a3fda4044a7215bc2c9eb01a4b9e517a.sqlite3 +0 -0
- data/db/daa0e6378a5ec76fcce83b7070dad219.sqlite3 +0 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +0 -15058
- data/spec/dummy/log/test.log +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 42ed3ad8440c5f79c6ccbd20908ffc3e3458b342
|
4
|
+
data.tar.gz: 421fcb272ca0fb0d7c01b191ab4d47da9789d1c4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
63
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
70
|
-
|
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
|
4
|
-
|
4
|
+
module PandaPal::Helpers
|
5
|
+
module ControllerHelper
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include SessionReplacement
|
5
8
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
30
|
-
@
|
31
|
-
|
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
|
-
|
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
|
-
|
70
|
-
|
30
|
+
def validate_launch!
|
31
|
+
safari_override
|
71
32
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
53
|
+
if !authorized
|
54
|
+
render plain: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
|
55
|
+
end
|
102
56
|
|
103
|
-
|
104
|
-
|
105
|
-
raise JSON::JWT::VerificationFailed, 'Unrecognized Organization' unless @organization.present?
|
57
|
+
authorized
|
58
|
+
end
|
106
59
|
|
107
|
-
|
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
|
-
|
110
|
-
|
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
|
-
|
113
|
-
raise JSON::JWT::VerificationFailed, jwt_verifier.errors unless jwt_verifier.valid?
|
68
|
+
decoded_jwt.verify!(current_lti_platform.public_jwks)
|
114
69
|
|
115
|
-
|
116
|
-
|
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
|
-
|
120
|
-
|
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
|
-
|
127
|
-
|
76
|
+
@decoded_lti_jwt = decoded_jwt
|
77
|
+
rescue JSON::JWT::VerificationFailed => e
|
78
|
+
payload = Array(e.message)
|
128
79
|
|
129
|
-
|
130
|
-
|
131
|
-
|
80
|
+
render json: {
|
81
|
+
message: [
|
82
|
+
{ errors: payload },
|
83
|
+
{ id_token: params.require(:id_token) },
|
84
|
+
],
|
85
|
+
}, status: :unauthorized
|
132
86
|
|
133
|
-
|
134
|
-
yield
|
87
|
+
false
|
135
88
|
end
|
136
|
-
end
|
137
89
|
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
}
|
99
|
+
def forbid_access_if_lacking_session
|
100
|
+
super
|
101
|
+
safari_override
|
102
|
+
end
|
192
103
|
|
193
|
-
|
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
|
-
|
115
|
+
def safari_override
|
116
|
+
use_secure_headers_override(:safari_override) if browser.safari?
|
117
|
+
end
|
198
118
|
|
199
|
-
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
217
|
-
|
218
|
-
|
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
|
-
|
221
|
-
|
222
|
-
|
136
|
+
def organization_id
|
137
|
+
params[:organization_id]
|
138
|
+
end
|
223
139
|
end
|
224
140
|
end
|