panda_pal 5.1.0 → 5.2.0

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