panda_pal 5.7.0.beta2 → 5.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- 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 +43 -11
- 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 +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 35907d0726df7ab474a251eedff8ff28a5d96fbf4996566454c777e20443d90d
|
4
|
+
data.tar.gz: 4f883c646c445d4a0a8ad918c669308c17a2a8392818e4645a5d9efdce5ee0e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f259a0c7c5928892748bd08f88b259adc5e7bb41f90f7f71e90a6c7c5e37e7a243ad426d34a4ad6b59857891e2430a7f7860cbd15938f11eaae0f74f6571247
|
7
|
+
data.tar.gz: 10a665a5f0d00604f623a863255435c4f85b850de0e175dc1ef29417a83a0263c41d3ff6a7016bda9e74b2abe558da18ebb9ee87b21503aeb8920fada15c1b91
|
@@ -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
@@ -39,12 +39,16 @@ module PandaPal
|
|
39
39
|
|
40
40
|
initializer 'Sidekiq Scheduler Hooks' do
|
41
41
|
ActiveSupport.on_load(:active_record) do
|
42
|
-
if defined?(Sidekiq) && Sidekiq.server? && PandaPal::Organization.respond_to?(:sync_schedules)
|
43
|
-
PandaPal::Organization.sync_schedules
|
42
|
+
if defined?(Sidekiq) && Sidekiq.server? # && PandaPal::Organization.respond_to?(:sync_schedules)
|
44
43
|
|
45
|
-
|
46
|
-
PandaPal::Organization.sync_schedules
|
44
|
+
Rails.application.reloader.to_prepare do
|
45
|
+
PandaPal::Organization.sync_schedules if PandaPal::Organization.respond_to?(:sync_schedules)
|
47
46
|
end
|
47
|
+
# PandaPal::Organization.sync_schedules
|
48
|
+
|
49
|
+
# ActiveSupport::Reloader.to_prepare do
|
50
|
+
# PandaPal::Organization.sync_schedules
|
51
|
+
# end
|
48
52
|
end
|
49
53
|
end
|
50
54
|
end
|
@@ -74,21 +78,51 @@ module PandaPal
|
|
74
78
|
def _panda_pal_console_app_name
|
75
79
|
app_class = Rails.application.class
|
76
80
|
app_name = app_class.respond_to?(:parent) ? app_class.parent : app_class.module_parent
|
81
|
+
app_name.to_s
|
82
|
+
end
|
83
|
+
|
84
|
+
def _panda_pal_short_console_app_name
|
85
|
+
_panda_pal_console_app_name[0...10]
|
86
|
+
end
|
87
|
+
|
88
|
+
def _panda_pal_console_env
|
89
|
+
if Rails.env.production?
|
90
|
+
env = ENV["SENTRY_CURRENT_ENV"].presence || "PROD"
|
91
|
+
|
92
|
+
if env.downcase.include?("prod")
|
93
|
+
Pry::Helpers::Text.red(env)
|
94
|
+
else
|
95
|
+
Pry::Helpers::Text.cyan(env)
|
96
|
+
end
|
97
|
+
elsif Rails.env.development?
|
98
|
+
Pry::Helpers::Text.cyan("dev")
|
99
|
+
elsif Rails.env.test?
|
100
|
+
Pry::Helpers::Text.cyan("test")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def _panda_pal_console_prefix
|
105
|
+
pfx = [
|
106
|
+
Pry::Helpers::Text.cyan(_panda_pal_short_console_app_name),
|
107
|
+
_panda_pal_console_env,
|
108
|
+
Pry::Helpers::Text.cyan(Apartment::Tenant.current),
|
109
|
+
].compact.join('-')
|
110
|
+
pfx
|
77
111
|
end
|
78
112
|
end
|
79
113
|
|
80
114
|
if defined? IRB
|
81
|
-
module
|
115
|
+
module PandaPalIrbPrompt
|
82
116
|
def prompt(prompt, ltype, indent, line_no)
|
83
117
|
formatted = super(prompt, ltype, indent, line_no)
|
84
|
-
app_bit =
|
118
|
+
app_bit = _panda_pal_console_prefix
|
85
119
|
"[#{app_bit}] #{formatted}"
|
86
120
|
end
|
87
121
|
end
|
88
122
|
|
89
123
|
module ::IRB
|
90
124
|
class Irb
|
91
|
-
prepend
|
125
|
+
prepend PandaPalIrbPrompt
|
92
126
|
end
|
93
127
|
end
|
94
128
|
end
|
@@ -97,18 +131,16 @@ module PandaPal
|
|
97
131
|
default_prompt = Pry::Prompt[:default]
|
98
132
|
env = Pry::Helpers::Text.red(Rails.env.upcase)
|
99
133
|
|
100
|
-
app_name = _panda_pal_console_app_name
|
101
|
-
|
102
134
|
Pry.config.prompt = Pry::Prompt.new(
|
103
135
|
'custom',
|
104
136
|
'my custom prompt',
|
105
137
|
[
|
106
138
|
->(*args) {
|
107
|
-
app_bit =
|
139
|
+
app_bit = _panda_pal_console_prefix
|
108
140
|
"#{app_bit}#{default_prompt.wait_proc.call(*args)}"
|
109
141
|
},
|
110
142
|
->(*args) {
|
111
|
-
app_bit =
|
143
|
+
app_bit = _panda_pal_console_prefix
|
112
144
|
"#{app_bit}#{default_prompt.incomplete_proc.call(*args)}"
|
113
145
|
},
|
114
146
|
],
|
@@ -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