panda_pal 5.1.0 → 5.2.0

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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +139 -93
  3. data/app/controllers/panda_pal/lti_controller.rb +0 -18
  4. data/app/controllers/panda_pal/lti_v1_p0_controller.rb +34 -0
  5. data/app/controllers/panda_pal/lti_v1_p3_controller.rb +98 -0
  6. data/app/lib/lti_xml/base_platform.rb +4 -4
  7. data/app/lib/panda_pal/launch_url_helpers.rb +69 -0
  8. data/app/lib/panda_pal/lti_jwt_validator.rb +88 -0
  9. data/app/lib/panda_pal/misc_helper.rb +11 -0
  10. data/app/models/panda_pal/organization.rb +6 -3
  11. data/app/models/panda_pal/{organization → organization_concerns}/settings_validation.rb +21 -5
  12. data/app/models/panda_pal/{organization → organization_concerns}/task_scheduling.rb +32 -0
  13. data/app/models/panda_pal/platform.rb +40 -0
  14. data/app/views/panda_pal/lti_v1_p3/login.html.erb +1 -0
  15. data/app/views/panda_pal/partials/_auto_submit_form.html.erb +9 -0
  16. data/config/dev_lti_key.key +27 -0
  17. data/config/routes.rb +12 -2
  18. data/db/migrate/20160412205931_create_panda_pal_organizations.rb +1 -1
  19. data/db/migrate/20160413135653_create_panda_pal_sessions.rb +1 -1
  20. data/db/migrate/20160425130344_add_panda_pal_organization_to_session.rb +1 -1
  21. data/db/migrate/20170106165533_add_salesforce_id_to_organizations.rb +1 -1
  22. data/db/migrate/20171205183457_encrypt_organization_settings.rb +1 -1
  23. data/db/migrate/20171205194657_remove_old_organization_settings.rb +8 -3
  24. data/lib/panda_pal.rb +28 -15
  25. data/lib/panda_pal/engine.rb +8 -39
  26. data/lib/panda_pal/helpers.rb +1 -0
  27. data/lib/panda_pal/helpers/controller_helper.rb +138 -44
  28. data/lib/panda_pal/helpers/route_helper.rb +8 -8
  29. data/lib/panda_pal/helpers/secure_headers.rb +79 -0
  30. data/lib/panda_pal/version.rb +1 -1
  31. data/panda_pal.gemspec +3 -2
  32. metadata +32 -8
@@ -76,7 +76,7 @@ module LtiXml
76
76
 
77
77
  def add_lti_nav
78
78
  PandaPal.lti_paths.each do |k, v|
79
- @tc.set_ext_param(platform, k.to_sym, ext_params(v))
79
+ @tc.set_ext_param(platform, k.to_sym, ext_params(v, k))
80
80
  end
81
81
  end
82
82
 
@@ -84,9 +84,9 @@ module LtiXml
84
84
  @tc.set_ext_param(platform, :environments, PandaPal.lti_environments)
85
85
  end
86
86
 
87
- def ext_params(options)
88
- url = options.delete(:url)
89
- options[:url] = [parsed_request_url.to_s, main_app.send([url, '_url'].join, only_path: true)].join
87
+ def ext_params(options, k)
88
+ options.delete(:route_helper_key)
89
+ options[:url] = PandaPal::LaunchUrlHelpers.absolute_launch_url(k.to_sym, host: parsed_request_url, launch_handler: nil)
90
90
  options
91
91
  end
92
92
  end
