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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8f1101fcff4fef0cb7d541ab3bab4cc55073e8c31ed1605990efe8a561c389b
4
- data.tar.gz: aa35bbe30ae591094643f584b2840d5ff0404cc7655882f0f436761ba82bc9e1
3
+ metadata.gz: bde41047ef682bd599b2ee621fdb050dc3d4269eca64272f1415c667dbbbffe2
4
+ data.tar.gz: 9a3cb2ccab6721ceae849e29a7080685575578997fa310e0f912c2cb2888a4ea
5
5
  SHA512:
6
- metadata.gz: 2d22eeeef183b3f16c3000a312e57308fc432b558096f69c5ee63df9fafb3d60c09d5b253f3675257e7539a9e5ca54c464954be6e753058c6e6a01d4f765ccdb
7
- data.tar.gz: 466e22660f22fe7d79a86d46576f1e0704d35b88332324b1f444e5b4d892c758d9331628d05153bd521489275f08d4ed00f6b12799bc491809fe791d5a784f39
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
- switch_tenant(org.name) do
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
- current_session_data[:canvas_user_id] = params[:custom_canvas_user_id]
567
- current_session_data[:canvas_course_id] = params[:custom_canvas_course_id]
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
- current_session_data.merge!({
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 :validate_launch!, only: [:resource_link_request]
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
- current_session_data[:lti_platform] = @current_lti_platform&.serialize
22
- current_session_data[:lti_oauth_nonce] = SecureRandom.uuid
23
- current_session_data[:canvas_environment] = params['canvas_environment']
24
- current_session_data[:canvas_region] = params['canvas_region']
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: current_session_data[:lti_oauth_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
- current_session_data.merge!({
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 = current_session_data[:canvas_environment]
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.Rails.application.credentials.secret_key_base if Rails.version > "7.0"
84
- secret_key_base ||= Rails.Rails.application.secrets.secret_key_base if Rails.version < "7.2"
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
- Apartment::Tenant.switch(org.name) do
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(:name)
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(:name)
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(:name)
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.current_session(create_missing: false) unless defined?(@panda_pal_session)
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
@@ -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 block_given?
62
- Apartment::Tenant.switch(tenant, &block)
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
- Apartment::Tenant.switch!(tenant)
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.name}'"
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 ||= 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)
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 ||= current_session(create_missing: false)&.lti_platform
17
+ @current_lti_platform ||= current_panda_session&.lti_platform
17
18
  end
18
19
 
19
20
  def lti_launch_params
20
- current_session_data[:launch_params]
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 params[:id_token].present?
27
- validate_v1p3_launch
28
- elsif params[:oauth_consumer_key].present?
29
- validate_v1p0_launch
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: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
62
+ render plain: 'Failed to validate LTI v1p0 launch', :status => :unauthorized
63
+ return false
52
64
  end
53
65
 
54
- authorized
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 current_session_data[:lti_oauth_nonce] == decoded_jwt['nonce']
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
- current_session.update(panda_pal_organization: @organization)
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
- Apartment::Tenant.switch(organization.name) do
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 current_session(create_missing: false)&.persisted?
130
+ return false unless current_panda_session&.persisted?
121
131
  return false unless current_organization
122
- return false unless current_session.panda_pal_organization_id == current_organization.id
123
- return false unless Apartment::Tenant.current == current_organization.name
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
- def find_or_create_session(key:)
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 current_session(create_missing: true)
37
- return @current_session if @current_session.present?
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 = find_or_create_session(key: payload[:session_key])
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 = find_or_create_session(key: session_key)
60
+ @current_session = PandaPal::Session.find_by(session_key: session_key)
56
61
  end
62
+ end
57
63
 
58
- @current_session ||= find_or_create_session(key: :create) if create_missing
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
- current_session_data[:link_nonce] = SecureRandom.hex
119
- payload.merge!(nonce: current_session_data[:link_nonce])
131
+ current_session[:link_nonce] = SecureRandom.hex
132
+ payload.merge!(nonce: current_session[:link_nonce])
120
133
  elsif type == 'fixed_ip'
121
- current_session_data[:remote_ip] ||= request.remote_ip
122
- current_session_data[:last_ip_token_requested] = DateTime.now.iso8601
134
+ current_session[:remote_ip] ||= request.remote_ip
135
+ current_session[:last_ip_token_requested] = DateTime.now.iso8601
123
136
  elsif type == 'expiring'
124
- current_session_data[:last_token_requested] = DateTime.now.iso8601
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 = current_session_data['flashes']).present?
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
- current_session_data['flashes'] = flash.to_session_value
208
+ current_session['flashes'] = flash.to_session_value
196
209
  flash.discard()
197
210
  end
198
211
  end
@@ -1,3 +1,3 @@
1
1
  module PandaPal
2
- VERSION = "5.12.9"
2
+ VERSION = "5.13.0"
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.12.9
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 00:00:00.000000000 Z
11
+ date: 2025-03-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails