panda_pal 5.7.0.beta2 → 5.8.1
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 +1 -1
- data/app/controllers/panda_pal/lti_v1_p3_controller.rb +38 -5
- data/app/models/panda_pal/organization.rb +27 -0
- data/app/models/panda_pal/platform/canvas.rb +56 -11
- data/app/models/panda_pal/platform.rb +37 -12
- data/app/models/panda_pal/session.rb +1 -1
- data/lib/panda_pal/engine.rb +40 -11
- data/lib/panda_pal/helpers/console_helpers.rb +27 -0
- data/lib/panda_pal/helpers/controller_helper.rb +3 -2
- data/lib/panda_pal/version.rb +1 -1
- data/spec/dummy/log/test.log +16378 -0
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 732dbb8785b96326d0d10cd6e602cbecaffe5cb4c6ee8b1b6ce7722cb5611ea5
|
4
|
+
data.tar.gz: 8ed7ae8382b0a7cd6534f5bc75bf403d9666b7fdce00d6b6f80c79bc68c2721f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f55395d5b346c6f9a5c9d6f31716ed3b9ab2aa616cb46ff7e31b5b184703d8d087ce4b47b536d736a5e22e0da0d5ccf14bbc3e0d4cead243487ddb303a24d24
|
7
|
+
data.tar.gz: bd19857f79323ab6fa4f50c79e66bddf436a54344e02dab3794a95ee8cc2d54d0566ce764ddd42c88c838702279d4b4907a122a552ebf5f75f04e5438410d54f
|
data/README.md
CHANGED
@@ -27,7 +27,7 @@ As of version 5.5.0, LTIs can be installed into Canvas via the console:
|
|
27
27
|
org.install_lti(
|
28
28
|
host: "https://your_lti.herokuapp.com",
|
29
29
|
context: "account/self", # (Optional) Or "account/3", "course/1", etc
|
30
|
-
exists: :error, # (Optional) Action to take if an LTI with the same Key already exists. Options are :error, :replace, :duplicate
|
30
|
+
exists: :error, # (Optional) Action to take if an LTI with the same Key already exists. Options are :error, :replace, :duplicate, :update
|
31
31
|
version: "v1p3", # (Optional, default `v1p3`) LTI Version. Accepts `v1p0` or `v1p3`.
|
32
32
|
dedicated_deployment: false, # (Optional) If true, the Organization will be updated to link to a single deployment rather then to the general LTI Key. (experimental, LTI 1.3 only)
|
33
33
|
)
|
@@ -11,6 +11,9 @@ module PandaPal
|
|
11
11
|
before_action :validate_launch!, only: [:resource_link_request]
|
12
12
|
around_action :switch_tenant, only: [:resource_link_request]
|
13
13
|
|
14
|
+
# Redirect to beta/test as necessary
|
15
|
+
before_action :forward_to_env_and_region, only: [:login]
|
16
|
+
|
14
17
|
def login
|
15
18
|
@current_lti_platform = PandaPal::Platform.resolve_platform(params)
|
16
19
|
|
@@ -35,16 +38,16 @@ module PandaPal
|
|
35
38
|
end
|
36
39
|
|
37
40
|
def resource_link_request
|
38
|
-
|
41
|
+
ltype = @decoded_lti_jwt['https://www.instructure.com/placement']
|
39
42
|
|
40
|
-
if
|
43
|
+
if ltype
|
41
44
|
current_session_data.merge!({
|
42
45
|
lti_version: 'v1p3',
|
43
|
-
lti_launch_placement:
|
46
|
+
lti_launch_placement: ltype,
|
44
47
|
launch_params: @decoded_lti_jwt,
|
45
48
|
})
|
46
49
|
|
47
|
-
redirect_with_session_to(:"#{LaunchUrlHelpers.launch_route(
|
50
|
+
redirect_with_session_to(:"#{LaunchUrlHelpers.launch_route(ltype)}_url", route_context: main_app)
|
48
51
|
end
|
49
52
|
# render json: {
|
50
53
|
# launch_type: params[:launch_type],
|
@@ -90,7 +93,6 @@ module PandaPal
|
|
90
93
|
privacy_level: "public",
|
91
94
|
settings: {
|
92
95
|
placements: mapped_placements,
|
93
|
-
environments: PandaPal.lti_environments,
|
94
96
|
},
|
95
97
|
}],
|
96
98
|
custom_fields: PandaPal.lti_custom_params, # PandaPal.lti_options[:custom_fields],
|
@@ -105,6 +107,37 @@ module PandaPal
|
|
105
107
|
}
|
106
108
|
end
|
107
109
|
|
110
|
+
protected
|
111
|
+
|
112
|
+
def forward_to_env_and_region
|
113
|
+
login_redirects = params['lti_login_redirects'] || []
|
114
|
+
login_redirects << request.url
|
115
|
+
|
116
|
+
redir_url = environment_login_url
|
117
|
+
|
118
|
+
if redir_url.present? && !login_redirects.include?(redir_url)
|
119
|
+
@form_action = redir_url
|
120
|
+
@method = request.method.downcase
|
121
|
+
@form_data = (request.POST.presence || request.GET).merge({
|
122
|
+
lti_login_redirects: login_redirects,
|
123
|
+
})
|
124
|
+
|
125
|
+
render action: :login
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def environment_login_url
|
130
|
+
if (canvas_env = params['canvas_environment']).present?
|
131
|
+
tdomain = PandaPal.lti_environments[:"#{canvas_env}_domain"]
|
132
|
+
|
133
|
+
if tdomain.present? && !request.url.include?(tdomain)
|
134
|
+
return v1p3_oidc_login_url.gsub(PandaPal.lti_environments[:domain], tdomain)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
nil
|
139
|
+
end
|
140
|
+
|
108
141
|
private
|
109
142
|
|
110
143
|
def auth_redirect_query
|
@@ -58,6 +58,12 @@ module PandaPal
|
|
58
58
|
after_create :create_schema
|
59
59
|
after_commit :destroy_schema, on: :destroy
|
60
60
|
|
61
|
+
define_setting("lti", {
|
62
|
+
type: 'Hash',
|
63
|
+
required: false,
|
64
|
+
properties: {},
|
65
|
+
})
|
66
|
+
|
61
67
|
if defined?(scheduled_task)
|
62
68
|
scheduled_task '0 0 3 * * *', :clean_old_sessions do
|
63
69
|
PandaPal::Session.where(panda_pal_organization: self).where('updated_at < ?', 1.week.ago).delete_all
|
@@ -152,11 +158,32 @@ module PandaPal
|
|
152
158
|
#
|
153
159
|
# The API is currently an Organization subclass (using `becomes()`), but such may change.
|
154
160
|
def platform_api(platform_type = primary_platform)
|
161
|
+
return nil if platform_type.nil?
|
155
162
|
scl = platform_type.organization_api
|
156
163
|
return self if scl == self.class
|
157
164
|
becomes(scl)
|
158
165
|
end
|
159
166
|
|
167
|
+
define_setting("lti.trusted_platforms", {
|
168
|
+
type: 'Array',
|
169
|
+
required: false,
|
170
|
+
description: "Additional trusted JWT issuers when validating the LTI 1.3 OIDC handshake",
|
171
|
+
})
|
172
|
+
|
173
|
+
# Determines if the specified platform can create sessions in this organization
|
174
|
+
def trusted_platform?(platform)
|
175
|
+
# Trust Instructure-hosted Canvas
|
176
|
+
return true if platform.is_a?(PandaPal::Platform::Canvas) && platform.is_trusted_env?
|
177
|
+
|
178
|
+
# Trust issuers added to the Org settings
|
179
|
+
if (issuer = platform.platform_uri rescue nil).present?
|
180
|
+
trusted_issuers = settings.dig(:lti, :trusted_platforms) || []
|
181
|
+
return true if trusted_issuers.include?(issuer)
|
182
|
+
end
|
183
|
+
|
184
|
+
false
|
185
|
+
end
|
186
|
+
|
160
187
|
protected
|
161
188
|
|
162
189
|
# OrgExtension-Overridable method to allow multi-platform tool Orgs to implicitly include a Platform API
|
@@ -5,24 +5,57 @@ end
|
|
5
5
|
|
6
6
|
module PandaPal
|
7
7
|
class Platform::Canvas < Platform
|
8
|
-
|
8
|
+
TRUSTED_ISSUERS = [
|
9
|
+
"https://sso.canvaslms.com",
|
10
|
+
"https://sso.beta.canvaslms.com",
|
11
|
+
"https://sso.test.canvaslms.com",
|
9
12
|
|
10
|
-
|
13
|
+
# Deprecated (but still secure):
|
11
14
|
"https://canvas.instructure.com",
|
12
15
|
"https://canvas.beta.instructure.com",
|
13
16
|
"https://canvas.test.instructure.com",
|
14
17
|
]
|
15
18
|
|
19
|
+
def initialize(options)
|
20
|
+
@issuer = options[:iss]
|
21
|
+
end
|
22
|
+
|
23
|
+
def platform_uri
|
24
|
+
@issuer
|
25
|
+
end
|
26
|
+
|
16
27
|
def jwks_url
|
17
|
-
"#{
|
28
|
+
"#{lti_api_domain}/api/lti/security/jwks"
|
18
29
|
end
|
19
30
|
|
20
31
|
def authentication_redirect_url
|
21
|
-
"#{
|
32
|
+
"#{lti_api_domain}/api/lti/authorize_redirect"
|
22
33
|
end
|
23
34
|
|
24
35
|
def grant_url
|
25
|
-
"#{
|
36
|
+
"#{lti_api_domain}/login/oauth2/token"
|
37
|
+
end
|
38
|
+
|
39
|
+
def is_trusted_env?
|
40
|
+
return true unless Rails.env.production?
|
41
|
+
|
42
|
+
TRUSTED_ISSUERS.include?(platform_uri)
|
43
|
+
end
|
44
|
+
|
45
|
+
def serialize
|
46
|
+
super.merge(iss: @issuer)
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def lti_api_domain
|
52
|
+
case @issuer
|
53
|
+
when "https://canvas.instructure.com"; "https://sso.canvaslms.com"
|
54
|
+
when "https://canvas.beta.instructure.com"; "https://sso.beta.canvaslms.com"
|
55
|
+
when "https://canvas.test.instructure.com"; "https://sso.test.canvaslms.com"
|
56
|
+
else
|
57
|
+
@issuer
|
58
|
+
end
|
26
59
|
end
|
27
60
|
|
28
61
|
module OrgExtension
|
@@ -44,13 +77,11 @@ module PandaPal
|
|
44
77
|
def install_lti(host: nil, context: :root_account, version: 'v1p3', exists: :error, dedicated_deployment: false)
|
45
78
|
raise "Automatically installing this LTI requires Bearcat." unless defined?(Bearcat)
|
46
79
|
|
47
|
-
version = version.to_s
|
48
|
-
|
49
80
|
run_callbacks :lti_install do
|
50
81
|
ctype, cid = _parse_lti_context(context)
|
51
82
|
|
52
83
|
if version == 'v1p0'
|
53
|
-
existing_installs = _find_existing_installs(
|
84
|
+
existing_installs = _find_existing_installs(exists: exists) do |lti|
|
54
85
|
lti[:consumer_key] == self.key
|
55
86
|
end
|
56
87
|
|
@@ -116,14 +147,28 @@ module PandaPal
|
|
116
147
|
lti_json_url = PandaPal::LaunchUrlHelpers.resolve_route(:v1p3_config_url, host: host)
|
117
148
|
lti_json = JSON.parse(HTTParty.get(lti_json_url, format: :plain).body)
|
118
149
|
|
150
|
+
valid_redirect_uris = [
|
151
|
+
lti_json["target_link_uri"]
|
152
|
+
]
|
153
|
+
|
154
|
+
prod_domain = PandaPal.lti_environments[:domain]
|
155
|
+
if prod_domain.present?
|
156
|
+
PandaPal.lti_environments.each do |env, domain|
|
157
|
+
env = env.to_s
|
158
|
+
next unless env.endswith?("_domain")
|
159
|
+
env = env.split('_')[0]
|
160
|
+
valid_redirect_uris << lti_json["target_link_uri"].gsub(prod_domain, domain)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
valid_redirect_uris.uniq!
|
165
|
+
|
119
166
|
if !ekey
|
120
167
|
# Create New
|
121
168
|
ekey = bearcat_client.post("api/lti/accounts/self/developer_keys/tool_configuration", {
|
122
169
|
developer_key: {
|
123
170
|
name: PandaPal.lti_options[:title],
|
124
|
-
redirect_uris:
|
125
|
-
lti_json["target_link_uri"],
|
126
|
-
].join("\n")
|
171
|
+
redirect_uris: valid_redirect_uris.join("\n"),
|
127
172
|
},
|
128
173
|
tool_configuration: {
|
129
174
|
settings: lti_json,
|
@@ -5,10 +5,6 @@ module PandaPal
|
|
5
5
|
class Platform
|
6
6
|
require_relative "platform/canvas"
|
7
7
|
|
8
|
-
def initialize(params)
|
9
|
-
@issuer = params[:iss]
|
10
|
-
end
|
11
|
-
|
12
8
|
def public_jwks
|
13
9
|
require "json/jwt"
|
14
10
|
|
@@ -27,14 +23,18 @@ module PandaPal
|
|
27
23
|
nil
|
28
24
|
end
|
29
25
|
|
26
|
+
def self.from_serialized(ser)
|
27
|
+
cls = ser[:platform_class].safe_constantize
|
28
|
+
cls.deserialize(ser)
|
29
|
+
end
|
30
|
+
|
30
31
|
def self.deserialize(params)
|
31
|
-
|
32
|
+
new(params)
|
32
33
|
end
|
33
34
|
|
34
35
|
def serialize
|
35
36
|
{
|
36
37
|
platform_class: self.class.to_s,
|
37
|
-
iss: @issuer,
|
38
38
|
}
|
39
39
|
end
|
40
40
|
|
@@ -67,7 +67,7 @@ module PandaPal
|
|
67
67
|
end
|
68
68
|
|
69
69
|
# TODO Default logic?
|
70
|
-
return Platform::Canvas if Platform::Canvas::
|
70
|
+
return Platform::Canvas if Platform::Canvas::TRUSTED_ISSUERS.include?(params[:iss])
|
71
71
|
|
72
72
|
raise "Unknown platform '#{platform}'"
|
73
73
|
end
|
@@ -96,12 +96,37 @@ module PandaPal
|
|
96
96
|
end
|
97
97
|
|
98
98
|
class Platform::Generic < Platform
|
99
|
-
|
99
|
+
def self.from_urls(base_url, jwks: nil, auth_redirect: nil, grant: nil)
|
100
|
+
new({
|
101
|
+
base_url: base_url,
|
102
|
+
jwks_url: jwks,
|
103
|
+
auth_redirect_url: auth_redirect,
|
104
|
+
grant_url: grant,
|
105
|
+
})
|
106
|
+
end
|
107
|
+
|
108
|
+
def initialize(options)
|
109
|
+
@options = options
|
110
|
+
end
|
111
|
+
|
112
|
+
def platform_uri
|
113
|
+
options[:issuer] || options[:base_url]
|
114
|
+
end
|
115
|
+
|
116
|
+
def jwks_url
|
117
|
+
URI.join(options[:base_url], options[:jwks_url] || "/api/lti/security/jwks").to_s
|
118
|
+
end
|
119
|
+
|
120
|
+
def authentication_redirect_url
|
121
|
+
URI.join(options[:base_url], options[:auth_redirect_url] || "/api/lti/authorize_redirect").to_s
|
122
|
+
end
|
123
|
+
|
124
|
+
def grant_url
|
125
|
+
URI.join(options[:base_url], options[:grant_url] || "/login/oauth2/token").to_s
|
126
|
+
end
|
100
127
|
|
101
|
-
def
|
102
|
-
|
103
|
-
@authentication_redirect_url = URI.join(base_url, auth_redirect).to_s
|
104
|
-
@grant_url = URI.join(base_url, grant).to_s
|
128
|
+
def serialize
|
129
|
+
super.merge(options)
|
105
130
|
end
|
106
131
|
end
|
107
132
|
end
|
data/lib/panda_pal/engine.rb
CHANGED
@@ -3,6 +3,8 @@ require 'ims/lti'
|
|
3
3
|
require 'attr_encrypted'
|
4
4
|
require 'secure_headers'
|
5
5
|
|
6
|
+
require_relative './helpers/console_helpers'
|
7
|
+
|
6
8
|
module PandaPal
|
7
9
|
class Engine < ::Rails::Engine
|
8
10
|
config.autoload_once_paths += Dir["#{config.root}/lib/**/"]
|
@@ -39,11 +41,10 @@ module PandaPal
|
|
39
41
|
|
40
42
|
initializer 'Sidekiq Scheduler Hooks' do
|
41
43
|
ActiveSupport.on_load(:active_record) do
|
42
|
-
if defined?(Sidekiq) && Sidekiq.server?
|
43
|
-
PandaPal::Organization.sync_schedules
|
44
|
+
if defined?(Sidekiq) && Sidekiq.server?
|
44
45
|
|
45
|
-
|
46
|
-
PandaPal::Organization.sync_schedules
|
46
|
+
Rails.application.reloader.to_prepare do
|
47
|
+
PandaPal::Organization.sync_schedules if PandaPal::Organization.respond_to?(:sync_schedules)
|
47
48
|
end
|
48
49
|
end
|
49
50
|
end
|
@@ -74,21 +75,51 @@ module PandaPal
|
|
74
75
|
def _panda_pal_console_app_name
|
75
76
|
app_class = Rails.application.class
|
76
77
|
app_name = app_class.respond_to?(:parent) ? app_class.parent : app_class.module_parent
|
78
|
+
app_name.to_s
|
79
|
+
end
|
80
|
+
|
81
|
+
def _panda_pal_short_console_app_name
|
82
|
+
_panda_pal_console_app_name[0...10]
|
83
|
+
end
|
84
|
+
|
85
|
+
def _panda_pal_console_env
|
86
|
+
if Rails.env.production?
|
87
|
+
env = ENV["SENTRY_CURRENT_ENV"].presence || "PROD"
|
88
|
+
|
89
|
+
if env.downcase.include?("prod")
|
90
|
+
PandaPal::ConsoleHelpers.red(env)
|
91
|
+
else
|
92
|
+
PandaPal::ConsoleHelpers.cyan(env)
|
93
|
+
end
|
94
|
+
elsif Rails.env.development?
|
95
|
+
PandaPal::ConsoleHelpers.cyan("dev")
|
96
|
+
elsif Rails.env.test?
|
97
|
+
PandaPal::ConsoleHelpers.cyan("test")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def _panda_pal_console_prefix
|
102
|
+
pfx = [
|
103
|
+
PandaPal::ConsoleHelpers.cyan(_panda_pal_short_console_app_name),
|
104
|
+
_panda_pal_console_env,
|
105
|
+
PandaPal::ConsoleHelpers.cyan(Apartment::Tenant.current),
|
106
|
+
].compact.join('-')
|
107
|
+
pfx
|
77
108
|
end
|
78
109
|
end
|
79
110
|
|
80
111
|
if defined? IRB
|
81
|
-
module
|
112
|
+
module PandaPalIrbPrompt
|
82
113
|
def prompt(prompt, ltype, indent, line_no)
|
83
114
|
formatted = super(prompt, ltype, indent, line_no)
|
84
|
-
app_bit =
|
115
|
+
app_bit = _panda_pal_console_prefix
|
85
116
|
"[#{app_bit}] #{formatted}"
|
86
117
|
end
|
87
118
|
end
|
88
119
|
|
89
120
|
module ::IRB
|
90
121
|
class Irb
|
91
|
-
prepend
|
122
|
+
prepend PandaPalIrbPrompt
|
92
123
|
end
|
93
124
|
end
|
94
125
|
end
|
@@ -97,18 +128,16 @@ module PandaPal
|
|
97
128
|
default_prompt = Pry::Prompt[:default]
|
98
129
|
env = Pry::Helpers::Text.red(Rails.env.upcase)
|
99
130
|
|
100
|
-
app_name = _panda_pal_console_app_name
|
101
|
-
|
102
131
|
Pry.config.prompt = Pry::Prompt.new(
|
103
132
|
'custom',
|
104
133
|
'my custom prompt',
|
105
134
|
[
|
106
135
|
->(*args) {
|
107
|
-
app_bit =
|
136
|
+
app_bit = _panda_pal_console_prefix
|
108
137
|
"#{app_bit}#{default_prompt.wait_proc.call(*args)}"
|
109
138
|
},
|
110
139
|
->(*args) {
|
111
|
-
app_bit =
|
140
|
+
app_bit = _panda_pal_console_prefix
|
112
141
|
"#{app_bit}#{default_prompt.incomplete_proc.call(*args)}"
|
113
142
|
},
|
114
143
|
],
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module PandaPal
|
2
|
+
module ConsoleHelpers
|
3
|
+
extend self
|
4
|
+
|
5
|
+
COLORS = {
|
6
|
+
"black" => 0,
|
7
|
+
"red" => 1,
|
8
|
+
"green" => 2,
|
9
|
+
"yellow" => 3,
|
10
|
+
"blue" => 4,
|
11
|
+
"purple" => 5,
|
12
|
+
"magenta" => 5,
|
13
|
+
"cyan" => 6,
|
14
|
+
"white" => 7
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
COLORS.each_pair do |color, value|
|
18
|
+
define_method color do |text|
|
19
|
+
"\033[0;#{30 + value}m#{text}\033[0m"
|
20
|
+
end
|
21
|
+
|
22
|
+
define_method "bright_#{color}" do |text|
|
23
|
+
"\033[1;#{30 + value}m#{text}\033[0m"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -70,10 +70,11 @@ module PandaPal::Helpers
|
|
70
70
|
|
71
71
|
@organization ||= PandaPal::Organization.find_by(key: client_id)
|
72
72
|
|
73
|
-
raise JSON::JWT::VerificationFailed, 'Unrecognized Organization' unless @organization.present?
|
74
|
-
|
75
73
|
params[:session_key] = params[:state]
|
76
74
|
|
75
|
+
raise JSON::JWT::VerificationFailed, 'Unrecognized Organization' unless @organization.present?
|
76
|
+
raise JSON::JWT::VerificationFailed, 'Organization does not trust platform' unless @organization.trusted_platform?(current_lti_platform)
|
77
|
+
|
77
78
|
decoded_jwt.verify!(current_lti_platform.public_jwks)
|
78
79
|
|
79
80
|
raise JSON::JWT::VerificationFailed, 'State is invalid' unless current_session_data[:lti_oauth_nonce] == decoded_jwt['nonce']
|
data/lib/panda_pal/version.rb
CHANGED