@@ -0,0 +1,69 @@
1
+ module PandaPal
2
+ module LaunchUrlHelpers
3
+ def self.absolute_launch_url(launch_type, host:, launch_handler: nil)
4
+ opts = PandaPal.lti_paths[launch_type]
5
+ final_url = launch_url(opts, launch_type: launch_type)
6
+
7
+ is_direct = opts[:route_helper_key].present? || !launch_handler.present?
8
+
9
+ if is_direct
10
+ return final_url if URI.parse(final_url).absolute?
11
+ return [host.to_s, final_url].join
12
+ else
13
+ launch_handler = resolve_route(launch_handler) if launch_handler.is_a?(Symbol)
14
+ return add_url_params([host.to_s, launch_handler].join, {
15
+ launch_type: launch_type,
16
+ })
17
+ end
18
+ end
19
+
20
+ def self.launch_url(opts, launch_type: nil)
21
+ url = launch_route(opts, launch_type: launch_type)
22
+ url = resolve_url_symbol(url) if url.is_a?(Symbol)
23
+ url
24
+ end
25
+
26
+ def self.launch_route(opts, launch_type: nil)
27
+ if opts.is_a?(Symbol) || opts.is_a?(String)
28
+ launch_type = opts.to_sym
29
+ opts = PandaPal.lti_paths[launch_type]
30
+ end
31
+
32
+ if opts[:route_helper_key]
33
+ opts[:route_helper_key].to_sym
34
+ else
35
+ opts[:url] ||= launch_type
36
+ opts[:url]
37
+ end
38
+ end
39
+
40
+ def self.resolve_url_symbol(sym, **opts)
41
+ sym = :"#{sym}_url" unless sym.to_s.ends_with?('_url')
42
+ opts[:only_path] = true unless opts.key?(:only_path)
43
+ resolve_route(:"MainApp/#{sym}", **opts)
44
+ end
45
+
46
+ def self.add_url_params(url, params)
47
+ uri = URI(url)
48
+ decoded_params = URI.decode_www_form(uri.query || "")
49
+ params.each do |k, v|
50
+ decoded_params << [k, v]
51
+ end
52
+ uri.query = URI.encode_www_form(decoded_params)
53
+ uri.to_s
54
+ end
55
+
56
+ private
57
+
58
+ def self.resolve_route(key, *arguments, engine: 'PandaPal', **kwargs)
59
+ return key if key.is_a?(String)
60
+
61
+ key_bits = key.to_s.split('/')
62
+ key_bits.prepend(engine) if key_bits.count == 1
63
+ key_bits[0] = key_bits[0].classify
64
+
65
+ engine = key_bits[0] == 'MainApp' ? Rails.application : (key_bits[0].constantize)::Engine
66
+ engine.routes.url_helpers.send(key_bits[1], *arguments, **kwargs)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,88 @@
1
+ module PandaPal
2
+ class LtiJwtValidator
3
+ attr_reader :errors
4
+
5
+ def initialize(jwt, client_id)
6
+ @jwt = jwt
7
+ @client_id = client_id
8
+ @errors = []
9
+ end
10
+
11
+ def valid?
12
+ verify_audience(@jwt)
13
+ verify_sub(@jwt)
14
+ verify_nonce(@jwt)
15
+ verify_issued_at(@jwt)
16
+ verify_expiration(@jwt)
17
+ errors.empty?
18
+ end
19
+
20
+ private
21
+
22
+ def verify_audience(jwt)
23
+ aud = jwt[:aud]
24
+ aud_array = [*aud]
25
+ errors << 'Audience must be a string or Array of strings.' unless aud_array.all? { |a| a.is_a? String }
26
+ if jwt.key? :azp
27
+ verify_azp(aud, jwt[:azp])
28
+ else
29
+ errors << 'Audience not found' unless public_key_matches_one_of_client_ids(aud)
30
+ end
31
+ end
32
+
33
+ def verify_azp(aud, azp)
34
+ errors << 'Audience does not contain/match Authorized Party' unless azp_in_aud(aud, azp)
35
+ errors << 'Audience does not match Platform client_id' unless public_key_matches_one_of_client_ids(aud)
36
+ end
37
+
38
+ def verify_sub(jwt)
39
+ errors << 'Subject not present' if jwt[:sub].blank?
40
+ end
41
+
42
+ def verify_issued_at(jwt)
43
+ lower_bound = issued_at_lower_bound
44
+ errors << "Issued at of #{jwt[:iat]} not between #{lower_bound.to_i} and #{Time.zone.now.to_i}" unless Time.zone.at(
45
+ jwt[:iat]
46
+ ).between?(
47
+ lower_bound, Time.zone.now
48
+ )
49
+ end
50
+
51
+ def verify_expiration(jwt)
52
+ now = Time.zone.now
53
+ errors << "Expiration time of #{jwt[:exp]} before #{now.to_i}" unless Time.zone.at(jwt[:exp]) > Time.zone.now
54
+ end
55
+
56
+ def verify_nonce(jwt)
57
+ unless jwt[:nonce].present?
58
+ errors << 'Nonce not present'
59
+ return
60
+ end
61
+
62
+ return unless defined?(Redis) && Redis.current
63
+
64
+ cache_key = "nonces/#{jwt[:nonce]}"
65
+ if Redis.current.get(cache_key)
66
+ errors << 'Nonce has been used already'
67
+ else
68
+ Redis.current.set(cache_key, Time.zone.now.to_i, ex: 5.minutes)
69
+ end
70
+ end
71
+
72
+ def azp_in_aud(aud, azp)
73
+ if aud.is_a? Array
74
+ aud.include? azp
75
+ else
76
+ aud == azp
77
+ end
78
+ end
79
+
80
+ def issued_at_lower_bound
81
+ ENV.key?('issued_at_minutes_ago') ? ENV['issued_at_minutes_ago'].minutes.ago : 10.minutes.ago
82
+ end
83
+
84
+ def public_key_matches_one_of_client_ids(aud)
85
+ (Array(aud) & Array(@client_id)).any?
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,11 @@
1
+ module PandaPal
2
+ module MiscHelper
3
+ def self.to_boolean(v)
4
+ if Rails.version < '5.0'
5
+ ActiveRecord::Type::Boolean.new.type_cast_from_user("0")
6
+ else
7
+ ActiveRecord::Type::Boolean.new.deserialize('0')
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,3 @@
1
- Dir[File.dirname(__FILE__) + "/organization/*.rb"].each { |file| require file }
2
-
3
1
  module PandaPal
4
2
  module OrganizationConcerns; end
5
3
 
@@ -7,7 +5,6 @@ module PandaPal
7
5
  include OrganizationConcerns::SettingsValidation
8
6
  include OrganizationConcerns::TaskScheduling if defined?(Sidekiq.schedule)
9
7
 
10
- attribute :settings
11
8
  serialize :settings, Hash
12
9
  attr_encrypted :settings, marshal: true, key: :encryption_key
13
10
  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
@@ -21,6 +18,12 @@ module PandaPal
21
18
  after_create :create_schema
22
19
  after_commit :destroy_schema, on: :destroy
23
20
 
21
+ if defined?(scheduled_task)
22
+ scheduled_task '0 0 3 * * *', :clean_old_sessions do
23
+ PandaPal::Session.where(panda_pal_organization: self).where('updated_at < ?', 1.week.ago).delete_all
24
+ end
25
+ end
26
+
24
27
  before_validation on: [:update] do
25
28
  errors.add(:name, 'should not be changed after creation') if name_changed?
26
29
  end
@@ -52,15 +52,31 @@ module PandaPal
52
52
 
53
53
  if settings.nil?
54
54
  errors << "Entry #{human_path} is required" if spec[:required]
55
- return
55
+ return errors
56
56
  end
57
57
 
58
58
  if spec[:type]
59
- resolved_type = spec[:type]
60
- resolved_type = resolved_type.constantize if resolved_type.is_a?(String)
61
- unless settings.is_a?(resolved_type)
59
+ norm_types = Array(spec[:type]).map do |t|
60
+ if [:bool, :boolean, 'Bool', 'Boolean'].include?(t)
61
+ 'Boolean'
62
+ elsif t.is_a?(String)
63
+ t.constantize
64
+ else
65
+ t
66
+ end
67
+ end
68
+
69
+ any_match = norm_types.any? do |t|
70
+ if t == 'Boolean'
71
+ settings == true || settings == false
72
+ else
73
+ settings.is_a?(t)
74
+ end
75
+ end
76
+
77
+ unless any_match
62
78
  errors << "Expected #{human_path} to be a #{spec[:type]}. Was a #{settings.class.to_s}"
63
- return
79
+ return errors
64
80
  end
65
81
  end
66
82
 
@@ -36,9 +36,36 @@ module PandaPal
36
36
  type: 'Hash',
37
37
  required: false,
38
38
  properties: _schedule_descriptors.keys.reduce({}) do |hash, k|
39
+ desc = _schedule_descriptors[k]
40
+
39
41
  hash.tap do |hash|
