panda_pal 5.12.9 → 5.13.1.beta1
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/app/models/panda_pal/session.rb +3 -2
- 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 +28 -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: 1974ddd7a57543013591382a72540dd295abd082e9133a1742d56e550f469efd
|
4
|
+
data.tar.gz: e27eba365f15c39ca52a0cb4092131bb1ef237f293dde66bce26836023b7fd3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 386bff58eea08c7cacc07891fb614735b251fd63a47a606be9615ea1d103bb73b6be8a4f012a730dcf3b70e35be2a9aa7172672e2f6d3a1ff8fe95900fdecff4
|
7
|
+
data.tar.gz: 2fa5a83a6a47303bdf968174277236a987f47ecee66a5b10a6e62939abfb58b5b7c17ae390b68150873ed1d4dd9e1ba679789d91afc71801a8b7ca90d114043d
|
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)
|
@@ -4,6 +4,7 @@ module PandaPal
|
|
4
4
|
|
5
5
|
after_initialize do
|
6
6
|
self.session_key ||= SecureRandom.urlsafe_base64(60)
|
7
|
+
self.data ||= {}.with_indifferent_access
|
7
8
|
end
|
8
9
|
|
9
10
|
def [](key)
|
@@ -190,7 +191,7 @@ module PandaPal
|
|
190
191
|
|
191
192
|
class DataSerializer
|
192
193
|
def self.load(str)
|
193
|
-
return {} unless str.present?
|
194
|
+
return {}.with_indifferent_access unless str.present?
|
194
195
|
begin
|
195
196
|
parsed = JSON.parse(str)
|
196
197
|
rescue JSON::ParserError
|
@@ -216,7 +217,7 @@ module PandaPal
|
|
216
217
|
end
|
217
218
|
end
|
218
219
|
|
219
|
-
if Rails.version > '7.0'
|
220
|
+
if Rails.version > '7.0'x
|
220
221
|
serialize :data, coder: DataSerializer
|
221
222
|
else
|
222
223
|
serialize :data, DataSerializer
|
@@ -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,25 @@ 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
|
57
62
|
|
58
|
-
@current_session
|
63
|
+
@current_session
|
64
|
+
end
|
65
|
+
|
66
|
+
# @deprecated - User current_panda_session instead
|
67
|
+
def current_session(create_missing: false)
|
68
|
+
current_panda_session
|
69
|
+
|
70
|
+
if !@current_session && create_missing
|
71
|
+
Rails.logger.warn("current_session(create_missing: true) is deprecated. Use start_panda_session! instead.") unless Rails.env.production?
|
72
|
+
start_panda_session!
|
73
|
+
end
|
59
74
|
|
60
75
|
@current_session
|
61
76
|
end
|
62
77
|
|
63
|
-
# @deprecated
|
78
|
+
# @deprecated - Use current_session[:key] directly
|
64
79
|
def current_session_data
|
65
80
|
current_session.data
|
66
81
|
end
|
@@ -115,13 +130,13 @@ module PandaPal::Helpers
|
|
115
130
|
}
|
116
131
|
|
117
132
|
if type == 'nonce'
|
118
|
-
|
119
|
-
payload.merge!(nonce:
|
133
|
+
current_session[:link_nonce] = SecureRandom.hex
|
134
|
+
payload.merge!(nonce: current_session[:link_nonce])
|
120
135
|
elsif type == 'fixed_ip'
|
121
|
-
|
122
|
-
|
136
|
+
current_session[:remote_ip] ||= request.remote_ip
|
137
|
+
current_session[:last_ip_token_requested] = DateTime.now.iso8601
|
123
138
|
elsif type == 'expiring'
|
124
|
-
|
139
|
+
current_session[:last_token_requested] = DateTime.now.iso8601
|
125
140
|
else
|
126
141
|
raise StandardError, "Unsupported link_nonce_type: '#{type}'"
|
127
142
|
end
|
@@ -180,7 +195,7 @@ module PandaPal::Helpers
|
|
180
195
|
end
|
181
196
|
|
182
197
|
def monkeypatch_flash
|
183
|
-
if valid_session? && (value =
|
198
|
+
if valid_session? && (value = current_session['flashes']).present?
|
184
199
|
flashes = value["flashes"]
|
185
200
|
if discard = value["discard"]
|
186
201
|
flashes.except!(*discard)
|
@@ -192,7 +207,7 @@ module PandaPal::Helpers
|
|
192
207
|
yield
|
193
208
|
|
194
209
|
if @current_session.present?
|
195
|
-
|
210
|
+
current_session['flashes'] = flash.to_session_value
|
196
211
|
flash.discard()
|
197
212
|
end
|
198
213
|
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.1.beta1
|
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
|