panda_pal 5.12.9 → 5.13.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 +4 -4
- data/README.md +4 -5
- data/app/controllers/panda_pal/lti_v1_p0_controller.rb +5 -1
- data/app/controllers/panda_pal/lti_v1_p3_controller.rb +10 -8
- data/app/models/panda_pal/organization.rb +2 -2
- data/app/models/panda_pal/organization_concerns/task_scheduling.rb +1 -1
- data/config/initializers/apartment.rb +3 -3
- data/lib/panda_pal/concerns/ability_helper.rb +1 -1
- data/lib/panda_pal/engine.rb +9 -11
- data/lib/panda_pal/helpers/controller_helper.rb +32 -28
- data/lib/panda_pal/helpers/session_replacement.rb +26 -13
- data/lib/panda_pal/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bde41047ef682bd599b2ee621fdb050dc3d4269eca64272f1415c667dbbbffe2
|
4
|
+
data.tar.gz: 9a3cb2ccab6721ceae849e29a7080685575578997fa310e0f912c2cb2888a4ea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11e6da03b76101f436cf2581192df1d68308e2568fd193e58ec64ada4d95305cfc5ed96994ce0fa5e718dd1f135c2975605179515dc53210429d5e1c8c5d2b64
|
7
|
+
data.tar.gz: e349ae30482529c6df4137b856f3383a58b4a4244e4ac0735790fe41ae69dd2af7a25839931b8d5adbdbad4af8f64df45e64d2e5e4e4acd62ff00900d30bb7a7
|
data/README.md
CHANGED
@@ -218,7 +218,7 @@ namespace :lti_tasks do
|
|
218
218
|
desc 'some rake task'
|
219
219
|
task some_rake_task: :environment do
|
220
220
|
PandaPal::Organization.all.each do |org|
|
221
|
-
|
221
|
+
org.switch_tenant do
|
222
222
|
SomeJob.perform_later
|
223
223
|
end
|
224
224
|
end
|
@@ -231,7 +231,6 @@ Controllers will automatically have access to a number of methods that can be he
|
|
231
231
|
|
232
232
|
* `validate_launch!` - should be used in any tool launch points, and will verify that the launch request received from Canvas is legitimate based on the saved key and secret.
|
233
233
|
* `current_session` - PandaPal provides support to use database persisted sessions, rather than the standard rails variety, as these play better when the tool is launched in an `iframe`. The `session_key` attribute from the returned object needs to be served to the client at some point during the launch and during subsequent requests. The benefit from this is that sessions will exist entirely in the frame used to launch the tool, and will not be visible should the user be accessing the tool in other contexts simultaneously. See section below for more information about this method.
|
234
|
-
* `current_session_data` - Shortcut method to access the `data` hash given by the above method.
|
235
234
|
* `current_organization` - Used to return the organization related to the currently selected tenant. See section below for more information about this method.
|
236
235
|
* `session_changed?` - returns true if the session given by `current_session` has been modified during the request.
|
237
236
|
* `save_session` - Saves the session given by `current_session` to the database.
|
@@ -558,13 +557,13 @@ class LaunchController < ApplicationController
|
|
558
557
|
# request, and Canvas wouldn't know anything about the CSRF
|
559
558
|
skip_before_action :verify_authenticity_token
|
560
559
|
skip_before_action :forbid_access_if_lacking_session # We don't have a session yet
|
561
|
-
around_action :switch_tenant
|
562
560
|
before_action :validate_launch!
|
561
|
+
around_action :switch_tenant
|
563
562
|
before_action :handle_launch
|
564
563
|
|
565
564
|
def handle_launch
|
566
|
-
|
567
|
-
|
565
|
+
current_session[:canvas_user_id] = params[:custom_canvas_user_id]
|
566
|
+
current_session[:canvas_course_id] = params[:custom_canvas_course_id]
|
568
567
|
end
|
569
568
|
|
570
569
|
def account
|
@@ -8,8 +8,12 @@ module PandaPal
|
|
8
8
|
skip_before_action :verify_authenticity_token, raise: false
|
9
9
|
end
|
10
10
|
|
11
|
+
before_action ->{ validate_launch!(version: :v1p0) }, only: [:launch]
|
12
|
+
around_action :switch_tenant, only: [:launch]
|
13
|
+
|
14
|
+
# LTI v1 Auto-Launch Action
|
11
15
|
def launch
|
12
|
-
|
16
|
+
current_session.data.merge!({
|
13
17
|
lti_version: 'v1p0',
|
14
18
|
lti_launch_placement: params[:launch_type],
|
15
19
|
launch_params: params.to_unsafe_h,
|
@@ -8,7 +8,7 @@ module PandaPal
|
|
8
8
|
skip_before_action :verify_authenticity_token, raise: false
|
9
9
|
end
|
10
10
|
|
11
|
-
before_action :
|
11
|
+
before_action ->{ validate_launch!(version: :v1p3) }, only: [:resource_link_request]
|
12
12
|
around_action :switch_tenant, only: [:resource_link_request]
|
13
13
|
before_action :enforce_environment!, only: [:resource_link_request]
|
14
14
|
|
@@ -18,10 +18,12 @@ module PandaPal
|
|
18
18
|
def login
|
19
19
|
@current_lti_platform = PandaPal::Platform.resolve_platform(params)
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
start_panda_session!
|
22
|
+
|
23
|
+
current_session[:lti_platform] = @current_lti_platform&.serialize
|
24
|
+
current_session[:lti_oauth_nonce] = SecureRandom.uuid
|
25
|
+
current_session[:canvas_environment] = params['canvas_environment']
|
26
|
+
current_session[:canvas_region] = params['canvas_region']
|
25
27
|
current_session.panda_pal_organization_id = -1
|
26
28
|
|
27
29
|
@form_action = current_lti_platform.authentication_redirect_url
|
@@ -36,7 +38,7 @@ module PandaPal
|
|
36
38
|
login_hint: params.require(:login_hint),
|
37
39
|
lti_message_hint: params.require(:lti_message_hint),
|
38
40
|
state: current_session.session_key,
|
39
|
-
nonce:
|
41
|
+
nonce: current_session[:lti_oauth_nonce]
|
40
42
|
}
|
41
43
|
end
|
42
44
|
|
@@ -44,7 +46,7 @@ module PandaPal
|
|
44
46
|
ltype = @decoded_lti_jwt['https://www.instructure.com/placement']
|
45
47
|
|
46
48
|
if ltype
|
47
|
-
|
49
|
+
current_session.data.merge!({
|
48
50
|
lti_version: 'v1p3',
|
49
51
|
lti_launch_placement: ltype,
|
50
52
|
launch_params: @decoded_lti_jwt,
|
@@ -140,7 +142,7 @@ module PandaPal
|
|
140
142
|
end
|
141
143
|
|
142
144
|
def enforce_environment!
|
143
|
-
canvas_env =
|
145
|
+
canvas_env = current_session[:canvas_environment]
|
144
146
|
return unless canvas_env.present?
|
145
147
|
|
146
148
|
org_canvas_url = current_organization.canvas_url
|
@@ -80,8 +80,8 @@ module PandaPal
|
|
80
80
|
# production environment might not have loaded secret_key_base yet.
|
81
81
|
# In that case, just read it from env.
|
82
82
|
secret_key_base = Rails.application.try(:secret_key_base)
|
83
|
-
secret_key_base ||= Rails.
|
84
|
-
secret_key_base ||= Rails.
|
83
|
+
secret_key_base ||= Rails.application.credentials.secret_key_base if Rails.version > "7.0"
|
84
|
+
secret_key_base ||= Rails.application.secrets.secret_key_base if Rails.version < "7.2"
|
85
85
|
secret_key_base ||= ENV["SECRET_KEY_BASE"]
|
86
86
|
|
87
87
|
secret_key_base[0,32]
|
@@ -199,7 +199,7 @@ module PandaPal
|
|
199
199
|
task = Organization._schedule_descriptors[task_key]
|
200
200
|
worker = task[:worker]
|
201
201
|
|
202
|
-
|
202
|
+
org.switch_tenant do
|
203
203
|
if worker.is_a?(Proc)
|
204
204
|
return org.instance_exec(&worker)
|
205
205
|
elsif worker.is_a?(Symbol)
|
@@ -215,11 +215,11 @@ end
|
|
215
215
|
|
216
216
|
Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request|
|
217
217
|
if match = request.path.match(/\/(?:orgs?|organizations?)\/(\d+)/)
|
218
|
-
PandaPal::Organization.find_by(id: match[1]).try(:
|
218
|
+
PandaPal::Organization.find_by(id: match[1]).try(:tenant_name)
|
219
219
|
elsif match = request.path.match(/\/(?:orgs?|organizations?|o)\/(\w+)/)
|
220
|
-
PandaPal::Organization.find_by(name: match[1]).try(:
|
220
|
+
PandaPal::Organization.find_by(name: match[1]).try(:tenant_name)
|
221
221
|
elsif request.path.start_with?('/rails/active_storage/blobs/')
|
222
|
-
PandaPal::Organization.find_by(id: request.params['organization_id']).try(:
|
222
|
+
PandaPal::Organization.find_by(id: request.params['organization_id']).try(:tenant_name)
|
223
223
|
end
|
224
224
|
}
|
225
225
|
|
@@ -30,7 +30,7 @@ module PandaPal::Concerns
|
|
30
30
|
raise "Ability class needs to set @panda_pal_session or @controller to use this feature"
|
31
31
|
end
|
32
32
|
|
33
|
-
@panda_pal_session = @controller.
|
33
|
+
@panda_pal_session = @controller.current_panda_session unless defined?(@panda_pal_session)
|
34
34
|
|
35
35
|
if @panda_pal_session.is_a?(Hash)
|
36
36
|
# This is a breaking-change to CanvasSync, but not to PandaPal
|
data/lib/panda_pal/engine.rb
CHANGED
@@ -58,21 +58,19 @@ module PandaPal
|
|
58
58
|
class ::Object
|
59
59
|
unless defined?(switch_tenant)
|
60
60
|
def switch_tenant(tenant, &block)
|
61
|
-
if
|
62
|
-
|
61
|
+
if tenant.to_s.include?(":")
|
62
|
+
if block_given?
|
63
|
+
Apartment::Tenant.switch(tenant, &block)
|
64
|
+
else
|
65
|
+
Apartment::Tenant.switch!(tenant)
|
66
|
+
end
|
63
67
|
else
|
64
|
-
|
68
|
+
org = PandaPal::Organization.find_by(name: tenant)
|
69
|
+
org.switch_tenant(&block)
|
65
70
|
end
|
66
71
|
end
|
67
72
|
end
|
68
73
|
|
69
|
-
unless defined?(switch_org)
|
70
|
-
def switch_org(org, &block)
|
71
|
-
org = PandaPal::Organization.for_apt_tenant(org) if org.is_a?(String)
|
72
|
-
org.switch_tenant(&block)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
74
|
unless defined?(current_organization)
|
77
75
|
def current_organization
|
78
76
|
PandaPal::Organization.for_apt_tenant(Apartment::Tenant.current)
|
@@ -185,7 +183,7 @@ module PandaPal
|
|
185
183
|
|
186
184
|
if org
|
187
185
|
org.switch_tenant
|
188
|
-
puts "PandaPal: Auto Switched to tenant '#{org.
|
186
|
+
puts "PandaPal: Auto Switched to tenant '#{org.tenant_name}'"
|
189
187
|
end
|
190
188
|
rescue => err
|
191
189
|
puts "PandaPal: Error occurred auto-switching tenant: #{err}"
|
@@ -7,27 +7,38 @@ module PandaPal::Helpers
|
|
7
7
|
include SessionReplacement
|
8
8
|
|
9
9
|
def current_organization
|
10
|
-
@organization ||=
|
11
|
-
@organization ||= PandaPal::Organization.find_by(
|
12
|
-
@organization ||= PandaPal::Organization.
|
10
|
+
@organization ||= current_session&.panda_pal_organization
|
11
|
+
@organization ||= PandaPal::Organization.find_by!(key: organization_key) if organization_key # Deprecated
|
12
|
+
@organization ||= PandaPal::Organization.find_by(id: organization_id) if organization_id # Deprecated
|
13
|
+
@organization ||= PandaPal::Organization.for_apt_tenant(Apartment::Tenant.current)
|
13
14
|
end
|
14
15
|
|
15
16
|
def current_lti_platform
|
16
|
-
@current_lti_platform ||=
|
17
|
+
@current_lti_platform ||= current_panda_session&.lti_platform
|
17
18
|
end
|
18
19
|
|
19
20
|
def lti_launch_params
|
20
|
-
|
21
|
+
current_panda_session[:launch_params]
|
21
22
|
end
|
22
23
|
|
23
|
-
def validate_launch!
|
24
|
+
def validate_launch!(version: :any)
|
24
25
|
safari_override
|
25
26
|
|
26
|
-
if
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
version = %i[v1p0 v1p3] if version == :any
|
28
|
+
|
29
|
+
if version.is_a?(Array)
|
30
|
+
version = case
|
31
|
+
when params[:id_token].present?
|
32
|
+
:v1p3
|
33
|
+
when params[:oauth_consumer_key].present?
|
34
|
+
:v1p0
|
35
|
+
end
|
30
36
|
end
|
37
|
+
|
38
|
+
valmthd = :"validate_#{version}_launch"
|
39
|
+
return send(valmthd) if respond_to?(valmthd)
|
40
|
+
|
41
|
+
render plain: 'Failed to validate LTI launch', status: :unauthorized
|
31
42
|
end
|
32
43
|
|
33
44
|
def validate_v1p0_launch
|
@@ -48,10 +59,11 @@ module PandaPal::Helpers
|
|
48
59
|
end
|
49
60
|
|
50
61
|
if !authorized
|
51
|
-
render plain: '
|
62
|
+
render plain: 'Failed to validate LTI v1p0 launch', :status => :unauthorized
|
63
|
+
return false
|
52
64
|
end
|
53
65
|
|
54
|
-
|
66
|
+
start_panda_session!
|
55
67
|
end
|
56
68
|
|
57
69
|
def validate_v1p3_launch
|
@@ -81,12 +93,12 @@ module PandaPal::Helpers
|
|
81
93
|
decoded_jwt.verify!(current_lti_platform.public_jwks(force: true))
|
82
94
|
end
|
83
95
|
|
84
|
-
raise JSON::JWT::VerificationFailed, 'State is invalid' unless
|
96
|
+
raise JSON::JWT::VerificationFailed, 'State is invalid' unless current_panda_session[:lti_oauth_nonce] == decoded_jwt['nonce']
|
85
97
|
|
86
98
|
jwt_verifier = PandaPal::LtiJwtValidator.new(decoded_jwt, client_id)
|
87
99
|
raise JSON::JWT::VerificationFailed, jwt_verifier.errors unless jwt_verifier.valid?
|
88
100
|
|
89
|
-
|
101
|
+
current_panda_session.update(panda_pal_organization: @organization)
|
90
102
|
|
91
103
|
@decoded_lti_jwt = decoded_jwt
|
92
104
|
rescue JSON::JWT::VerificationFailed => e
|
@@ -106,9 +118,7 @@ module PandaPal::Helpers
|
|
106
118
|
return unless organization
|
107
119
|
raise 'This method should be called in an around_action callback' unless block_given?
|
108
120
|
|
109
|
-
|
110
|
-
yield
|
111
|
-
end
|
121
|
+
organization.switch_tenant(&block)
|
112
122
|
end
|
113
123
|
|
114
124
|
def forbid_access_if_lacking_session
|
@@ -117,10 +127,10 @@ module PandaPal::Helpers
|
|
117
127
|
end
|
118
128
|
|
119
129
|
def valid_session?
|
120
|
-
return false unless
|
130
|
+
return false unless current_panda_session&.persisted?
|
121
131
|
return false unless current_organization
|
122
|
-
return false unless
|
123
|
-
return false unless Apartment::Tenant.current == current_organization.
|
132
|
+
return false unless current_panda_session.panda_pal_organization_id == current_organization.id
|
133
|
+
return false unless Apartment::Tenant.current == current_organization.tenant_name
|
124
134
|
true
|
125
135
|
rescue SessionNonceMismatch
|
126
136
|
false
|
@@ -132,20 +142,14 @@ module PandaPal::Helpers
|
|
132
142
|
|
133
143
|
private
|
134
144
|
|
135
|
-
|
136
|
-
if key == :create
|
137
|
-
PandaPal::Session.new(panda_pal_organization_id: current_organization&.id)
|
138
|
-
else
|
139
|
-
PandaPal::Session.find_by(session_key: key)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
145
|
+
# Deprecated
|
143
146
|
def organization_key
|
144
147
|
org_key ||= params[:oauth_consumer_key]
|
145
148
|
org_key ||= session[:organization_key]
|
146
149
|
org_key
|
147
150
|
end
|
148
151
|
|
152
|
+
# Deprecated
|
149
153
|
def organization_id
|
150
154
|
params[:organization_id]
|
151
155
|
end
|
@@ -33,12 +33,17 @@ module PandaPal::Helpers
|
|
33
33
|
current_session.try(:save)
|
34
34
|
end
|
35
35
|
|
36
|
-
def
|
37
|
-
|
36
|
+
def start_panda_session!
|
37
|
+
raise "Session already started" if @current_session.present?
|
38
|
+
@current_session = PandaPal::Session.new(panda_pal_organization_id: current_organization&.id)
|
39
|
+
end
|
40
|
+
|
41
|
+
def current_panda_session
|
42
|
+
return @current_session if defined?(@current_session)
|
38
43
|
|
39
44
|
if params[:session_token]
|
40
45
|
payload = JSON.parse(session_cryptor.decrypt_and_verify(params[:session_token])).with_indifferent_access
|
41
|
-
matched_session =
|
46
|
+
matched_session = PandaPal::Session.find_by(session_key: payload[:session_key])
|
42
47
|
if matched_session.present?
|
43
48
|
if payload[:token_type] == 'nonce' && matched_session.data[:link_nonce] == payload[:nonce]
|
44
49
|
@current_session = matched_session
|
@@ -52,15 +57,23 @@ module PandaPal::Helpers
|
|
52
57
|
end
|
53
58
|
raise SessionNonceMismatch, "Session Not Found" unless @current_session.present?
|
54
59
|
elsif (session_key = params[:session_key] || session_key_header || flash[:session_key] || session[:session_key]).present?
|
55
|
-
@current_session =
|
60
|
+
@current_session = PandaPal::Session.find_by(session_key: session_key)
|
56
61
|
end
|
62
|
+
end
|
57
63
|
|
58
|
-
|
64
|
+
# @deprecated - User current_panda_session instead
|
65
|
+
def current_session(create_missing: false)
|
66
|
+
current_panda_session
|
67
|
+
|
68
|
+
if !@current_session && create_missing
|
69
|
+
Rails.logger.warn("current_session(create_missing: true) is deprecated. Use start_panda_session! instead.") unless Rails.env.production?
|
70
|
+
start_panda_session!
|
71
|
+
end
|
59
72
|
|
60
73
|
@current_session
|
61
74
|
end
|
62
75
|
|
63
|
-
# @deprecated
|
76
|
+
# @deprecated - Use current_session[:key] directly
|
64
77
|
def current_session_data
|
65
78
|
current_session.data
|
66
79
|
end
|
@@ -115,13 +128,13 @@ module PandaPal::Helpers
|
|
115
128
|
}
|
116
129
|
|
117
130
|
if type == 'nonce'
|
118
|
-
|
119
|
-
payload.merge!(nonce:
|
131
|
+
current_session[:link_nonce] = SecureRandom.hex
|
132
|
+
payload.merge!(nonce: current_session[:link_nonce])
|
120
133
|
elsif type == 'fixed_ip'
|
121
|
-
|
122
|
-
|
134
|
+
current_session[:remote_ip] ||= request.remote_ip
|
135
|
+
current_session[:last_ip_token_requested] = DateTime.now.iso8601
|
123
136
|
elsif type == 'expiring'
|
124
|
-
|
137
|
+
current_session[:last_token_requested] = DateTime.now.iso8601
|
125
138
|
else
|
126
139
|
raise StandardError, "Unsupported link_nonce_type: '#{type}'"
|
127
140
|
end
|
@@ -180,7 +193,7 @@ module PandaPal::Helpers
|
|
180
193
|
end
|
181
194
|
|
182
195
|
def monkeypatch_flash
|
183
|
-
if valid_session? && (value =
|
196
|
+
if valid_session? && (value = current_session['flashes']).present?
|
184
197
|
flashes = value["flashes"]
|
185
198
|
if discard = value["discard"]
|
186
199
|
flashes.except!(*discard)
|
@@ -192,7 +205,7 @@ module PandaPal::Helpers
|
|
192
205
|
yield
|
193
206
|
|
194
207
|
if @current_session.present?
|
195
|
-
|
208
|
+
current_session['flashes'] = flash.to_session_value
|
196
209
|
flash.discard()
|
197
210
|
end
|
198
211
|
end
|
data/lib/panda_pal/version.rb
CHANGED
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.
|
4
|
+
version: 5.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Instructure CustomDev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-03-
|
11
|
+
date: 2025-03-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|