panda_pal 4.1.0.beta3 → 5.0.0.beta.1

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
- 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