panda_pal 5.6.7.beta1 → 5.6.7

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
2
  SHA256:
3
- metadata.gz: 6e63f91a338bf3876346da15600d8d51de860c1b7eca7cb988f54d1edcd9f425
4
- data.tar.gz: 34aa9d21697f44409ae69fb16e6bf412d2d6c4c00a492c6c745e2202625d90d5
3
+ metadata.gz: c335f4ab51dec72916a2a630af60dbbed1f681bcd220cbf4585f5291127ca8ac
4
+ data.tar.gz: 3031b40de1ba6899077609843b1ed57d30cec38867f68f3c9c45281affea54c2
5
5
  SHA512:
6
- metadata.gz: fca067f026f174fb1ce47561c0adca4ec454a61ae5e03437dbf8b7ebfb784aa2624b4e232ece1fbb642442ac3fe7c5c0f2169a9de1b71c37e8430409133e045a
7
- data.tar.gz: 3de864a856dcb3928ef53066275946a24136c4cb877207090110aaf5a9b3cceca095748860d6bcbc132956570bd893dd0ede48b9672fd8025bcc3526bee383cc
6
+ metadata.gz: 7c225b256e06497ddc5ca6faf9604a28cb9290d0b37911ad39ac00e88beb2d3041541d758e55e992608db79ca9b5249c8aaf0ca69310b651f6635095fc7a2650
7
+ data.tar.gz: 7d2e7c371bd4bab83ee8c2e001d8affc48d0f5e88b2c21476b181c134a37de9db535c527b41812759eef0d0b96cc7d6bb3a427ef7187f5bc9d3efca145f59838
@@ -12,14 +12,11 @@ module PandaPal
12
12
  around_action :switch_tenant, only: [:resource_link_request]
13
13
 
14
14
  def login
15
- # TODO Figure out how to make this support deployment_ids
16
- # May need to move Platform#jwks_url and Platform#authentication_redirect_url to
17
- # a new GenericPlatform class/model and then associate the here-generated Session with
18
- # the public tenant, re-homing it once we have a deployment_id available
19
- # ... or create a new AuthState model instead of using Session here
20
- @organization = PandaPal::Organization.find_by!(key: params[:client_id])
15
+ @current_lti_platform = PandaPal::Platform.resolve_platform(params)
21
16
 
17
+ current_session_data[:lti_platform] = @current_lti_platform&.serialize
22
18
  current_session_data[:lti_oauth_nonce] = SecureRandom.uuid
19
+ current_session.panda_pal_organization_id = -1
23
20
 
24
21
  @form_action = current_lti_platform.authentication_redirect_url
25
22
  @method = :post
@@ -39,6 +36,7 @@ module PandaPal
39
36
 
40
37
  def resource_link_request
41
38
  # Redirect to correct region/env?
39
+
42
40
  if params[:launch_type]
