panda_pal 5.3.4 → 5.3.13
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 +39 -1
- data/app/controllers/panda_pal/lti_v1_p0_controller.rb +2 -0
- data/app/controllers/panda_pal/lti_v1_p3_controller.rb +8 -3
- data/app/lib/lti_xml/base_platform.rb +7 -2
- data/app/lib/panda_pal/batch_processor.rb +41 -0
- data/app/lib/panda_pal/launch_url_helpers.rb +15 -8
- data/app/models/panda_pal/organization_concerns/settings_validation.rb +50 -2
- data/app/models/panda_pal/organization_concerns/task_scheduling.rb +76 -65
- data/config/initializers/apartment.rb +3 -1
- data/config/routes.rb +1 -0
- data/db/migrate/20171205194657_remove_old_organization_settings.rb +0 -1
- data/lib/panda_pal/engine.rb +8 -0
- data/lib/panda_pal/helpers/controller_helper.rb +8 -3
- data/lib/panda_pal/helpers/route_helper.rb +6 -0
- data/lib/panda_pal/helpers/session_replacement.rb +9 -2
- data/lib/panda_pal/version.rb +1 -1
- data/lib/panda_pal.rb +1 -0
- data/panda_pal.gemspec +1 -0
- data/spec/dummy/log/development.log +40 -15042
- data/spec/dummy/log/test.log +83348 -0
- metadata +22 -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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c8eb58ee2c4df4d91c08b20c3ea717a1227ebbc212500b6f5a0abad01855fd8c
|
4
|
+
data.tar.gz: ba0b60f6ee46634a554ac779b6cadf671edb922bc33201ac762de4fa9263ec45
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,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
|
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(
|
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.
|
89
|
-
options[:url] = PandaPal::LaunchUrlHelpers.absolute_launch_url(
|
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
|
-
|
5
|
+
auto_launch = opts[:auto_launch] != nil ? opts[:auto_launch] : default_auto_launch
|
6
|
+
auto_launch = auto_launch && launch_handler.present?
|
6
7
|
|
7
|
-
|
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
|
-
|
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 &&
|
161
|
-
cron_time += " #{
|
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
|
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
@@ -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.
|
data/lib/panda_pal/engine.rb
CHANGED
@@ -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
|
-
|
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:
|
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]) >
|
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
|
data/lib/panda_pal/version.rb
CHANGED
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'
|