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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de960e9dd44bfec67832afe2e1cde116ea8a9e608f9658dea7cced1d45b1d934
4
- data.tar.gz: 8795f3fed5583c49d3ff729ef18e1e4b4ab21214b4d6319fe13a28e90073e00f
3
+ metadata.gz: 35907d0726df7ab474a251eedff8ff28a5d96fbf4996566454c777e20443d90d
4
+ data.tar.gz: 4f883c646c445d4a0a8ad918c669308c17a2a8392818e4645a5d9efdce5ee0e2
5
5
  SHA512:
6
- metadata.gz: 65b30bd9f2bfca02fcf92b451f85fd98e701a5251e0952d4468b4b84178b2a7149705fd7dd20783f6d89c8f1b02e4630ced8d00a0ceb22928927e19aa405c72e
7
- data.tar.gz: 320abed36d66d03e24c7c1cb38f545d094779559107f10fa084d464989ebb473a07b497b75eeb9d84c3febc16bfadd88f0002ca298ab67456e4c657261fdc8d6
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
- # Redirect to correct region/env?
41
+ ltype = @decoded_lti_jwt['https://www.instructure.com/placement']
39
42
 
40
- if params[:launch_type]
43
+ if ltype
41
44
  current_session_data.merge!({
42
45
  lti_version: 'v1p3',
43
- lti_launch_placement: params[:launch_type],
46
+ lti_launch_placement: ltype,
44
47
  launch_params: @decoded_lti_jwt,
45
48
  })
46
49
 
47
- redirect_with_session_to(:"#{LaunchUrlHelpers.launch_route(params[:launch_type])}_url", route_context: main_app)
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
- attr_accessor :organization
8
+ TRUSTED_ISSUERS = [
9
+ "https://sso.canvaslms.com",
10
+ "https://sso.beta.canvaslms.com",
11
+ "https://sso.test.canvaslms.com",
9
12
 
10
- KNOWN_ISSUERS = [
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
- "#{@issuer}/api/lti/security/jwks"
28
+ "#{lti_api_domain}/api/lti/security/jwks"
18
29
  end
19
30
 
20
31
  def authentication_redirect_url
21
- "#{@issuer}/api/lti/authorize_redirect"
32
+ "#{lti_api_domain}/api/lti/authorize_redirect"
22
33
  end
23
34
 
24
35
  def grant_url
25
- "#{@issuer}/login/oauth2/token"
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(context, exists: exists) do |lti|
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
- params[:platform_class].safe_constantize.new(params)
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::KNOWN_ISSUERS.include?(params[:iss])
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
- attr_reader :jwks_url, :authentication_redirect_url, :grant_url
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 init(base_url, jwks: "/api/lti/security/jwks", auth_redirect: "/api/lti/authorize_redirect", grant: "/login/oauth2/token")
102
- @jwks_url = URI.join(base_url, jwks).to_s
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
@@ -9,7 +9,7 @@ module PandaPal
9
9
  def lti_platform
10
10
  return nil unless data[:lti_platform].present?
11
11
 
12
- @lti_platform ||= Platform.deserialize(data[:lti_platform])
12
+ @lti_platform ||= Platform.from_serialized(data[:lti_platform])
13
13
  end
14
14
 
15
15
  class DataSerializer
@@ -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
- ActiveSupport::Reloader.to_prepare do
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 PandaPalIrbTimePrompt
115
+ module PandaPalIrbPrompt
82
116
  def prompt(prompt, ltype, indent, line_no)
83
117
  formatted = super(prompt, ltype, indent, line_no)
84
- app_bit = Pry::Helpers::Text.cyan("#{_panda_pal_console_app_name}-#{Apartment::Tenant.current}")
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 PandaPalIrbTimePrompt
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 = Pry::Helpers::Text.cyan("#{app_name}-#{Apartment::Tenant.current}")
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 = Pry::Helpers::Text.cyan("#{app_name}-#{Apartment::Tenant.current}")
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']
@@ -1,3 +1,3 @@
1
1
  module PandaPal
2
- VERSION = "5.7.0.beta2"
2
+ VERSION = "5.8.0"
3
3
  end