43
41
  current_session_data.merge!({
44
42
  lti_version: 'v1p3',
@@ -88,20 +88,8 @@ module PandaPal
88
88
  end
89
89
  end
90
90
 
91
- def lti_platform
92
- lti_platform_type.new(self)
93
- end
94
-
95
- def lti_platform_type
96
- platform = PandaPal.lti_options[:platform] || 'canvas.instructure.com'
97
- case platform
98
- when 'canvas.instructure.com'
99
- PandaPal::Platform::Canvas
100
- # when 'bridgeapp.com'
101
- # TODO Add support for Bridge?
102
- else
103
- raise "Unknown platform '#{platform}'"
104
- end
91
+ def self.resolve_platform(hint)
92
+ iss = hint["iss"]
105
93
  end
106
94
 
107
95
  def rename!(new_name)
@@ -114,25 +102,15 @@ module PandaPal
114
102
  switch_tenant if do_switch
115
103
  end
116
104
 
117
- def respond_to_missing?(name, include_private = false)
118
- (platform_org_extension_module&.instance_method(name) rescue nil) || super
119
- end
120
-
121
- def method_missing(method, *args, **kwargs, &block)
122
- ext = platform_org_extension_module
123
- if (ext.instance_method(method) rescue nil)
124
- ext.instance_method(method).bind_call(self, *args, **kwargs, &block)
125
- else
126
- super
105
+ if !PandaPal.lti_options[:platform].present? || PandaPal.lti_options[:platform].is_a?(String)
106
+ platform_cls = Platform.resolve_platform_class(nil) rescue nil
107
+ if platform_cls && defined?(platform_cls::OrgExtension)
108
+ include platform_cls::OrgExtension
127
109
  end
128
110
  end
129
111
 
130
112
  private
131
113
 
132
- def platform_org_extension_module
133
- defined?(lti_platform_type::OrgExtension) ? lti_platform_type::OrgExtension : nil
134
- end
135
-
136
114
  def create_schema
137
115
  Apartment::Tenant.create name
138
116
  end
@@ -74,6 +74,8 @@ module PandaPal
74
74
  end
75
75
 
76
76
  def validate_settings
77
+ return unless defined?(Rails::Console)
78
+
77
79
  validate_settings_level(settings || {}, settings_structure).each do |err|
78
80
  errors[:settings] << err
79
81
  end
@@ -0,0 +1,163 @@
1
+ begin
2
+ require 'bearcat'
3
+ rescue LoadError
4
+ end
5
+
6
+ module PandaPal
7
+ class Platform::Canvas < Platform
8
+ attr_accessor :organization
9
+
10
+ KNOWN_ISSUERS = [
11
+ "https://canvas.instructure.com",
12
+ "https://canvas.beta.instructure.com",
13
+ "https://canvas.test.instructure.com",
14
+ ]
15
+
16
+ def jwks_url
17
+ "#{@issuer}/api/lti/security/jwks"
18
+ end
19
+
20
+ def authentication_redirect_url
21
+ "#{@issuer}/api/lti/authorize_redirect"
22
+ end
23
+
24
+ def grant_url
25
+ "#{@issuer}/login/oauth2/token"
26
+ end
27
+
28
+ module OrgExtension
29
+ extend ActiveSupport::Concern
30
+
31
+ def install_lti(host: nil, context: :root_account, version: 'v1p1', exists: :error)
32
+ raise "Automatically installing this LTI requires Bearcat." unless defined?(Bearcat)
33
+
34
+ context = context.to_s
35
+ context = 'account/self' if context == 'root_account'
36
+ cid, ctype = context.split(/[\.\/]/).reverse
37
+ ctype ||= 'account'
38
+
39
+ existing_installs = bearcat_client.send(:"#{ctype}_external_tools", cid).all_pages_each.filter do |cet|
40
+ cet[:consumer_key] == self.key
41
+ end
42
+
43
+ if existing_installs.present?
44
+ case exists
45
+ when :error
46
+ raise "Tool with key #{self.key} already installed"
47
+ when :duplicate
48
+ when :replace
49
+ existing_installs.each do |install|
50
+ bearcat_client.send(:"delete_#{ctype}_external_tool", cid, install[:id])
51
+ end
52
+ else
53
+ raise "exists: #{exists} is not supported"
54
+ end
55
+ end
56
+
57
+ # TODO LTI 1.3 Support
58
+
59
+ conf = {
60
+ name: PandaPal.lti_options[:title],
61
+ description: PandaPal.lti_options[:description],
62
+ consumer_key: self.key,
63
+ shared_secret: self.secret,
64
+ privacy_level: "public",
65
+ config_type: 'by_url',
66
+ config_url: PandaPal::LaunchUrlHelpers.resolve_route(:v1p0_config_url, host: host),
67
+ }
68
+
69
+ bearcat_client.send(:"create_#{ctype}_external_tool", cid, conf)
70
+ end
71
+
72
+ def reinstall_lti!(*args, **kwargs)
73
+ install_lti(*args, exists: :replace, **kwargs)
74
+ end
75
+
76
+ def lti_api_configuration(host: nil)
77
+ PandaPal::LaunchUrlHelpers.with_uri_host(host) do
78
+ domain = PandaPal.lti_properties[:domain] || host.host
79
+ launch_url = PandaPal.lti_options[:secure_launch_url] ||
80
+ "#{domain}#{PandaPal.lti_options[:secure_launch_path]}" ||
81
+ PandaPal.lti_options[:launch_url] ||
82
+ "#{domain}#{PandaPal.lti_options[:launch_path]}" ||
83
+ domain
84
+
85
+ lti_json = {
86
+ name: PandaPal.lti_options[:title],
87
+ description: PandaPal.lti_options[:description],
88
+ domain: host.host,
89
+ url: launch_url,
90
+ consumer_key: self.key,
91
+ shared_secret: self.secret,
92
+ privacy_level: "public",
93
+
94
+ custom_fields: {},
95
+
96
+ environments: PandaPal.lti_environments,
97
+ }
98
+
99
+ lti_json = lti_json.with_indifferent_access
100
+ lti_json.merge!(PandaPal.lti_properties)
101
+
102
+ (PandaPal.lti_options[:custom_fields] || []).each do |k, v|
103
+ lti_json[:custom_fields][k] = v
104
+ end
105
+
106
+ PandaPal.lti_paths.each do |k, options|
107
+ options = PandaPal::LaunchUrlHelpers.normalize_lti_launch_desc(options)
108
+ options[:url] = PandaPal::LaunchUrlHelpers.absolute_launch_url(
109
+ k.to_sym,
110
+ host: host,
111
+ launch_handler: :v1p0_launch_path,
112
+ default_auto_launch: false
113
+ ).to_s
114
+ lti_json[k] = options
115
+ end
116
+
117
+ lti_json
118
+ end
119
+ end
120
+
121
+ def canvas_url
122
+ PandaPal::Platform.find_org_setting([
123
+ "canvas.base_url",
124
+ "canvas_url",
125
+ "canvas_base_url",
126
+ "canvas.url",
127
+ "base_url",
128
+ ], self) || (Rails.env.development? && 'http://localhost:3000') || 'https://canvas.instructure.com'
129
+ end
130
+
131
+ def canvas_api_token
132
+ PandaPal::Platform.find_org_setting([
133
+ "canvas.api_token",
134
+ "canvas.api_key",
135
+ "canvas.token",
136
+ "canvas_api_token",
137
+ "canvas_token",
138
+ "api_token",
139
+ ], self)
140
+ end
141
+
142
+ def root_account_info
143
+ Rails.cache.fetch("panda_pal/org:#{name}/root_account_info", expires_in: 24.hours) do
144
+ response = bearcat_client.account("self")
145
+ response = bearcat_client.account(response[:root_account_id]) if response[:root_account_id].present?
146
+ response
147
+ end
148
+ end
149
+
150
+ if defined?(Bearcat)
151
+ def bearcat_client
152
+ return canvas_sync_client if defined?(canvas_sync_client)
153
+
154
+ Bearcat::Client.new(
155
+ prefix: canvas_url,
156
+ token: canvas_token,
157
+ master_rate_limit: (Rails.env.production? && !!defined?(Redis) && ENV['REDIS_URL'].present?),
158
+ )
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -1,187 +1,106 @@
1
- begin
2
- require 'bearcat'
3
- rescue LoadError
4
- end
5
1
 
6
2
  require 'httparty'
7
3
 
8
4
  module PandaPal
9
5
  class Platform
6
+ require_relative "platform/canvas"
7
+
8
+ def initialize(params)
9
+ @issuer = params[:iss]
10
+ end
11
+
10
12
  def public_jwks
11
13
  require "json/jwt"
12
14
 
13
- response = HTTParty.get(jwks_url)
14
- return nil unless response.success?
15
+ jwk_json = Rails.cache.fetch("panda_pal/jwks/#{jwks_url}")
16
+ jwk_json ||= begin
17
+ response = HTTParty.get(jwks_url)
18
+ response.success? ? response.body : nil
19
+ end
15
20
 
16
- JSON::JWK::Set.new(JSON.parse(response.body))
17
- rescue
18
- nil
19
- end
21
+ return nil unless jwk_json.present?
20
22
 
21
- protected
23
+ Rails.cache.write("panda_pal/jwks/#{jwks_url}", jwk_json, expires_in: 24.hours)
22
24
 
23
- def self.find_org_setting(paths, org = current_organization)
24
- paths.each do |p|
25
- p = p.split('.').map(&:to_sym)
26
- val = org.settings.dig(*p)
27
- return val if val.present?
28
- end
25
+ JSON::JWK::Set.new(JSON.parse(jwk_json))
26
+ rescue
29
27
  nil
30
28
  end
31
- end
32
-
33
- class Platform::Canvas < Platform
34
- attr_accessor :organization
35
29
 
36
- def initialize(org)
37
- @organization = org
30
+ def self.deserialize(params)
31
+ params[:platform_class].safe_constantize.new(params)
38
32
  end
39
33
 
40
- def host
41
- base_url
34
+ def serialize
35
+ {
36
+ platform_class: self.class.to_s,
37
+ iss: @issuer,
38
+ }
42
39
  end
43
40
 
44
- def jwks_url
45
- "#{base_url}/api/lti/security/jwks"
41
+ def self.resolve_platform(params)
42
+ resolved = resolve_raw_platform(params)
43
+ resolved.is_a?(Class) ? resolved.new({ iss: params[:iss] }) : resolved
46
44
  end
47
45
 
48
- def authentication_redirect_url
49
- "#{base_url}/api/lti/authorize_redirect"
46
+ def self.resolve_platform_class(params)
47
+ resolved = resolve_raw_platform(params)
48
+ resolved.is_a?(Class) ? resolved : resolved.class
50
49
  end
51
50
 
52
- def grant_url
53
- "#{base_url}/login/oauth2/token"
54
- end
51
+ def self.resolve_raw_platform(params)
52
+ platform = PandaPal.lti_options[:platform] || "canvas.instructure.com"
55
53
 
56
- def base_url; @organization.canvas_url; end # TODO Dissolve?
57
-
58
- module OrgExtension
59
- def install_lti(host: nil, context: :root_account, version: 'v1p1', exists: :error)
60
- raise "Automatically installing this LTI requires Bearcat." unless defined?(Bearcat)
61
-
62
- context = context.to_s
63
- context = 'account/self' if context == 'root_account'
64
- cid, ctype = context.split(/[\.\/]/).reverse
65
- ctype ||= 'account'
66
-
67
- existing_installs = bearcat_client.send(:"#{ctype}_external_tools", cid).all_pages_each.filter do |cet|
68
- cet[:consumer_key] == self.key
69
- end
70
-
71
- if existing_installs.present?
72
- case exists
73
- when :error
74
- raise "Tool with key #{self.key} already installed"
75
- when :duplicate
76
- when :replace
77
- existing_installs.each do |install|
78
- bearcat_client.send(:"delete_#{ctype}_external_tool", cid, install[:id])
79
- end
80
- else
81
- raise "exists: #{exists} is not supported"
82
- end
83
- end
84
-
85
- # TODO LTI 1.3 Support
86
-
87
- conf = {
88
- name: PandaPal.lti_options[:title],
89
- description: PandaPal.lti_options[:description],
90
- consumer_key: self.key,
91
- shared_secret: self.secret,
92
- privacy_level: "public",
93
- config_type: 'by_url',
94
- config_url: PandaPal::LaunchUrlHelpers.resolve_route(:v1p0_config_url, host: host),
95
- }
96
-
97
- bearcat_client.send(:"create_#{ctype}_external_tool", cid, conf)
98
- end
54
+ if platform.is_a? Symbol
55
+ platform_const = Object.send(platform, params)
56
+ return platform_const if platform_const
99
57
 
100
- def lti_api_configuration(host: nil)
101
- PandaPal::LaunchUrlHelpers.with_uri_host(host) do
102
- domain = PandaPal.lti_properties[:domain] || host.host
103
- launch_url = PandaPal.lti_options[:secure_launch_url] ||
104
- "#{domain}#{PandaPal.lti_options[:secure_launch_path]}" ||
105
- PandaPal.lti_options[:launch_url] ||
106
- "#{domain}#{PandaPal.lti_options[:launch_path]}" ||
107
- domain
108
-
109
- lti_json = {
110
- name: PandaPal.lti_options[:title],
111
- description: PandaPal.lti_options[:description],
112
- domain: host.host,
113
- url: launch_url,
114
- consumer_key: self.key,
115
- shared_secret: self.secret,
116
- privacy_level: "public",
117
-
118
- custom_fields: {},
119
-
120
- environments: PandaPal.lti_environments,
121
- }
122
-
123
- lti_json = lti_json.with_indifferent_access
124
- lti_json.merge!(PandaPal.lti_properties)
125
-
126
- (PandaPal.lti_options[:custom_fields] || []).each do |k, v|
127
- lti_json[:custom_fields][k] = v
128
- end
129
-
130
- PandaPal.lti_paths.each do |k, options|
131
- options = PandaPal::LaunchUrlHelpers.normalize_lti_launch_desc(options)
132
- options[:url] = PandaPal::LaunchUrlHelpers.absolute_launch_url(
133
- k.to_sym,
134
- host: host,
135
- launch_handler: :v1p0_launch_path,
136
- default_auto_launch: false
137
- ).to_s
138
- lti_json[k] = options
139
- end
140
-
141
- lti_json
142
- end
143
- end
58
+ elsif platform.is_a? Proc
59
+ platform_const = platform.call(params)
60
+ return platform_const if platform_const
144
61
 
145
- def canvas_url
146
- PandaPal::Platform.find_org_setting([
147
- "canvas.base_url",
148
- "canvas_url",
149
- "canvas_base_url",
150
- "canvas.url",
151
- "base_url",
152
- ], self) || (Rails.env.development? && 'http://localhost:3000') || 'https://canvas.instructure.com'
153
- end
62
+ else
63
+ return Platform::Canvas if platform == "canvas.instructure.com"
154
64
 
155
- def canvas_api_token
156
- PandaPal::Platform.find_org_setting([
157
- "canvas.api_token",
158
- "canvas.api_key",
159
- "canvas.token",
160
- "canvas_api_token",
161
- "canvas_token",
162
- "api_token",
163
- ], self)
65
+ platform_const = platform.safe_constantize
66
+ return platform_const if platform_const.is_a? Class
164
67
  end
165
68
 
166
- def root_account_info
167
- Rails.cache.fetch("panda_pal/org:#{name}/root_account_info", expires_in: 24.hours) do
168
- response = bearcat_client.account("self")
169
- response = bearcat_client.account(response[:root_account_id]) if response[:root_account_id].present?
170
- response
171
- end
172
- end
69
+ # TODO Default logic?
70
+ return Platform::Canvas if Platform::Canvas::KNOWN_ISSUERS.include?(params[:iss])
71
+
72
+ raise "Unknown platform '#{platform}'"
73
+ end
173
74
 
174
- if defined?(Bearcat)
175
- def bearcat_client
176
- return canvas_sync_client if defined?(canvas_sync_client)
75
+ def self.materialize_extension(org)
76
+ return nil unless defined?(self::OrgExtension)
77
+
78
+ ext_mod = self::OrgExtension
79
+ org = org.dup
80
+ org.extend(ext_mod)
81
+
82
+ org
83
+ end
84
+
85
+ protected
177
86
 
178
- Bearcat::Client.new(
179
- prefix: canvas_url,
180
- token: canvas_token,
181
- master_rate_limit: (Rails.env.production? && !!defined?(Redis) && ENV['REDIS_URL'].present?),
182
- )
183
- end
87
+ def self.find_org_setting(paths, org = current_organization)
88
+ paths.each do |p|
89
+ p = p.split('.').map(&:to_sym)
90
+ val = org.settings.dig(*p)
91
+ return val if val.present?
184
92
  end
93
+ nil
94
+ end
95
+ end
96
+
97
+ class Platform::Generic < Platform
98
+ attr_reader :jwks_url, :authentication_redirect_url, :grant_url
99
+
100
+ def init(base_url, jwks: "/api/lti/security/jwks", auth_redirect: "/api/lti/authorize_redirect", grant: "/login/oauth2/token")
101
+ @jwks_url = URI.join(base_url, jwks).to_s
102
+ @authentication_redirect_url = URI.join(base_url, auth_redirect).to_s
103
+ @grant_url = URI.join(base_url, grant).to_s
185
104
  end
186
105
  end
187
106
  end
@@ -1,13 +1,17 @@
1
1
  module PandaPal
2
2
  class Session < ActiveRecord::Base
3
- belongs_to :panda_pal_organization, class_name: 'PandaPal::Organization'
4
-
5
- validates :panda_pal_organization_id, presence: true
3
+ belongs_to :panda_pal_organization, class_name: 'PandaPal::Organization', optional: true
6
4
 
7
5
  after_initialize do
8
6
  self.session_key ||= SecureRandom.urlsafe_base64(60)
9
7
  end
10
8
 
9
+ def lti_platform
10
+ return nil unless data[:lti_platform].present?
11
+
12
+ @lti_platform ||= Platform.deserialize(data[:lti_platform])
13
+ end
14
+
11
15
  class DataSerializer
12
16
  def self.load(str)
13
17
  return {} unless str.present?
@@ -13,7 +13,7 @@ module PandaPal::Helpers
13
13
  end
14
14
 
15
15
  def current_lti_platform
16
- @current_lti_platform ||= current_organization.lti_platform
16
+ @current_lti_platform ||= current_session(create_missing: false)&.lti_platform
17
17
  end
18
18
 
19
19
  def lti_launch_params
@@ -55,6 +55,8 @@ module PandaPal::Helpers
55
55
  end
56
56
 
57
57
  def validate_v1p3_launch
58
+ require "json/jwt"
59
+
58
60
  decoded_jwt = JSON::JWT.decode(params.require(:id_token), :skip_verification)
59
61
  raise JSON::JWT::VerificationFailed, 'error decoding id_token' if decoded_jwt.blank?
60
62
 
@@ -70,14 +72,17 @@ module PandaPal::Helpers
70
72
 
71
73
  raise JSON::JWT::VerificationFailed, 'Unrecognized Organization' unless @organization.present?
72
74
 
75
+ params[:session_key] = params[:state]
76
+
73
77
  decoded_jwt.verify!(current_lti_platform.public_jwks)
74
78
 
75
- params[:session_key] = params[:state]
76
79
  raise JSON::JWT::VerificationFailed, 'State is invalid' unless current_session_data[:lti_oauth_nonce] == decoded_jwt['nonce']
77
80
 
78
81
  jwt_verifier = PandaPal::LtiJwtValidator.new(decoded_jwt, client_id)
79
82
  raise JSON::JWT::VerificationFailed, jwt_verifier.errors unless jwt_verifier.valid?
80
83
 
84
+ current_session.update(panda_pal_organization: @organization)
85
+
81
86
  @decoded_lti_jwt = decoded_jwt
82
87
  rescue JSON::JWT::VerificationFailed => e
83
88
  payload = Array(e.message)
@@ -124,7 +129,7 @@ module PandaPal::Helpers
124
129
 
125
130
  def find_or_create_session(key:)
126
131
  if key == :create
127
- PandaPal::Session.new(panda_pal_organization_id: current_organization.id)
132
+ PandaPal::Session.new(panda_pal_organization_id: current_organization&.id)
128
133
  else
129
134
  PandaPal::Session.find_by(session_key: key)
130
135
  end
@@ -29,9 +29,11 @@ module PandaPal
29
29
 
30
30
  # Allow stuff like rack-mini-profiler to work in development:
31
31
  # https://github.com/MiniProfiler/rack-mini-profiler/issues/327
32
- # DON'T ENABLE THIS FOR PRODUCTION!
33
32
  csp_entry(:script_src, "'unsafe-eval'")
34
33
 
34
+ # React Devtools
35
+ csp_entry(:script_src, "'unsafe-inline'")
36
+
35
37
  # Detect and permit Scout APM in Dev
36
38
  if MiscHelper.to_boolean(ENV['SCOUT_DEV_TRACE'])
37
39
  csp_entry(:default_src, 'https://scoutapm.com')
@@ -1,3 +1,3 @@
1
1
  module PandaPal
2
- VERSION = "5.6.7.beta1"
2
+ VERSION = "5.6.7"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: panda_pal
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.6.7.beta1
4
+ version: 5.6.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure ProServe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-31 00:00:00.000000000 Z
11
+ date: 2023-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -315,6 +315,7 @@ files:
315
315
  - app/models/panda_pal/organization_concerns/settings_validation.rb
316
316
  - app/models/panda_pal/organization_concerns/task_scheduling.rb
317
317
  - app/models/panda_pal/platform.rb
318
+ - app/models/panda_pal/platform/canvas.rb
318
319
  - app/models/panda_pal/session.rb
319
320
  - app/views/layouts/panda_pal/application.html.erb
320
321
  - app/views/panda_pal/lti/launch.html.erb
@@ -402,9 +403,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
402
403
  version: '0'
403
404
  required_rubygems_version: !ruby/object:Gem::Requirement
404
405
  requirements:
405
- - - ">"
406
+ - - ">="
406
407
  - !ruby/object:Gem::Version
407
- version: 1.3.1
408
+ version: '0'
408
409
  requirements: []
409
410
  rubygems_version: 3.1.6
410
411
  signing_key: