panda_pal 5.3.4 → 5.3.13

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c598919f88d72cc23c02724fe9689b051ee82d553b8328e528253ec8e4f6622
4
- data.tar.gz: 24b76f1904796d1672cb060482f63fc76fa56d34e8b248aad9480ac01e21fa3c
3
+ metadata.gz: c8eb58ee2c4df4d91c08b20c3ea717a1227ebbc212500b6f5a0abad01855fd8c
4
+ data.tar.gz: ba0b60f6ee46634a554ac779b6cadf671edb922bc33201ac762de4fa9263ec45
5
5
  SHA512:
6
- metadata.gz: 75ea92798635fa208e51fc29d492c37fc8b75429b97a8e9e814e07b95e393d26aa03fe1599c42ed4a5dcbd1cb85b2d438a4d8a99ae17d885b7791c0458c4c749
7
- data.tar.gz: 2cabc475ece25dadd88da19d3667e80a0f2d94d875d85cf05b244d692b0438771140cf4b9cc9d355f46c46aeca98583cb96323d7ee58c81eed8074cc7a7b87b8
6
+ metadata.gz: 1337a89241f5a12bc0f5d202197f17f6ea4478be3e44786e68cd2b2161fc8df0cc2338418c4cdacbaa99cb1ec9e07a06446bac3a9a9a965285f35b4cbdce91cb
7
+ data.tar.gz: 83c6c892aad41adc5fd7ec2709dd8f5b5a70c238764b9caad989f5a2021ee92d81c64ac05e6ce7a3e72d321d2abfa8b1aba3b1298b2225fff4d476b57d4bd3c6
data/README.md CHANGED
@@ -21,6 +21,14 @@ PandaPal::stage_navigation(:account_navigation, {
21
21
 
22
22
  Configuration data for an installation can be set by creating a `PandaPal::Organization` record. Due to the nature of the data segregation, once created, the name of the organization should not be changed (and will raise a validation error).
23
23
 
24
+ ### LTI 1.3 Configuration
25
+ LTI 1.3 has some additional configuration steps required to setup an LTI:
26
+
27
+ 1. If you're running Canvas locally, make sure the `config/redis.yml` and `config/dynamic_settings.yml` files exist in Canvas.
28
+ 2. In prod, you'll need to generate a RSA Private Key for the LTI to use. You can set the `LTI_PRIVATE_KEY` ENV variable, or manually set `PandaPal.lti_private_key = OpenSSL::PKey::RSA.new(key)`.
29
+ 3. Make sure you have Redis installed and linked correctly
30
+ 4. Your PandaPal::Organization's `key` should be `CLIENT_ID/DEPLOYMENT_ID` (which can be found in Canvas). If a Deployment ID is not given, the key should just be `CLIENT_ID`.
31
+
24
32
  ### Launch URL property
25
33
  LTI Spec: `The launch_url contains the URL to which the LTI Launch is to be sent. The secure_launch_url is the URL to use if secure http is required. One of either the launch_url or the secure_launch_url must be specified.`
26
34
 
@@ -85,10 +93,22 @@ The following routes should be added to the routes.rb file of the implementing L
85
93
  ```ruby
86
94
  # config/routes.rb
87
95
  mount PandaPal::Engine, at: '/lti'
88
- lti_nav account_navigation: 'accounts#launch' # Use lti_nav to provide a custom Launch implementation, otherwise use the url: param of stage_navigation to let PandaPal handle launch.
89
96
  root to: 'panda_pal/lti#launch'
97
+
98
+ # Add Launch Endpoints:
99
+ lti_nav account_navigation: 'accounts#launch', auto_launch: false # (LTI <1.3 Default)
100
+ # -- OR --
101
+ scope '/organizations/:organization_id' do
102
+ lti_nav account_navigation: 'accounts#launch_landing', auto_launch: true # (LTI 1.3 Default)
103
+ lti_nav account_navigation: 'accounts#launch_landing' # Automatically sets auto_launch to true because :organization_id is part of the path
104
+ # ...
105
+ end
90
106
  ```
91
107
 
108
+ `auto_launch`: Setting to `true` will tell PandaPal to handle all of the launch details and session creation, and then pass off to
109
+ the defined action. Setting it to `false` indicates that the defined action handles launch validation and setup itself (this has been the legacy approach).
110
+ Because `auto_launch: false` is most similar to the previous behavior, it is the default for LTI 1.0/1.1 LTIs. For LTI 1.3 LTIs, `auto_launch: true` is the default. If not specified and `:organization_id` is detected in the Route Path, `auto_launch` will be set to `true`
111
+
92
112
  ## Implementating data segregation
93
113
  This engine uses Apartment to keep data segregated between installations of the implementing LTI tool.
94
114
  By default, it does this by inspecting the path of the request, and matching URLs containing `orgs` or `organizations`,
@@ -376,6 +396,24 @@ You will want to watch out for a few scenarios:
376
396
  link_to "Name", url_with_session(:somewhere_else_path, arg, kwarg: 1)
377
397
  ```
378
398
 
399
+ Persistent sessions have session_tokens as a way to safely communicate a session key in a way that is hopefully not too persistent in case it is logged somewhere.
400
+ Options for communicating session_token -
401
+ :nonce (default) - each nonce is good for exactly one communication with the backend server. Once the nonce is used, it is no longer valid.
402
+ :fixed_ip - each session_token is good until it expires. It must be used from the same ip the LTI launched from.
403
+ :expiring - this is the least secure. Each token is good until it expires.
404
+
405
+ For :fixed_ip and :expiring tokens you can override the default expiration period of 15 minutes.
406
+
407
+ See the following example of how to override the link_nonce_type and token expiration length.
408
+
409
+ class ApplicationController < ActionController::Base
410
+ link_nonce_type :fixed_ip
411
+ def session_expiration_period_minutes
412
+ 120
413
+ end
414
+ ...
415
+ end
416
+
379
417
  ### Previous Safari Instructions
380
418
  Safari is weird and you'll potentially run into issues getting `POST` requests to properly validate CSRF if you don't do the following:
381
419
 
@@ -2,6 +2,8 @@ require_dependency "panda_pal/application_controller"
2
2
 
3
3
  module PandaPal
4
4
  class LtiV1P0Controller < ApplicationController
5
+ skip_before_action :verify_authenticity_token
6
+
5
7
  def launch
6
8
  current_session_data.merge!({
7
9
  lti_version: 'v1p0',
@@ -2,6 +2,7 @@ require_dependency "panda_pal/application_controller"
2
2
 
3
3
  module PandaPal
4
4
  class LtiV1P3Controller < ApplicationController
5
+ skip_before_action :verify_authenticity_token
5
6
  before_action :validate_launch!, only: [:resource_link_request]
6
7
  around_action :switch_tenant, only: [:resource_link_request]
7
8
 
@@ -54,11 +55,15 @@ module PandaPal
54
55
  parsed_request_url = URI.parse(request_url)
55
56
 
56
57
  mapped_placements = PandaPal.lti_paths.map do |k, opts|
57
- opts = opts.dup
58
- opts.delete(:route_helper_key)
58
+ opts = LaunchUrlHelpers.normalize_lti_launch_desc(opts)
59
59
  opts.merge!({
60
60
  placement: k,
61
- target_link_uri: LaunchUrlHelpers.absolute_launch_url(k.to_sym, host: parsed_request_url, launch_handler: v1p3_resource_link_request_path),
61
+ target_link_uri: LaunchUrlHelpers.absolute_launch_url(
62
+ k.to_sym,
63
+ host: parsed_request_url,
64
+ launch_handler: v1p3_resource_link_request_path,
65
+ default_auto_launch: true
66
+ ),
62
67
  })
63
68
  opts
64
69
  end
@@ -85,8 +85,13 @@ module LtiXml
85
85
  end
86
86
 
87
87
  def ext_params(options, k)
88
- options.delete(:route_helper_key)
89
- options[:url] = PandaPal::LaunchUrlHelpers.absolute_launch_url(k.to_sym, host: parsed_request_url, launch_handler: nil)
88
+ options = PandaPal::LaunchUrlHelpers.normalize_lti_launch_desc(options)
89
+ options[:url] = PandaPal::LaunchUrlHelpers.absolute_launch_url(
90
+ k.to_sym,
91
+ host: parsed_request_url,
92
+ launch_handler: :v1p0_launch_path,
93
+ default_auto_launch: false
94
+ )
90
95
  options
91
96
  end
92
97
  end
@@ -0,0 +1,41 @@
1
+ module PandaPal
2
+ # An array that "processes" after so many items are added.
3
+ #
4
+ # Example Usage:
5
+ # batches = BatchProcessor.new(of: 1000) do |batch|
6
+ # # Process the batch somehow
7
+ # end
8
+ # enumerator_of_some_kind.each { |item| batches << item }
9
+ # batches.flush
10
+ class BatchProcessor
11
+ attr_reader :batch_size
12
+
13
+ def initialize(of: 1000, &blk)
14
+ @batch_size = of
15
+ @block = blk
16
+ @current_batch = []
17
+ end
18
+
19
+ def <<(item)
20
+ @current_batch << item
21
+ process_batch if @current_batch.count >= batch_size
22
+ end
23
+
24
+ def add_all(items)
25
+ items.each do |i|
26
+ self << i
27
+ end
28
+ end
29
+
30
+ def flush
31
+ process_batch if @current_batch.present?
32
+ end
33
+
34
+ protected
35
+
36
+ def process_batch
37
+ @block.call(@current_batch)
38
+ @current_batch = []
39
+ end
40
+ end
41
+ end
@@ -1,22 +1,29 @@
1
1
  module PandaPal
2
2
  module LaunchUrlHelpers
3
- def self.absolute_launch_url(launch_type, host:, launch_handler: nil)
3
+ def self.absolute_launch_url(launch_type, host:, launch_handler: nil, default_auto_launch: false)
4
4
  opts = PandaPal.lti_paths[launch_type]
5
- final_url = launch_url(opts, launch_type: launch_type)
5
+ auto_launch = opts[:auto_launch] != nil ? opts[:auto_launch] : default_auto_launch
6
+ auto_launch = auto_launch && launch_handler.present?
6
7
 
7
- is_direct = opts[:route_helper_key].present? || !launch_handler.present?
8
-
9
- if is_direct
10
- return final_url if URI.parse(final_url).absolute?
11
- return [host.to_s, final_url].join
12
- else
8
+ if auto_launch
13
9
  launch_handler = resolve_route(launch_handler) if launch_handler.is_a?(Symbol)
14
10
  return add_url_params([host.to_s, launch_handler].join, {
15
11
  launch_type: launch_type,
16
12
  })
13
+ else
14
+ final_url = launch_url(opts, launch_type: launch_type)
15
+ return final_url if URI.parse(final_url).absolute?
16
+ return [host.to_s, final_url].join
17
17
  end
18
18
  end
19
19
 
20
+ def self.normalize_lti_launch_desc(opts)
21
+ opts = opts.dup
22
+ opts.delete(:route_helper_key)
23
+ opts.delete(:auto_launch)
24
+ opts
25
+ end
26
+
20
27
  def self.launch_url(opts, launch_type: nil)
21
28
  url = launch_route(opts, launch_type: launch_type)
22
29
  url = resolve_url_symbol(url) if url.is_a?(Symbol)
@@ -8,8 +8,16 @@ module PandaPal
8
8
  end
9
9
 
10
10
  class_methods do
11
+ def define_setting(*args, &blk)
12
+ @_injected_settings_definitions ||= []
13
+ @_injected_settings_definitions << {
14
+ args: args,
15
+ block: blk,
16
+ }
17
+ end
18
+
11
19
  def settings_structure
12
- if PandaPal.lti_options&.[](:settings_structure).present?
20
+ struc = if PandaPal.lti_options&.[](:settings_structure).present?
13
21
  normalize_settings_structure(PandaPal.lti_options[:settings_structure])
14
22
  else
15
23
  {
@@ -18,6 +26,32 @@ module PandaPal
18
26
  properties: {},
19
27
  }
20
28
  end
29
+
30
+ (@_injected_settings_definitions || []).each do |sub|
31
+ args = [*sub[:args]]
32
+ path = args.shift || []
33
+ path = path.split('.') if path.is_a?(String)
34
+ path = Array(path)
35
+
36
+ if path.present?
37
+ key = path.pop
38
+
39
+ root = struc
40
+ path.each do |p|
41
+ root = root[:properties][p.to_sym]
42
+ end
43
+
44
+ if sub[:block]
45
+ root[:properties][key.to_sym] = sub[:block].call
46
+ else
47
+ root[:properties][key.to_sym] = args.shift
48
+ end
49
+ else
50
+ sub[:block].call(struc)
51
+ end
52
+ end
53
+
54
+ struc
21
55
  end
22
56
 
23
57
  def normalize_settings_structure(struc)
@@ -69,7 +103,7 @@ module PandaPal
69
103
  any_match = norm_types.any? do |t|
70
104
  if t == 'Boolean'
71
105
  settings == true || settings == false
72
- else
106
+ elsif t.is_a?(Class)
73
107
  settings.is_a?(t)
74
108
  end
75
109
  end
@@ -99,6 +133,20 @@ module PandaPal
99
133
  errors.concat(val_errors)
100
134
  end
101
135
 
136
+ if settings.is_a?(Array)
137
+ if spec[:length].is_a?(Range)
138
+ errors << "#{human_path} should contain #{spec[:length]} items" unless spec[:length].include?(settings.count)
139
+ elsif spec[:length].is_a?(Numeric)
140
+ errors << "#{human_path} should contain exactly #{spec[:length]} items" unless spec[:length] == settings.count
141
+ end
142
+
143
+ if spec[:item] != nil
144
+ settings.each_with_index do |value, i|
145
+ validate_settings_level(settings[i], spec[:item], path: [*path, i], errors: errors)
146
+ end
147
+ end
148
+ end
149
+
102
150
  if settings.is_a?(Hash)
103
151
  if spec[:properties] != nil
104
152
  spec[:properties].each do |key, pspec|
@@ -11,6 +11,31 @@ module PandaPal
11
11
  included do
12
12
  after_commit :sync_schedule, on: [:create, :update]
13
13
  after_commit :unschedule_tasks, on: :destroy
14
+
15
+ define_setting do |struc|
16
+ next unless _schedule_descriptors.present?
17
+
18
+ struc[:properties][:timezone] ||= {
19
+ type: 'String',
20
+ required: false,
21
+ validate: ->(timezone, *args) {
22
+ ActiveSupport::TimeZone[timezone].present? ? nil : "<path> Invalid Timezone '#{timezone}'"
23
+ },
24
+ }
25
+
26
+ struc[:properties][:task_schedules] = {
27
+ type: 'Hash',
28
+ required: false,
29
+ properties: _schedule_descriptors.keys.reduce({}) do |hash, k|
30
+ desc = _schedule_descriptors[k]
31
+
32
+ hash.tap do |hash|
33
+ kl = ' ' * (k.to_s.length - 4)
34
+ hash[k.to_sym] = hash[k.to_s] = PandaPal::OrganizationConcerns::TaskScheduling.build_settings_entry(desc)
35
+ end
36
+ end,
37
+ }
38
+ end
14
39
  end
15
40
 
16
41
  class_methods do
@@ -18,69 +43,6 @@ module PandaPal
18
43
  @_schedule_descriptors ||= {}
19
44
  end
20
45
 
21
- def settings_structure
22
- return super unless _schedule_descriptors.present?
23
-
24
- super.tap do |struc|
25
- struc[:properties] ||= {}
26
-
27
- struc[:properties][:timezone] ||= {
28
- type: 'String',
29
- required: false,
30
- validate: ->(timezone, *args) {
31
- ActiveSupport::TimeZone[timezone].present? ? nil : "<path> Invalid Timezone '#{timezone}'"
32
- },
33
- }
34
-
35
- struc[:properties][:task_schedules] = {
36
- type: 'Hash',
37
- required: false,
38
- properties: _schedule_descriptors.keys.reduce({}) do |hash, k|
39
- desc = _schedule_descriptors[k]
40
-
41
- hash.tap do |hash|
42
- kl = ' ' * (k.to_s.length - 4)
43
- hash[k.to_sym] = hash[k.to_s] = {
44
- required: false,
45
- description: <<~MARKDOWN,
46
- Override schedule for '#{k.to_s}' task.
47
-
48
- **Default**: #{desc[:schedule].is_a?(String) ? desc[:schedule] : '<Computed>'}
49
-
50
- Set to `false` to disable or supply a Cron string:
51
- ```yaml
52
- #{k.to_s}: 0 0 0 * * * America/Denver
53
- ##{kl} │ │ │ │ │ │ └── Timezone (Optional)
54
- ##{kl} │ │ │ │ │ └── Day of Week
55
- ##{kl} │ │ │ │ └── Month
56
- ##{kl} │ │ │ └── Day of Month
57
- ##{kl} │ │ └── Hour
58
- ##{kl} │ └── Minute
59
- ##{kl} └── Second (Optional)
60
- ````
61
- MARKDOWN
62
- json_schema: {
63
- oneOf: [
64
- { type: 'string', pattern: '^((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,6})(\w+\/\w+)?$' },
65
- { enum: [false] },
66
- ],
67
- default: desc[:schedule].is_a?(String) ? desc[:schedule] : '0 0 3 * * * America/Denver',
68
- },
69
- validate: ->(value, *args, errors:, **kwargs) {
70
- begin
71
- Rufus::Scheduler.parse(value) if value
72
- nil
73
- rescue ArgumentError
74
- errors << "<path> must be false or a Crontab string"
75
- end
76
- }
77
- }
78
- end
79
- end,
80
- }
81
- end
82
- end
83
-
84
46
  def scheduled_task(cron_time, name_or_method = nil, worker: nil, queue: nil, &block)