42
+ kl = ' ' * (k.to_s.length - 4)
40
43
  hash[k.to_sym] = hash[k.to_s] = {
41
44
  required: false,
45
+ description: <<~MARKDOWN,
46
+ Override schedule for '#{k.to_s}' task.
47
+
48
+ **Default**: #{desc[:schedule].is_a?(String) ? desc[:schedule] : '<Computed>'}
49
+
50
+ Set to `false` to disable or supply a Cron string:
51
+ ```yaml
52
+ #{k.to_s}: 0 0 0 * * * America/Denver
53
+ ##{kl} │ │ │ │ │ │ └── Timezone (Optional)
54
+ ##{kl} │ │ │ │ │ └── Day of Week
55
+ ##{kl} │ │ │ │ └── Month
56
+ ##{kl} │ │ │ └── Day of Month
57
+ ##{kl} │ │ └── Hour
58
+ ##{kl} │ └── Minute
59
+ ##{kl} └── Second (Optional)
60
+ ````
61
+ MARKDOWN
62
+ json_schema: {
63
+ oneOf: [
64
+ { type: 'string', pattern: '^((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,6})(\w+\/\w+)?$' },
65
+ { enum: [false] },
66
+ ],
67
+ default: desc[:schedule].is_a?(String) ? desc[:schedule] : '0 0 3 * * * America/Denver',
68
+ },
42
69
  validate: ->(value, *args, errors:, **kwargs) {
43
70
  begin
44
71
  Rufus::Scheduler.parse(value) if value
@@ -66,6 +93,11 @@ module PandaPal
66
93
  }
67
94
  end
68
95
 
96
+ def remove_scheduled_task(name_or_method)
97
+ dval = _schedule_descriptors.delete(name_or_method.to_s)
98
+ Rails.logger.warn("No task with key '#{name_or_method}' to delete!") unless dval.present?
99
+ end
100
+
69
101
  def sync_schedules
70
102
  # Ensure deleted Orgs are removed
71
103
  existing_orgs = pluck(:name)
@@ -0,0 +1,40 @@
1
+ module PandaPal
2
+ class Platform
3
+ def public_jwks
4
+ response = HTTParty.get(jwks_url)
5
+ return nil unless response.success?
6
+
7
+ JSON::JWK::Set.new(JSON.parse(response.body))
8
+ rescue
9
+ nil
10
+ end
11
+ end
12
+
13
+ class Platform::Canvas < Platform
14
+ attr_accessor :base_url
15
+
16
+ def initialize(base_url)
17
+ @base_url = base_url
18
+ end
19
+
20
+ def host
21
+ base_url
22
+ end
23
+
24
+ def jwks_url
25
+ "#{base_url}/api/lti/security/jwks"
26
+ end
27
+
28
+ def authentication_redirect_url
29
+ "#{base_url}/api/lti/authorize_redirect"
30
+ end
31
+
32
+ def grant_url
33
+ "#{base_url}/login/oauth2/token"
34
+ end
35
+ end
36
+
37
+ class Platform
38
+ CANVAS = Platform::Canvas.new('https://canvas.instructure.com')
39
+ end
40
+ end
@@ -0,0 +1 @@
1
+ <%= render "panda_pal/partials/auto_submit_form" %>
@@ -0,0 +1,9 @@
1
+ <%= form_tag(@form_action, method: @method, id: 'redirect-form') do %>
2
+ <% @form_data.each do |k, v| %>
3
+ <%= hidden_field_tag(k, v) %>
4
+ <% end %>
5
+ <% end %>
6
+
7
+ <script type="text/javascript" nonce="<%= content_security_policy_script_nonce %>">
8
+ document.getElementById('redirect-form').submit();
9
+ </script>
@@ -0,0 +1,27 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIEowIBAAKCAQEAt+Za2scYD223YjVvAeuxpYvBjTf4yp2Y3udPFllG85zIVH/K
3
+ XKsP4QVP9de/fh4FzouZPOfXMDY1KmWSlMK9jNYUm2rh0zp7+X/F6cnc5QMCzbqV
4
+ MNnfALI4kvGM8yNPLbMJx4nA47T7T9TmlMJUZQxtyvC8zxBMv8Mv4Ae0arXevpI1
5
+ Vcm/3Huz6dAJPRjTnL2CBsXrmNzf51OJU5r27HYFE4367g9njix1lGOcQqVDVjnT
6
+ uXFTlidfUa2Bp2v5owzZU7Fe9jSQmeAQpqQ/XdYzWQNEJPrz8ESz5jP3brJ4q5Y5
7
+ FaK9l8DHILqomGWhttYQ6IMCfdpLb4D4B2gcLQIDAQABAoIBAAuaXislmrAGhSaO
8
+ JoXhgCDo03p8iJcIIIgX4haP5XkjcERcl8EHDgZtlmD1juB/NnCUwENmgV5KXUpi
9
+ hEAclWcYbs5rjPoN25qfZDZfBS/x47BlUFp3tKlPlWA4G2OP28QPYtOTLndviNe9
10
+ oBrMtBR4F0lRrSgHaEBFKXUiJ1EAMycKcXab63HA7Fp9HArLAu0NM0VWCsJBmxB/
11
+ BprW4LtYRG7iJl9kfsHyAzOaAV5H1zLqhfA2YvEI2XADrILQtpPwZpmNKq/5L5On
12
+ RlGy1WcBgxj8cqWdqir1PxVN/xDKVnUT9Mrf+SHn7xzgDL2uXvhD1Qt/5iLeVlvN
13
+ nQTNJyECgYEA681T7A0oj9/FuqLwE1gxm+RUczv+EyXcNvHCUkpFSaiyEyqdQB9f
14
+ EpHgeg7pXmlRmUwNkT3ZAH7O0uv3CVR6b8j9N7XmYbmcuRU2A6loeiBB/HkOOt21
15
+ hxiC+tXD/K7c5BfRkThL6ca213xqztbqxxj/C8qGnWVaLc7SLPlJZfUCgYEAx6bp
16
+ 7tRGq8Q5xkyKvSDUSln9MI/iZjSysd44rTtj8Vo9LfQsBi5JbWm0+UEtaZoabeC1
17
+ xaAp/ZETLe4oGu7CJxBreSE+UE/a6zc5MIcJblP+8RIiAYkf67iHNwSbiqSBvPqj
18
+ S0HFxUZqw1NigmqsptHrPtvuTUZI8Hx3hKMgwlkCgYAa8RrlnZtE1QyChptnmmwQ
19
+ o8YCZJhjF7BRls3dGR9RizTNe9D7wpnaRVCgoZOIdgAcw9PJBIgGxnZbIxrWthBH
20
+ NW+5Lc9k2xBNFV9Wi8SkL4tajXpSv4I+LU7J2iLKfDBA33fSX9xMmafKdyy89VFd
21
+ 7j01264Fzc6/7SGWgeUhAQKBgDKGSgMXkz7arKhDLIUKLs8WEN3eO7QTt/kNPJiS
22
+ RAuLA5qChTWXNxvKOXMujFiCGBggWr/FdXrm4MypzVpre5S5Mgl4YTWfz83grsda
23
+ FQfnl8fYB+UNl5dmnklNEDO4x+BUKUjdPzhaRqBhlLdeWYzp6LeCnr7Nf53kUbau
24
+ NZcZAoGBAJcYyWegsQCEVMY/ck42uNmZ00DmK1XSAU7hYulBo8iD3OqvXZ7Etppw
25
+ SaRhSTCoTsLBWlqGccNDGoXH/r2/jJAFb8hUjGa2Z5dKP9byWMONEQEh6g8wzL/w
26
+ XRthIsgdIE58a+cJGwHh/raUQwvD72S1C/epQSYcLe3TdUumtZm/
27
+ -----END RSA PRIVATE KEY-----
@@ -1,4 +1,14 @@
1
1
  PandaPal::Engine.routes.draw do
2
- get '/config' => 'lti#tool_config'
3
- get '/launch' => 'lti#launch'
2
+ get '/config' => 'lti_v1_p0#tool_config' # Legacy Support
3
+
4
+ scope '/v1p0', as: 'v1p0' do
5
+ get '/config' => 'lti_v1_p0#tool_config'
6
+ end
7
+
8
+ scope '/v1p3', as: 'v1p3' do
9
+ get '/config' => 'lti_v1_p3#tool_config'
10
+ post '/oidc_login' => 'lti_v1_p3#login'
11
+ post '/resource_link_request' => 'lti_v1_p3#resource_link_request'
12
+ get '/public_jwks' => 'lti_v1_p3#public_jwks'
13
+ end
4
14
  end
@@ -1,4 +1,4 @@
1
- class CreatePandaPalOrganizations < ActiveRecord::Migration[5.1]
1
+ class CreatePandaPalOrganizations < ActiveRecord::Migration
2
2
  def change
3
3
  create_table :panda_pal_organizations do |t|
4
4
  t.string :name
@@ -1,4 +1,4 @@
1
- class CreatePandaPalSessions < ActiveRecord::Migration[5.1]
1
+ class CreatePandaPalSessions < ActiveRecord::Migration
2
2
  def change
3
3
  create_table :panda_pal_sessions do |t|
4
4
  t.string :session_key
@@ -1,4 +1,4 @@
1
- class AddPandaPalOrganizationToSession < ActiveRecord::Migration[5.1]
1
+ class AddPandaPalOrganizationToSession < ActiveRecord::Migration
2
2
  def change
3
3
  add_column :panda_pal_sessions, :panda_pal_organization_id, :integer
4
4
  add_index :panda_pal_sessions, :panda_pal_organization_id
@@ -1,4 +1,4 @@
1
- class AddSalesforceIdToOrganizations < ActiveRecord::Migration[5.1]
1
+ class AddSalesforceIdToOrganizations < ActiveRecord::Migration
2
2
  def change
3
3
  add_column :panda_pal_organizations, :salesforce_id, :string, unique: true
4
4
  end
@@ -1,4 +1,4 @@
1
- class EncryptOrganizationSettings < ActiveRecord::Migration[5.1]
1
+ class EncryptOrganizationSettings < ActiveRecord::Migration
2
2
  def up
3
3
  # don't rerun this if it was already run before we renamed the migration.
4
4
  existing_versions = execute ("SELECT * from schema_migrations where version = '30171205183457'")
@@ -1,4 +1,4 @@
1
- class RemoveOldOrganizationSettings < ActiveRecord::Migration[5.1]
1
+ class RemoveOldOrganizationSettings < ActiveRecord::Migration
2
2
  def current_tenant
3
3
  @current_tenant ||= PandaPal::Organization.find_by_name(Apartment::Tenant.current)
4
4
  end
@@ -10,14 +10,16 @@ class RemoveOldOrganizationSettings < ActiveRecord::Migration[5.1]
10
10
  execute "DELETE from schema_migrations where version = '30171205194657'"
11
11
  return
12
12
  end
13
+
13
14
  # migrations run for public and local tenants. However, PandaPal::Organization
14
15
  # is going to always go to public tenant. So don't do this active record
15
16
  # stuff unless we are on the public tenant.
16
- if current_tenant == 'public'
17
+ if Apartment::Tenant.current == 'public'
17
18
  #PandaPal::Organization.connection.schema_cache.clear!
18
19
  #PandaPal::Organization.reset_column_information
19
20
  PandaPal::Organization.find_each do |o|
20
21
  # Would like to just be able to do this:
22
+ # PandaPal::Organization.reset_column_information
21
23
  # o.settings = YAML.load(o.old_settings)
22
24
  # o.save!
23
25
  # but for some reason that is always making the settings null. Instead we will encrypt the settings manually.
@@ -26,14 +28,17 @@ class RemoveOldOrganizationSettings < ActiveRecord::Migration[5.1]
26
28
  key = o.encryption_key
27
29
  encrypted_settings = PandaPal::Organization.encrypt_settings(YAML.load(o.old_settings), iv: iv, key: key)
28
30
  o.update_columns(encrypted_settings_iv: [iv].pack("m"), encrypted_settings: encrypted_settings)
31
+ o = PandaPal::Organization.find_by!(name: o.name)
32
+ raise "Failed to migrate PandaPal Settings" if o.settings != YAML.load(o.old_settings)
29
33
  end
30
34
  end
35
+
31
36
  remove_column :panda_pal_organizations, :old_settings
32
37
  end
33
38
 
34
39
  def down
35
40
  add_column :panda_pal_organizations, :old_settings, :text
36
- if current_tenant == 'public'
41
+ if Apartment::Tenant.current == 'public'
37
42
  PandaPal::Organization.find_each do |o|
38
43
  o.old_settings = o.settings.to_yaml
39
44
  o.save