panda_pal 4.1.0.beta3 → 5.0.0.beta.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ef5b128c1f14031f45d4955e56094db8cab358ba
4
- data.tar.gz: 0e55cffec0dc1dd61ce847d018f1140a047549ab
2
+ SHA256:
3
+ metadata.gz: 417de10d63af26bbeadb9b1fa5d57f74fad8070dec477ecf42f7ba914c46be4e
4
+ data.tar.gz: '0268833a242fda47935ec01207644c3a74daaa31b37b311401ac216b9e34f9bf'
5
5
  SHA512:
6
- metadata.gz: db77dbe724b48bad8a1439e6296c48f1063cb90b22c9bf22a96f7e351cd5c6cbe186b1abe847983e35f3417bba4f1d4d9ce849f0628e158873a4db46d515b20f
7
- data.tar.gz: a9257806e5ad3c1d24b5faf0b0ca030ed7819b2ddd4aa3f8c20f80da103a1f95fb08a0bda9fe714b4cda4da2b678c46ff2ed4a176e730e7627fc4ad2f4e2dc13
6
+ metadata.gz: 1093764dca3b51778eb98cf8c8ce64bc88af73fb970e9f61597c3f71c92761729ad636ea156da7f2aad7d656ca2f35311f4cce72a338c3eb49f8cb85b788f229
7
+ data.tar.gz: ee960dc3a06b1da790af9ce3f9a7e6bb76bb06e75a2645097654833226ed84f1f9655455c396607b74981879bed44029147b55ead6efc6437b4abe1c9feb4699
data/README.md CHANGED
@@ -28,38 +28,6 @@ Use one of these 6 options in `PandaPal.lti_options` hash.
28
28
  5. Leave this property off, and you will get the dynamic host with the root path ('http://appdomain.com/') by default.
29
29
  6. If you really do not want this property use the option `launch_url: false` for it to be left off.
30
30
 
31
- ### Task Scheduling
32
- `PandaPal` includes an integration with `sidekiq-scheduler`. You can define tasks on an Organization class Stub like so:
33
- ```ruby
34
- # <your_app>/app/models/organization.rb
35
- module PandaPal
36
- class Organization
37
- # Will invoke CanvasSyncStarterWorker.perform_async() according to the cron schedule
38
- scheduled_task '0 15 05 * * *', :identifier, worker: CanvasSyncStarterWorker
39
-
40
- # Will invoke the method 'organization_method' on the Organization
41
- scheduled_task '0 15 05 * * *', :organization_method_and_identifier
42
-
43
- # If you need to invoke the same method on multiple schedules
44
- scheduled_task '0 15 05 * * *', :identifier, worker: :organization_method
45
-
46
- # You can also use a block
47
- scheduled_task '0 15 05 * * *', :identifier do
48
- # Do Stuff
49
- end
50
-
51
- # You can use a Proc (called in the context of the Organization) to determine the schedule
52
- scheduled_task -> { settings[:cron] }, :identifier
53
-
54
- # You can specify a timezone. If a TZ is not coded and settings[:timezone] is present, it will be appended automatically
55
- scheduled_task '0 15 05 * * * America/Denver', :identifier, worker: :organization_method
56
-
57
- # Setting settings[:task_schedules][:identifier] will override the code cron schedule. Setting it to false will disable the Task
58
- # :identifer values _must_ be unique, but can be nil, in which case they will be determined by where (lineno etc) scheduled_task is called
59
- end
60
- end
61
- ```
62
-
63
31
  # Organization Attributes
64
32
  id: Primary Key
65
33
  name: Name of the organization. Used to on requests to select the tenant
@@ -258,43 +226,9 @@ In your panda_pal initializer (i.e. config/initializers/panda_pal.rb or config/i
258
226
  You can specify options that can include a structure for your settings. If specified, PandaPal will
259
227
  enforce this structure on any new / updated organizations.
260
228
 
261
- ```ruby
262
- PandaPal.lti_options = {
263
- title: 'LBS Gradebook',
264
- settings_structure: {
265
- allow_additional: true, # Allow additional properties that aren't included in the :properties Hash.
266
- allow_additional: { type: 'String' }, # You can also set :allow_additional to a settings specification that will be used to validate each additional setting
267
- validate: ->(value, spec, **kwargs) {
268
- # kwargs currently includes:
269
- # :errors => [An array to push errors to]
270
- # :path => [An array representation of the current path in the settings object]
271
-
272
- # To add errors, you may:
273
- # Push strings to the kwargs[:errors] Array:
274
- kwargs[:errors] << "Your error message at <path>" unless value < 10
275
- # Or return a string or string array:
276
- value.valid? ? nil : "Your error message at <path>" # <path> will be replaced with the actual path that the error occurred at
277
- },
278
- properties: {
279
- canvas_api_token: { type: 'String', required: true, },
280
- catalog: { # :validate, :allow_additional, :properties keys are all supported at this level as well
281
- type: 'Hash',
282
- required: false,
283
- validate: -> (*args) {},
284
- allow_additional: false,
285
- properties: {
286
-
287
- },
288
- }
289
- }
290
- },
291
- }
292
- ```
293
-
294
- #### Legacy Settings Structure:
295
229
  Here is an example options specification:
296
230
 
297
- ```ruby
231
+ ```
298
232
  PandaPal.lti_options = {
299
233
  title: 'LBS Gradebook',
300
234
  settings_structure: YAML.load("
@@ -347,7 +281,83 @@ Safari is weird, and you'll potentially run into issues getting `POST` requests
347
281
 
348
282
  This will allow `PandaPal` to apply an iframe cookie fix that will allow CSRF validation to work.
349
283
 
350
- ## Running Specs:
351
- Initialize the Specs DB:
352
- `cd spec/dummy; bundle exec rake db:drop; bundle exec rake db:create; bundle exec rake db:schema:load`
353
- Then `bundle exec rspec`
284
+
285
+ ### PandaPal 5
286
+
287
+ It has been a constant struggle to force safari to store and allow
288
+ access to a rails session while the application is embedded in Canvas.
289
+
290
+ As of PandaPal 5, a forced persistent session is now optional, and defaults to off.
291
+
292
+ This means that safari will likely refuse to send info about your rails session
293
+ back to the LTI, and the application will start up a new session each time the
294
+ browser navigates. This likely means a new session each time the LTI launches.
295
+
296
+ You will want to watch out for a few scenarios:
297
+
298
+ 1) Make sure you are using "redirect_with_session_to" if you need to redirect
299
+ and have your PandaPal session_key persisted server side.
300
+ 2) Use the "Authorization" header with "token={session_key}" to send your
301
+ PandaPal session info into api calls.
302
+
303
+ You can force a persistent session with -
304
+ PandaPal.lti_options = {
305
+ require_persistent_session: true
306
+ }
307
+ in your config/initializer. With that setting, the user will be required to
308
+ allow our application to store / access cookies in safari before they can use
309
+ the LTI.
310
+
311
+ # Notes on require_persistent_session
312
+
313
+ IF you must have a persistent session this is the logical flow of how panda_pal
314
+ attempts to set that up.
315
+
316
+ 1) LTI laumches.
317
+ 2) LTI will attempt to POST message from iframe to top window (canvas) telling
318
+ canvas to relaunch in full screen so we aren't inhibited by safari.
319
+ 3) LTI will setup session and cookies in full-screen mode. Session will be saved
320
+ in browser.
321
+ 4) LTI will redirect to an authorization page, that will require user to give
322
+ access to the session store to our application.
323
+ 5) Once the user gives access to the session store, we will reload the LTI
324
+ and the cookie should now be persistent.
325
+
326
+ # Upgrading from PandaPal 4 to 5:
327
+
328
+ If your tool is setup according to a pretty standard pattern (see pace_plans,
329
+ canvas_group_enrollment, etc), you shouldn't have to do anything to upgrade.
330
+
331
+ You will want to make sure that IF your launch controller is redirecting, it is
332
+ using "redirect_with_session_to".
333
+
334
+ Here is an example launch / account controller setup, assuming an account launch.
335
+
336
+ ```
337
+ class LaunchController < ApplicationController
338
+ # We don't verify CSRF on launch because the LTI launch is done via a POST
339
+ # request, and Canvas wouldn't know anything about the CSRF
340
+ skip_before_action :verify_authenticity_token
341
+ skip_before_action :forbid_access_if_lacking_session # We don't have a session yet
342
+ around_action :switch_tenant
343
+ before_action :validate_launch!
344
+ before_action :handle_launch
345
+
346
+ def handle_launch
347
+ current_session_data[:canvas_user_id] = params[:custom_canvas_user_id]
348
+ current_session_data[:canvas_course_id] = params[:custom_canvas_course_id]
349
+ end
350
+
351
+ def account
352
+ redirect_with_session_to :accounts_url
353
+ end
354
+ end
355
+
356
+ class AccountController < ApplicationController
357
+ prepend_before_action :forbid_access_if_lacking_session
358
+
359
+ def index
360
+ end
361
+ end
362
+ ```
363
+
@@ -1,23 +1,15 @@
1
- Dir[File.dirname(__FILE__) + "/organization/*.rb"].each { |file| require file }
2
-
3
1
  module PandaPal
4
- module OrganizationConcerns; end
5
2
 
6
3
  class Organization < ActiveRecord::Base
7
- include OrganizationConcerns::SettingsValidation
8
- include OrganizationConcerns::TaskScheduling if defined?(Sidekiq.schedule)
9
-
10
4
  attribute :settings
11
- serialize :settings, Hash
12
5
  attr_encrypted :settings, marshal: true, key: :encryption_key
13
6
  before_save {|a| a.settings = a.settings} # this is a hacky work-around to a bug where attr_encrypted is not saving settings in place
14
-
15
7
  validates :key, uniqueness: { case_sensitive: false }, presence: true
16
8
  validates :secret, presence: true
17
9
  validates :name, uniqueness: { case_sensitive: false }, presence: true, format: { with: /\A[a-z0-9_]+\z/i }
18
10
  validates :canvas_account_id, presence: true
19
11
  validates :salesforce_id, presence: true, uniqueness: true
20
-
12
+ validate :validate_settings
21
13
  after_create :create_schema
22
14
  after_commit :destroy_schema, on: :destroy
23
15
 
@@ -25,6 +17,8 @@ module PandaPal
25
17
  errors.add(:name, 'should not be changed after creation') if name_changed?
26
18
  end
27
19
 
20
+ serialize :settings, Hash
21
+
28
22
  def encryption_key
29
23
  # production environment might not have loaded secret_key_base yet.
30
24
  # In that case, just read it from env.
@@ -35,14 +29,6 @@ module PandaPal
35
29
  end
36
30
  end
37
31
 
38
- def switch_tenant(&block)
39
- if block_given?
40
- Apartment::Tenant.switch(name, &block)
41
- else
42
- Apartment::Tenant.switch!(name)
43
- end
44
- end
45
-
46
32
  private
47
33
 
48
34
  def create_schema
@@ -52,5 +38,48 @@ module PandaPal
52
38
  def destroy_schema
53
39
  Apartment::Tenant.drop name
54
40
  end