85
47
  task_key = (name_or_method.presence || "scheduled_task_#{caller_locations[0].lineno}").to_s
86
48
  raise "Task key '#{task_key}' already taken!" if _schedule_descriptors.key?(task_key)
@@ -137,6 +99,51 @@ module PandaPal
137
99
  end
138
100
  end
139
101
 
102
+ def self.build_settings_entry(desc)
103
+ k = desc[:key]
104
+ kl = ' ' * (k.to_s.length - 4)
105
+
106
+ default_schedule = '<Computed>'
107
+ default_schedule = desc[:schedule] if desc[:schedule].is_a?(String)
108
+ default_schedule = '<Disabled>' unless desc[:schedule].present?
109
+
110
+ {
111
+ required: false,
112
+ description: <<~MARKDOWN,
113
+ Override schedule for '#{k.to_s}' task.
114
+
115
+ **Default**: #{default_schedule}
116
+
117
+ Set to `false` to disable or supply a Cron string:
118
+ ```yaml
119
+ #{k.to_s}: 0 0 0 * * * America/Denver
120
+ ##{kl} │ │ │ │ │ │ └── Timezone (Optional)
121
+ ##{kl} │ │ │ │ │ └── Day of Week
122
+ ##{kl} │ │ │ │ └── Month
123
+ ##{kl} │ │ │ └── Day of Month
124
+ ##{kl} │ │ └── Hour
125
+ ##{kl} │ └── Minute
126
+ ##{kl} └── Second (Optional)
127
+ ````
128
+ MARKDOWN
129
+ json_schema: {
130
+ oneOf: [
131
+ { type: 'string', pattern: '^((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,6})(\w+\/\w+)?$' },
132
+ { enum: [false] },
133
+ ],
134
+ default: desc[:schedule].is_a?(String) ? desc[:schedule] : '0 0 3 * * * America/Denver',
135
+ },
136
+ validate: ->(value, *args, errors:, **kwargs) {
137
+ begin
138
+ Rufus::Scheduler.parse(value) if value
139
+ nil
140
+ rescue ArgumentError
141
+ errors << "<path> must be false or a Crontab string"
142
+ end
143
+ }
144
+ }
145
+ end
146
+
140
147
  private
141
148
 
142
149
  def unschedule_tasks(new_task_keys = nil)
@@ -157,13 +164,17 @@ module PandaPal
157
164
  return nil unless cron_time.present?
158
165
 
159
166
  cron_time = instance_exec(&cron_time) if cron_time.is_a?(Proc)
160
- if !Rufus::Scheduler.parse(cron_time).zone.present? && settings && settings[:timezone]
161
- cron_time += " #{settings[:timezone]}"
167
+ if !Rufus::Scheduler.parse(cron_time).zone.present? && settings && settings_timezone
168
+ cron_time += " #{settings_timezone}"
162
169
  end
163
170
 
164
171
  cron_time
165
172
  end
166
173
 
174
+ def settings_timezone
175
+ settings[:timezone] || settings.dig(:canvas, :root_account_timezone).presence || nil
176
+ end
177
+
167
178
  class ScheduledTaskExecutor
168
179
  include Sidekiq::Worker
169
180
 
@@ -10,8 +10,10 @@ Apartment.configure do |config|
10
10
  end
11
11
 
12
12
  Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request|
13
- if match = request.path.match(/\/(?:orgs|organizations)\/(\d+)/)
13
+ if match = request.path.match(/\/(?:orgs?|organizations?)\/(\d+)/)
14
14
  PandaPal::Organization.find_by(id: match[1]).try(:name)
15
+ elsif request.path.starts_with?('/rails/active_storage/blobs/')
16
+ PandaPal::Organization.find_by(id: request.params['organization_id']).try(:name)
15
17
  end
16
18
  }
17
19
 
data/config/routes.rb CHANGED
@@ -3,6 +3,7 @@ PandaPal::Engine.routes.draw do
3
3
 
4
4
  scope '/v1p0', as: 'v1p0' do
5
5
  get '/config' => 'lti_v1_p0#tool_config'
6
+ post '/launch' => 'lti_v1_p0#launch'
6
7
  end
7
8
 
8
9
  scope '/v1p3', as: 'v1p3' do
@@ -19,7 +19,6 @@ class RemoveOldOrganizationSettings < PandaPal::MiscHelper::MigrationClass
19
19
  #PandaPal::Organization.reset_column_information
20
20
  PandaPal::Organization.find_each do |o|
21
21
  # Would like to just be able to do this:
22
- # PandaPal::Organization.reset_column_information
23
22
  # o.settings = YAML.load(o.old_settings)
24
23
  # o.save!
25
24
  # but for some reason that is always making the settings null. Instead we will encrypt the settings manually.
@@ -24,6 +24,14 @@ module PandaPal
24
24
  end
25
25
  end
26
26
 
27
+ initializer 'Sidekiq Scheduler Hooks' do
28
+ ActiveSupport.on_load(:active_record) do
29
+ if Sidekiq.server? && PandaPal::Organization.respond_to?(:sync_schedules)
30
+ PandaPal::Organization.sync_schedules
31
+ end
32
+ end
33
+ end
34
+
27
35
  initializer 'panda_pal.app_controller' do |app|
28
36
  OAUTH_10_SUPPORT = true
29
37
  ActiveSupport.on_load(:action_controller) do
@@ -39,7 +39,11 @@ module PandaPal::Helpers
39
39
 
40
40
  def validate_v1p0_launch
41
41
  authorized = false
42
- if @organization = params['oauth_consumer_key'] && PandaPal::Organization.find_by_key(params['oauth_consumer_key'])
42
+ # We should verify the timestamp is recent (within 5 minutes). The approved timestamp is part of the signature,
43
+ # so we don't need to worry about malicious users messing with it. We should deny requests that come too long
44
+ # after the approved timestamp.
45
+ good_timestamp = params['oauth_timestamp'] && params['oauth_timestamp'].to_i > Time.now.to_i - 300
46
+ if @organization = good_timestamp && params['oauth_consumer_key'] && PandaPal::Organization.find_by_key(params['oauth_consumer_key'])
43
47
  sanitized_params = request.request_parameters
44
48
  # These params come over with a safari-workaround launch. The authenticator doesn't like them, so clean them out.
45
49
  safe_unexpected_params = ["full_win_launch_requested", "platform_redirect_url", "dummy_param"]
@@ -62,7 +66,7 @@ module PandaPal::Helpers
62
66
  raise JSON::JWT::VerificationFailed, 'error decoding id_token' if decoded_jwt.blank?
63
67
 
64
68
  client_id = decoded_jwt['aud']
65
- @organization = PandaPal::Organization.find_by!(key: 'PandaPal') # client_id)
69
+ @organization = PandaPal::Organization.find_by!(key: client_id)
66
70
  raise JSON::JWT::VerificationFailed, 'Unrecognized Organization' unless @organization.present?
67
71
 
68
72
  decoded_jwt.verify!(current_lti_platform.public_jwks)
@@ -127,7 +131,8 @@ module PandaPal::Helpers
127
131
 
128
132
  def organization_key
129
133
  org_key ||= params[:oauth_consumer_key]
130
- org_key ||= "#{params[:client_id]}/#{params[:deployment_id]}" if params[:client_id].present?
134
+ org_key ||= "#{params[:client_id]}/#{params[:deployment_id]}" if params[:client_id].present? && params[:deployment_id].present?
135
+ org_key ||= params[:client_id] if params[:client_id].present?
131
136
  org_key ||= session[:organization_key]
132
137
  org_key
133
138
  end
@@ -9,6 +9,12 @@ module PandaPal::Helpers::RouteHelper
9
9
  path = "#{base_path}/#{nav.to_s}"
10
10
 
11
11
  lti_options = options.delete(:lti_options) || {}
12
+ lti_options[:auto_launch] = options.delete(:auto_launch)
13
+
14
+ if lti_options[:auto_launch].nil?
15
+ lti_options[:auto_launch] = (@scope[:path] || '').include?(':organization_id')
16
+ end
17
+
12
18
  lti_options[:route_helper_key] = path.split('/').reject(&:empty?).join('_')
13
19
  post(path, options.dup, &block)
14
20
  get(path, options.dup, &block)
@@ -32,13 +32,14 @@ module PandaPal::Helpers
32
32
  if params[:session_token]
33
33
  payload = JSON.parse(session_cryptor.decrypt_and_verify(params[:session_token])).with_indifferent_access
34
34
  matched_session = find_or_create_session(key: payload[:session_key])
35
-
36
35
  if matched_session.present?
37
36
  if payload[:token_type] == 'nonce' && matched_session.data[:link_nonce] == payload[:nonce]
38
37
  @current_session = matched_session
39
38
  @current_session.data[:link_nonce] = nil
40
39
  elsif payload[:token_type] == 'fixed_ip' && matched_session.data[:remote_ip] == request.remote_ip &&
41
- DateTime.parse(matched_session.data[:last_ip_token_requested]) > 15.minutes.ago
40
+ DateTime.parse(matched_session.data[:last_ip_token_requested]) > session_expiration_period_minutes.minutes.ago
41
+ @current_session = matched_session
42
+ elsif payload[:token_type] == 'expiring' && DateTime.parse(matched_session.data[:last_token_requested]) > session_expiration_period_minutes.minutes.ago
42
43
  @current_session = matched_session
43
44
  end
44
45
  end
@@ -111,6 +112,8 @@ module PandaPal::Helpers
111
112
  elsif type == 'fixed_ip'
112
113
  current_session_data[:remote_ip] ||= request.remote_ip
113
114
  current_session_data[:last_ip_token_requested] = DateTime.now.iso8601
115
+ elsif type == 'expiring'
116
+ current_session_data[:last_token_requested] = DateTime.now.iso8601
114
117
  else
115
118
  raise StandardError, "Unsupported link_nonce_type: '#{type}'"
116
119
  end
@@ -123,6 +126,10 @@ module PandaPal::Helpers
123
126
  self.class.link_nonce_type
124
127
  end
125
128
 
129
+ def session_expiration_period_minutes
130
+ 15
131
+ end
132
+
126
133
  private
127
134
 
128
135
  def session_cryptor
@@ -1,3 +1,3 @@
1
1
  module PandaPal
2
- VERSION = "5.3.4"
2
+ VERSION = "5.3.13"
3
3
  end
data/lib/panda_pal.rb CHANGED
@@ -81,6 +81,7 @@ module PandaPal
81
81
 
82
82
  def self.validate_lti_navigation(errors = [])
83
83
  @@lti_navigation.each do |k, v|
84
+ next if v[:route_helper_key]
84
85
  errors << "lti navigation '#{k}' does not have a Route!" unless (LaunchUrlHelpers.launch_url(k) rescue nil)
85
86
  end
86
87
  errors
data/panda_pal.gemspec CHANGED
@@ -23,6 +23,7 @@ Gem::Specification.new do |s|
23
23
  s.add_dependency 'attr_encrypted', '~> 3.0.0'
24
24
  s.add_dependency 'secure_headers', '~> 6.1'
25
25
  s.add_dependency 'json-jwt'
26
+ s.add_dependency 'httparty'
26
27
 
27
28
  s.add_development_dependency 'sidekiq'
28
29
  s.add_development_dependency 'sidekiq-scheduler'