panda_pal 5.7.0.beta2 → 5.8.1
Sign up to get free protection for your applications and to get access to all the features.
- 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