coalescing_panda 5.0.7 → 5.1.2
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 +5 -5
- data/app/controllers/coalescing_panda/oauth2_controller.rb +1 -1
- data/app/views/coalescing_panda/canvas_batches/_canvas_batch_flash.html.haml +3 -3
- data/lib/coalescing_panda/controller_helpers.rb +26 -90
- data/lib/coalescing_panda/misc_helper.rb +2 -2
- data/lib/coalescing_panda/secure_headers.rb +0 -5
- data/lib/coalescing_panda/session_replacement.rb +185 -0
- data/lib/coalescing_panda/version.rb +1 -1
- data/spec/controllers/coalescing_panda/oauth2_controller_spec.rb +2 -2
- data/spec/models/coalescing_panda/canvas_api_auth_spec.rb +2 -2
- data/spec/spec_helper.rb +7 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: db38b8cdb32e3f3d41eab15a3df4542cb9feeb9b
|
4
|
+
data.tar.gz: 4cb1f0ec31e628852a3cfe2100b2ca31a91f94cd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d05f38b6ab1f690c9b581c59da23814f6b2b505be56bf2ab057b3099dd15497d98de974ce9f43f779e6b59e863e825fb690d146121915da348484af391798518
|
7
|
+
data.tar.gz: 3663adb9a48a839fed284a7132083335578074ce210b382cbac8e6cb4a0a98aa59d0033ad4cc6b8116205143b13a787d5ed56fa65ca7eef0f7e38b9ec23e551c
|
@@ -1,4 +1,4 @@
|
|
1
1
|
- if current_batch.present?
|
2
|
-
- path = CoalescingPanda::Engine.routes.url_helpers.canvas_batch_path(current_batch)
|
3
|
-
- clear_path = CoalescingPanda::Engine.routes.url_helpers.clear_batch_session_path
|
4
|
-
#batch-progress{data: {batch: current_batch.try(:to_json), url: path, clear_path: clear_path} }
|
2
|
+
- path = CoalescingPanda::Engine.routes.url_helpers.canvas_batch_path(current_batch) + "?encrypted_session_key=#{encrypted_session_key}"
|
3
|
+
- clear_path = CoalescingPanda::Engine.routes.url_helpers.clear_batch_session_path + "?encrypted_session_key=#{encrypted_session_key}"
|
4
|
+
#batch-progress{data: {batch: current_batch.try(:to_json), url: path, clear_path: clear_path} }
|
@@ -1,51 +1,17 @@
|
|
1
1
|
require 'browser'
|
2
|
+
require_relative 'session_replacement'
|
2
3
|
|
3
4
|
module CoalescingPanda
|
4
5
|
module ControllerHelpers
|
5
6
|
extend ActiveSupport::Concern
|
6
|
-
|
7
|
-
included do
|
8
|
-
alias_method :rails_session, :session
|
9
|
-
|
10
|
-
helper_method :encrypted_session_key, :current_session_data, :current_session
|
11
|
-
append_after_action :save_session, if: -> { @current_session && session_changed? }
|
12
|
-
end
|
13
|
-
|
14
|
-
class_methods do
|
15
|
-
def use_native_sessions
|
16
|
-
after_action do
|
17
|
-
rails_session['persistent_session_key'] = current_session.session_key if @current_session.present?
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def current_session
|
23
|
-
@current_session ||= (CoalescingPanda::PersistentSession.find_by(session_key: session_key) if session_key)
|
24
|
-
@current_session ||= (CoalescingPanda::PersistentSession.create_from_launch(params, current_lti_account.id) if current_lti_account.present?)
|
25
|
-
@current_session
|
26
|
-
end
|
7
|
+
include SessionReplacement
|
27
8
|
|
28
9
|
def current_lti_account
|
29
10
|
@account ||= (CoalescingPanda::LtiAccount.find_by!(key: organization_key) if organization_key)
|
30
11
|
@account ||= (CoalescingPanda::LtiAccount.find_by(id: organization_id) if organization_id)
|
31
12
|
@account
|
32
13
|
end
|
33
|
-
|
34
|
-
def current_session_data
|
35
|
-
current_session.data
|
36
|
-
end
|
37
|
-
|
38
|
-
def encrypted_session_key
|
39
|
-
msg_encryptor.encrypt_and_sign(current_session.session_key)
|
40
|
-
end
|
41
|
-
|
42
|
-
def save_session
|
43
|
-
current_session.try(:save)
|
44
|
-
end
|
45
|
-
|
46
|
-
def session_changed?
|
47
|
-
current_session.changed? && current_session.changes[:data].present?
|
48
|
-
end
|
14
|
+
def current_organization; current_lti_account; end
|
49
15
|
|
50
16
|
def canvas_oauth2(*roles)
|
51
17
|
return if have_session?
|
@@ -81,9 +47,7 @@ module CoalescingPanda
|
|
81
47
|
client = Bearcat::Client.new(prefix: uri.prefix)
|
82
48
|
state = SecureRandom.hex(32)
|
83
49
|
OauthState.create! state_key: state, data: { key: params['oauth_consumer_key'], user_id: user_id, api_domain: uri.api_domain }
|
84
|
-
|
85
|
-
redirect_url = [coalescing_panda_url, redirect_path.sub(/^\/lti/, '')].join
|
86
|
-
@canvas_url = client.auth_redirect_url(client_id, redirect_url, { state: state })
|
50
|
+
@canvas_url = client.auth_redirect_url(client_id, resolve_coalescing_panda_url(:oauth2_redirect_url), { state: state })
|
87
51
|
|
88
52
|
#delete the added params so the original oauth sig still works
|
89
53
|
@lti_params = params.to_hash
|
@@ -94,7 +58,7 @@ module CoalescingPanda
|
|
94
58
|
|
95
59
|
def refresh_token(uri, api_auth)
|
96
60
|
refresh_client = Bearcat::Client.new(prefix: uri.prefix)
|
97
|
-
refresh_body = refresh_client.retrieve_token(@lti_account.oauth2_client_id,
|
61
|
+
refresh_body = refresh_client.retrieve_token(@lti_account.oauth2_client_id, resolve_coalescing_panda_url(:oauth2_redirect_url),
|
98
62
|
@lti_account.oauth2_client_key, api_auth.refresh_token, 'refresh_token')
|
99
63
|
api_auth.update({ api_token: refresh_body['access_token'], expires_at: (Time.now + refresh_body['expires_in']) })
|
100
64
|
end
|
@@ -193,14 +157,21 @@ module CoalescingPanda
|
|
193
157
|
end
|
194
158
|
end
|
195
159
|
|
196
|
-
def
|
197
|
-
|
160
|
+
def valid_session?
|
161
|
+
return false unless current_session(create_missing: false)&.persisted?
|
162
|
+
true
|
163
|
+
rescue SessionNonceMismatch
|
164
|
+
false
|
198
165
|
end
|
199
166
|
|
200
167
|
private
|
201
168
|
|
202
|
-
def
|
203
|
-
|
169
|
+
def find_or_create_session(key:)
|
170
|
+
if key == :create
|
171
|
+
CoalescingPanda::PersistentSession.create_from_launch(params, current_lti_account.id) if current_lti_account.present?
|
172
|
+
else
|
173
|
+
CoalescingPanda::PersistentSession.find_by(session_key: key)
|
174
|
+
end
|
204
175
|
end
|
205
176
|
|
206
177
|
def organization_key
|
@@ -211,51 +182,16 @@ module CoalescingPanda
|
|
211
182
|
params[:organization_id] || (current_session_data[:launch_params][:organization_id] if @current_session)
|
212
183
|
end
|
213
184
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
185
|
+
# This is necessitated by a bug in Rails Engines where it isn't resolving the URL correctly
|
186
|
+
# when using coalescing_panda.xyz_url (The Engine Prefix is not included)
|
187
|
+
# I believe https://github.com/rails/rails/issues/34452 is the same issue
|
188
|
+
def resolve_coalescing_panda_url(key)
|
189
|
+
key = key.to_s[0...-4] if key.to_s.ends_with?('_url')
|
190
|
+
resolved_path = coalescing_panda.send(:"#{key}_path")
|
191
|
+
cpurl = coalescing_panda_url
|
192
|
+
cppath = URI.parse(cpurl).path
|
193
|
+
resolved_path = cppath + resolved_path unless resolved_path.starts_with?(cppath)
|
194
|
+
URI.join(cpurl, resolved_path)
|
219
195
|
end
|
220
|
-
|
221
|
-
def session_key_header
|
222
|
-
if (match = request.headers['Authorization'].try(:match, /crypted_token=(.+)/))
|
223
|
-
msg_encryptor.decrypt_and_verify(match[1])
|
224
|
-
elsif (match = request.headers['Authorization'].try(:match, /token=(.+)/))
|
225
|
-
match[1]
|
226
|
-
end
|
227
|
-
end
|
228
|
-
|
229
|
-
# Redirect with the session key intact. In production,
|
230
|
-
# handle this by encrypting the session key. That way if the
|
231
|
-
# url is logged anywhere, it will all be encrypted data. In dev,
|
232
|
-
# just put it in the URL. Putting it in the URL
|
233
|
-
# is insecure, but is fine in development.
|
234
|
-
# Keeping it in the URL in development means that it plays
|
235
|
-
# nicely with webpack-dev-server live reloading (otherwise
|
236
|
-
# you get an access error every time it tries to live reload).
|
237
|
-
|
238
|
-
def redirect_with_session_to(path, id_or_resource = nil, redirect_params = {})
|
239
|
-
if Rails.env.development? || Rails.env.test?
|
240
|
-
redirect_development_mode(path, id_or_resource, redirect_params)
|
241
|
-
else
|
242
|
-
redirect_production_mode(path, id_or_resource, redirect_params)
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
|
-
def redirect_development_mode(path, id_or_resource = nil, redirect_params)
|
247
|
-
redirect_to send(path, id_or_resource, {
|
248
|
-
session_key: current_session.session_key,
|
249
|
-
organization_id: current_lti_account.id
|
250
|
-
}.merge(redirect_params))
|
251
|
-
end
|
252
|
-
|
253
|
-
def redirect_production_mode(path, id_or_resource = nil, redirect_params)
|
254
|
-
redirect_to send(path, id_or_resource, {
|
255
|
-
encrypted_session_key: encrypted_session_key,
|
256
|
-
organization_id: current_lti_account.id
|
257
|
-
}.merge(redirect_params))
|
258
|
-
end
|
259
|
-
|
260
196
|
end
|
261
197
|
end
|
@@ -4,9 +4,9 @@ module CoalescingPanda
|
|
4
4
|
|
5
5
|
def self.to_boolean(v)
|
6
6
|
if Rails.version < '5.0'
|
7
|
-
ActiveRecord::Type::Boolean.new.type_cast_from_user(
|
7
|
+
ActiveRecord::Type::Boolean.new.type_cast_from_user(v)
|
8
8
|
else
|
9
|
-
ActiveRecord::Type::Boolean.new.deserialize(
|
9
|
+
ActiveRecord::Type::Boolean.new.deserialize(v)
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -48,11 +48,6 @@ module CoalescingPanda
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
-
if CoalescingPanda.lti_options.has_key?(:allow_unsafe_eval) && CoalescingPanda.lti_options[:allow_unsafe_eval] == true
|
52
|
-
# For when code is returned from server and injected into dom. Need to have unsafe-eval or it won't work.
|
53
|
-
csp_entry(:script_src, "'unsafe-eval'")
|
54
|
-
end
|
55
|
-
|
56
51
|
# Detect and permit Sentry
|
57
52
|
if defined?(Raven) && Raven.configuration.server.present?
|
58
53
|
csp_entry(:connect_src, Raven.configuration.server)
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module CoalescingPanda
|
2
|
+
class SessionNonceMismatch < StandardError; end
|
3
|
+
|
4
|
+
module SessionReplacement
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
helper_method :link_nonce, :current_session, :current_session_data
|
9
|
+
helper_method :link_with_session_to, :url_with_session, :session_url_for
|
10
|
+
|
11
|
+
prepend_around_action :monkeypatch_flash
|
12
|
+
prepend_around_action :auto_save_session
|
13
|
+
end
|
14
|
+
|
15
|
+
class_methods do
|
16
|
+
def link_nonce_type(value = :not_given)
|
17
|
+
if value == :not_given
|
18
|
+
@link_nonce_type || superclass.try(:link_nonce_type) || :nonce
|
19
|
+
else
|
20
|
+
@link_nonce_type = value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def save_session
|
26
|
+
current_session.try(:save)
|
27
|
+
end
|
28
|
+
|
29
|
+
def current_session(create_missing: true)
|
30
|
+
return @current_session if @current_session.present?
|
31
|
+
|
32
|
+
if params[:session_token]
|
33
|
+
payload = JSON.parse(session_cryptor.decrypt_and_verify(params[:session_token])).with_indifferent_access
|
34
|
+
matched_session = find_or_create_session(key: payload[:session_key])
|
35
|
+
|
36
|
+
if matched_session.present?
|
37
|
+
if payload[:token_type] == 'nonce' && matched_session.data[:link_nonce] == payload[:nonce]
|
38
|
+
@current_session = matched_session
|
39
|
+
@current_session.data[:link_nonce] = nil
|
40
|
+
elsif payload[:token_type] == 'fixed_ip' && matched_session.data[:remote_ip] == request.remote_ip &&
|
41
|
+
DateTime.parse(matched_session.data[:last_ip_token_requested]) > 15.minutes.ago
|
42
|
+
@current_session = matched_session
|
43
|
+
end
|
44
|
+
end
|
45
|
+
raise SessionNonceMismatch, "Session Not Found" unless @current_session.present?
|
46
|
+
elsif (session_key = params[:session_key] || session_key_header || flash[:session_key] || session[:session_key]).present?
|
47
|
+
@current_session = find_or_create_session(key: session_key)
|
48
|
+
end
|
49
|
+
|
50
|
+
@current_session ||= find_or_create_session(key: :create) if create_missing
|
51
|
+
|
52
|
+
@current_session
|
53
|
+
end
|
54
|
+
|
55
|
+
def current_session_data
|
56
|
+
current_session.data
|
57
|
+
end
|
58
|
+
|
59
|
+
def session_changed?
|
60
|
+
current_session.changed? && current_session.changes[:data].present?
|
61
|
+
end
|
62
|
+
|
63
|
+
def forbid_access_if_lacking_session
|
64
|
+
render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session?
|
65
|
+
end
|
66
|
+
|
67
|
+
def verify_authenticity_token
|
68
|
+
# No need to check CSRF when no cookies were sent. This fixes CSRF failures in Browsers
|
69
|
+
# that restrict Cookie setting within an IFrame.
|
70
|
+
return unless request.cookies.keys.length > 0
|
71
|
+
super
|
72
|
+
end
|
73
|
+
|
74
|
+
# Redirect with the session key intact. In production,
|
75
|
+
# handle this by adding a one-time use encrypted token to the URL.
|
76
|
+
# Keeping it in the URL in development means that it plays
|
77
|
+
# nicely with webpack-dev-server live reloading (otherwise
|
78
|
+
# you get an access error everytime it tries to live reload).
|
79
|
+
|
80
|
+
def redirect_with_session_to(*args)
|
81
|
+
redirect_to url_with_session(*args)
|
82
|
+
end
|
83
|
+
|
84
|
+
def link_with_session_to(*args)
|
85
|
+
helpers.link_to url_with_session(*args)
|
86
|
+
end
|
87
|
+
|
88
|
+
def session_url_for(*args)
|
89
|
+
url_for(build_session_url_params(*args))
|
90
|
+
end
|
91
|
+
|
92
|
+
def url_with_session(location, *args, route_context: self, **kwargs)
|
93
|
+
route_context.send(location, *build_session_url_params(*args, **kwargs))
|
94
|
+
end
|
95
|
+
|
96
|
+
def link_nonce(type: link_nonce_type)
|
97
|
+
type = instance_exec(&type) if type.is_a?(Proc)
|
98
|
+
type = type.to_s
|
99
|
+
|
100
|
+
@cached_link_nonces ||= {}
|
101
|
+
@cached_link_nonces[type] ||= begin
|
102
|
+
payload = {
|
103
|
+
token_type: type,
|
104
|
+
session_key: current_session.session_key,
|
105
|
+
organization_id: current_organization.id,
|
106
|
+
}
|
107
|
+
|
108
|
+
if type == 'nonce'
|
109
|
+
current_session_data[:link_nonce] = SecureRandom.hex
|
110
|
+
payload.merge!(nonce: current_session_data[:link_nonce])
|
111
|
+
elsif type == 'fixed_ip'
|
112
|
+
current_session_data[:remote_ip] ||= request.remote_ip
|
113
|
+
current_session_data[:last_ip_token_requested] = DateTime.now.iso8601
|
114
|
+
else
|
115
|
+
raise StandardError, "Unsupported link_nonce_type: '#{type}'"
|
116
|
+
end
|
117
|
+
|
118
|
+
session_cryptor.encrypt_and_sign(payload.to_json)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def link_nonce_type
|
123
|
+
self.class.link_nonce_type
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def session_cryptor
|
129
|
+
secret_key_base = Rails.application.try(:secret_key_base) || Rails.application.secrets.secret_key_base
|
130
|
+
@session_cryptor ||= ActiveSupport::MessageEncryptor.new(secret_key_base[0..31])
|
131
|
+
end
|
132
|
+
|
133
|
+
def session_key_header
|
134
|
+
if match = request.headers['Authorization'].try(:match, /token=(.+)/)
|
135
|
+
match[1]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_session_url_params(*args, nonce_type: link_nonce_type, **kwargs)
|
140
|
+
if args[-1].is_a?(Hash)
|
141
|
+
args[-1] = args[-1].dup
|
142
|
+
else
|
143
|
+
args.push({})
|
144
|
+
end
|
145
|
+
|
146
|
+
if Rails.env.development?
|
147
|
+
args[-1].merge!(
|
148
|
+
session_key: current_session.session_key,
|
149
|
+
organization_id: current_organization.id,
|
150
|
+
)
|
151
|
+
else
|
152
|
+
args[-1].merge!(
|
153
|
+
session_token: link_nonce(type: nonce_type),
|
154
|
+
organization_id: current_organization.id,
|
155
|
+
)
|
156
|
+
end
|
157
|
+
|
158
|
+
args[-1].merge!(kwargs)
|
159
|
+
args
|
160
|
+
end
|
161
|
+
|
162
|
+
def auto_save_session
|
163
|
+
yield if block_given?
|
164
|
+
save_session if @current_session && session_changed?
|
165
|
+
end
|
166
|
+
|
167
|
+
def monkeypatch_flash
|
168
|
+
if valid_session? && (value = current_session_data['flashes']).present?
|
169
|
+
flashes = value["flashes"]
|
170
|
+
if discard = value["discard"]
|
171
|
+
flashes.except!(*discard)
|
172
|
+
end
|
173
|
+
flash.replace(flashes)
|
174
|
+
flash.discard()
|
175
|
+
end
|
176
|
+
|
177
|
+
yield
|
178
|
+
|
179
|
+
if @current_session.present?
|
180
|
+
current_session_data['flashes'] = flash.to_session_value
|
181
|
+
flash.discard()
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -11,7 +11,7 @@ describe CoalescingPanda::Oauth2Controller, :type => :controller do
|
|
11
11
|
Bearcat::Client.any_instance.stub(retrieve_token: { 'access_token' => 'token', 'refresh_token' => 'token', 'expires_in' => 3600 })
|
12
12
|
session[:state] = 'test'
|
13
13
|
CoalescingPanda::OauthState.create!(state_key: session[:state], data: { key: account.key, user_id: user.id, api_domain: 'foo.com' })
|
14
|
-
get :redirect, {user_id: user.id, api_domain: 'foo.com', code: 'bar', key: account.key, state: 'test'}
|
14
|
+
get :redirect, params: {user_id: user.id, api_domain: 'foo.com', code: 'bar', key: account.key, state: 'test'}
|
15
15
|
auth = CoalescingPanda::CanvasApiAuth.find_by_user_id_and_api_domain(user.id, 'foo.com')
|
16
16
|
auth.should_not == nil
|
17
17
|
expect(auth.api_token).to eql 'token'
|
@@ -20,7 +20,7 @@ describe CoalescingPanda::Oauth2Controller, :type => :controller do
|
|
20
20
|
end
|
21
21
|
|
22
22
|
it "doesn't create a token in the db" do
|
23
|
-
get :redirect, {error: 'your face'}
|
23
|
+
get :redirect, params: {error: 'your face'}
|
24
24
|
CoalescingPanda::CanvasApiAuth.all.count.should == 0
|
25
25
|
end
|
26
26
|
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe CoalescingPanda::CanvasApiAuth do
|
3
|
+
describe CoalescingPanda::CanvasApiAuth, type: :model do
|
4
4
|
|
5
5
|
it { should validate_uniqueness_of(:user_id).scoped_to(:api_domain)}
|
6
6
|
it { should validate_presence_of(:user_id)}
|
7
|
-
it {should validate_presence_of(:api_domain)}
|
7
|
+
it { should validate_presence_of(:api_domain)}
|
8
8
|
|
9
9
|
describe '#expired?' do
|
10
10
|
let(:auth) { FactoryGirl.create :canvas_api_auth }
|
data/spec/spec_helper.rb
CHANGED
@@ -24,6 +24,13 @@ SimpleCov.start
|
|
24
24
|
|
25
25
|
ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)
|
26
26
|
|
27
|
+
Shoulda::Matchers.configure do |config|
|
28
|
+
config.integrate do |with|
|
29
|
+
with.test_framework :rspec
|
30
|
+
with.library :rails
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
27
34
|
# This file was generated by the `rails generate rspec:install` command. Conventionally, all
|
28
35
|
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
29
36
|
# The generated `.rspec` file contains `--require spec_helper` which will cause this
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: coalescing_panda
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Mills
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2020-
|
13
|
+
date: 2020-09-16 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rails
|
@@ -490,6 +490,7 @@ files:
|
|
490
490
|
- lib/coalescing_panda/misc_helper.rb
|
491
491
|
- lib/coalescing_panda/route_helpers.rb
|
492
492
|
- lib/coalescing_panda/secure_headers.rb
|
493
|
+
- lib/coalescing_panda/session_replacement.rb
|
493
494
|
- lib/coalescing_panda/version.rb
|
494
495
|
- lib/tasks/coalescing_panda_tasks.rake
|
495
496
|
- spec/controllers/coalescing_panda/canvas_batches_controller_spec.rb
|
@@ -577,7 +578,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
577
578
|
- !ruby/object:Gem::Version
|
578
579
|
version: '0'
|
579
580
|
requirements: []
|
580
|
-
|
581
|
+
rubyforge_project:
|
582
|
+
rubygems_version: 2.6.14.4
|
581
583
|
signing_key:
|
582
584
|
specification_version: 4
|
583
585
|
summary: Canvas LTI and OAUTH2 mountable engine
|