41
+
42
+ def validate_settings
43
+ record = self
44
+ if PandaPal.lti_options && PandaPal.lti_options[:settings_structure]
45
+ validate_level(record, PandaPal.lti_options[:settings_structure], record.settings, [])
46
+ end
47
+ end
48
+ def validate_level(record, expectation, reality, previous_keys)
49
+ # Verify that the data elements at this level conform to requirements.
50
+ if expectation
51
+ expectation.each do |key, value|
52
+ is_required = expectation[key].try(:delete, :is_required)
53
+ data_type = expectation[key].try(:delete, :data_type)
54
+ value = reality.is_a?(Hash) ? reality.dig(key) : reality
55
+
56
+ if is_required && !value
57
+ record.errors[:settings] << "PandaPal::Organization.settings requires key [:#{previous_keys.push(key).join("][:")}]. It was not found."
58
+ end
59
+ if data_type && value
60
+ if value.class.to_s != data_type
61
+ record.errors[:settings] << "PandaPal::Organization.settings expected key [:#{previous_keys.push(key).join("][:")}] to be #{data_type} but it was instead #{value.class}."
62
+ end
63
+ end
64
+ end
65
+ end
66
+ # Verify that anything that is in the real settings has an expectation.
67
+ if reality
68
+ if reality.is_a? Hash
69
+ reality.each do |key, value|
70
+ was_expected = expectation.has_key?(key)
71
+ if !was_expected
72
+ record.errors[:settings] << "PandaPal::Organization.settings had unexpected key: #{key}. If settings have expanded please update your lti_options accordingly."
73
+ end
74
+ end
75
+ end
76
+ end
77
+ # Recursively check out any children settings as well.
78
+ if expectation
79
+ expectation.each do |key, value|
80
+ validate_level(record, expectation[key], (reality.is_a?(Hash) ? reality.dig(key) : nil), previous_keys.deep_dup.push(key))
81
+ end
82
+ end
83
+ end
55
84
  end
56
85
  end
@@ -0,0 +1,19 @@
1
+ <html>
2
+ <p>Safari requires your consent to access session information when applications are hosted inside of Canvas. Please consent by clicking the following button.</p>
3
+ <button id="myButton">Authorize application to use browser session</button>
4
+ <script nonce=<%= content_security_policy_script_nonce %>>
5
+ function makeRequestWithUserGesture() {
6
+ var promise = document.requestStorageAccess();
7
+ promise.then(
8
+ function () {
9
+ var referrer = document.referrer;
10
+ window.location='?safari_cookie_authorized=true&return_to='.concat(encodeURI(window.location));
11
+ },
12
+ function () {
13
+ // If the user doesn't consent, then do nothing.
14
+ }
15
+ );
16
+ }
17
+ document.getElementById("myButton").addEventListener("click", makeRequestWithUserGesture);
18
+ </script>
19
+ </html>
File without changes
File without changes
File without changes
File without changes
@@ -42,20 +42,29 @@ module PandaPal::Helpers::ControllerHelper
42
42
  render plain: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
43
43
  return authorized
44
44
  end
45
- if cookies_need_iframe_fix?
46
- fix_iframe_cookies
47
- return false
48
- end
49
- # For safari we may have been launched temporarily full-screen by canvas. This allows us to set the session cookie.
50
- # In this case, we should make sure the session cookie is fixed and redirect back to canvas to properly launch the embedded LTI.
51
- if params[:platform_redirect_url]
52
- session[:safari_cookie_fixed] = true
53
- redirect_to params[:platform_redirect_url]
54
- return false
45
+ if require_persistent_session
46
+ if cookies_need_iframe_fix?(false)
47
+ fix_iframe_cookies
48
+ return false
49
+ end
50
+ # For safari we may have been launched temporarily full-screen by canvas. This allows us to set the session cookie.
51
+ # In this case, we should make sure the session cookie is fixed and redirect back to canvas to properly launch the embedded LTI.
52
+ if params[:platform_redirect_url]
53
+ session[:safari_cookie_fixed] = true
54
+ redirect_to params[:platform_redirect_url]
55
+ return false
56
+ end
55
57
  end
56
58
  return authorized
57
59
  end
58
60
 
61
+ def require_persistent_session
62
+ if PandaPal.lti_options.has_key?(:require_persistent_session) && PandaPal.lti_options[:require_persistent_session] == true
63
+ return true
64
+ end
65
+ return false
66
+ end
67
+
59
68
  def switch_tenant(organization = current_organization, &block)
60
69
  return unless organization
61
70
  raise 'This method should be called in an around_action callback' unless block_given?
@@ -70,20 +79,26 @@ module PandaPal::Helpers::ControllerHelper
70
79
  # redirect the current page to the LTI using JavaScript, which will set the cookie,
71
80
  # and then immediately redirect back to Canvas.
72
81
  def fix_iframe_cookies
73
- if params[:safari_cookie_fix].present?
74
- session[:safari_cookie_fixed] = true
82
+ if params[:safari_cookie_authorized].present?
83
+ session[:safari_cookie_authorized] = true
75
84
  redirect_to params[:return_to]
85
+ elsif (session[:safari_cookie_fixed] && !params[:safari_cookie_authorized])
86
+ render 'panda_pal/lti/iframe_cookie_authorize', layout: false
76
87
  else
77
88
  render 'panda_pal/lti/iframe_cookie_fix', layout: false
78
89
  end
79
90
  end
80
91
 
81
- def cookies_need_iframe_fix?
82
- browser.safari? && !request.referrer&.include?('sessionless_launch') && !session[:safari_cookie_fixed] && !params[:platform_redirect_url]
92
+ def cookies_need_iframe_fix?(check_authorized=true)
93
+ if check_authorized
94
+ return browser.safari? && !request.referrer&.include?('sessionless_launch') && !(session[:safari_cookie_fixed] && session[:safari_cookie_authorized]) && !params[:platform_redirect_url]
95
+ else
96
+ return browser.safari? && !request.referrer&.include?('sessionless_launch') && !session[:safari_cookie_fixed] && !params[:platform_redirect_url]
97
+ end
83
98
  end
84
99
 
85
100
  def forbid_access_if_lacking_session
86
- if cookies_need_iframe_fix?
101
+ if require_persistent_session && cookies_need_iframe_fix?(true)
87
102
  fix_iframe_cookies
88
103
  else
89
104
  render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session?
@@ -122,4 +137,34 @@ module PandaPal::Helpers::ControllerHelper
122
137
  match[1]
123
138
  end
124
139
  end
140
+
141
+ # Redirect with the session key intact. In production,
142
+ # handle this by saving it to the flash. In dev,
143
+ # just put it in the URL. Putting it in the URL
144
+ # is insecure, but is fine in development.
145
+ # Keeping it in the URL in development means that it plays
146
+ # nicely with webpack-dev-server live reloading (otherwise
147
+ # you get an access error everytime it tries to live reload).
148
+
149
+ def redirect_with_session_to(location, params = {})
150
+ if Rails.env.development?
151
+ redirect_development_mode(location, params)
152
+ else
153
+ redirect_production_mode(location, params)
154
+ end
155
+ end
156
+
157
+ def redirect_development_mode(location, params)
158
+ redirect_to send(location, {
159
+ session_key: current_session.session_key,
160
+ organization_id: current_organization.id
161
+ }.merge(params))
162
+ end
163
+
164
+ def redirect_production_mode(location, params)
165
+ flash['session_key'] = current_session.session_key
166
+ redirect_to send(location, {
167
+ organization_id: current_organization.id
168
+ }.merge(params))
169
+ end
125
170
  end
@@ -1,3 +1,3 @@
1
1
  module PandaPal
2
- VERSION = "4.1.0.beta3"
2
+ VERSION = "5.0.0.beta.1"
3
3
  end
data/panda_pal.gemspec CHANGED
@@ -22,9 +22,6 @@ Gem::Specification.new do |s|
22
22
  s.add_dependency 'browser', '2.5.0'
23
23
  s.add_dependency 'attr_encrypted', '~> 3.0.0'
24
24
  s.add_dependency 'secure_headers', '~> 6.1.2'
25
-
26
- s.add_development_dependency 'sidekiq'
27
- s.add_development_dependency 'sidekiq-scheduler'
28
25
  s.add_development_dependency 'rspec-rails'
29
26
  s.add_development_dependency 'factory_girl_rails'
30
27
  end
@@ -1,12 +1,6 @@
1
1
  require File.expand_path('../boot', __FILE__)
2
2
 
3
- require "active_model/railtie"
4
- require "active_job/railtie"
5
- require "active_record/railtie"
6
- # require "active_storage/engine"
7
- require "action_controller/railtie"
8
- require "action_mailer/railtie"
9
- require "action_view/railtie"
3
+ require 'rails/all'
10
4
 
11
5
  Bundler.require(*Rails.groups)
12
6
  require "panda_pal"
@@ -22,6 +22,20 @@ Rails.application.configure do
22
22
  # Raise an error on page load if there are pending migrations.
23
23
  config.active_record.migration_error = :page_load
24
24
 
25
+ # Debug mode disables concatenation and preprocessing of assets.
26
+ # This option may cause significant delays in view rendering with a large
27
+ # number of complex assets.
28
+ config.assets.debug = true
29
+
30
+ # Asset digests allow you to set far-future HTTP expiration dates on all assets,
31
+ # yet still be able to expire them through the digest params.
32
+ config.assets.digest = true
33
+
34
+ # Adds additional error checking when serving assets at runtime.
35
+ # Checks for improperly declared sprockets dependencies.
36
+ # Raises helpful error messages.
37
+ config.assets.raise_runtime_errors = true
38
+
25
39
  # Raises error for missing translations
26
40
  # config.action_view.raise_on_missing_translations = true
27
41
  end
@@ -24,6 +24,17 @@ Rails.application.configure do
24
24
  # Apache or NGINX already handles this.
25
25
  config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
26
26
 
27
+ # Compress JavaScripts and CSS.
28
+ config.assets.js_compressor = :uglifier
29
+ # config.assets.css_compressor = :sass
30
+
31
+ # Do not fallback to assets pipeline if a precompiled asset is missed.
32
+ config.assets.compile = false
33
+
34
+ # Asset digests allow you to set far-future HTTP expiration dates on all assets,
35
+ # yet still be able to expire them through the digest params.
36
+ config.assets.digest = true
37
+
27
38
  # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
28
39
 
29
40
  # Specifies the header that your server uses for sending files.
@@ -0,0 +1,11 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Version of your assets, change this if you want to expire all your assets.
4
+ Rails.application.config.assets.version = '1.0'
5
+
6
+ # Add additional assets to the asset load path
7
+ # Rails.application.config.assets.paths << Emoji.images_path
8
+
9
+ # Precompile additional assets.
10
+ # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
11
+ # Rails.application.config.assets.precompile += %w( search.js )
Binary file
Binary file