coalescing_panda 5.0.6 → 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/app/controllers/coalescing_panda/canvas_batches_controller.rb +2 -2
- data/app/controllers/coalescing_panda/oauth2_controller.rb +1 -1
- data/app/helpers/coalescing_panda/canvas_batches_helper.rb +1 -1
- data/app/views/coalescing_panda/canvas_batches/_canvas_batch_flash.html.haml +3 -3
- data/lib/coalescing_panda/controller_helpers.rb +27 -90
- 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: ca4769293181b5ee47471cc9051ad0b678915f71
|
4
|
+
data.tar.gz: 60c8585ff7f5d28057b703a9ae7c5a85ab6dbdb7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6103b51481915a9d2719a87ff81c042a783d0111108e4c270fc3261a61008ab3683beaae3b2a9490450248fe882f4be75776d360219fe2dadb7377cc301f229f
|
7
|
+
data.tar.gz: f434c3a048769d55b72fe0ba190c7033ce0429b0acbf3ec0ec1a2ea2bc920262de49ef9336feb5e236d1767237bd79dd4ccf6bf14765c79358faa24fa329e91c
|
@@ -12,13 +12,13 @@ module CoalescingPanda
|
|
12
12
|
@batch.status = 'Queued'
|
13
13
|
@batch.save
|
14
14
|
worker = CoalescingPanda::Workers::CourseMiner.new(@batch.context, @batch.options)
|
15
|
-
|
15
|
+
current_session_data[:canvas_batch_id] = worker.batch.id
|
16
16
|
worker.start(true)
|
17
17
|
redirect_to :back
|
18
18
|
end
|
19
19
|
|
20
20
|
def clear_batch_session
|
21
|
-
|
21
|
+
current_session_data[:canvas_batch_id] = nil
|
22
22
|
render nothing: true
|
23
23
|
end
|
24
24
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module CoalescingPanda
|
2
2
|
module CanvasBatchesHelper
|
3
3
|
def current_batch
|
4
|
-
@current_batch ||= CoalescingPanda::CanvasBatch.find_by_id(
|
4
|
+
@current_batch ||= CoalescingPanda::CanvasBatch.find_by_id(current_session_data[:canvas_batch_id])
|
5
5
|
end
|
6
6
|
end
|
7
7
|
end
|
@@ -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,22 @@ module CoalescingPanda
|
|
193
157
|
end
|
194
158
|
end
|
195
159
|
|
196
|
-
def
|
197
|
-
|
160
|
+
def valid_session?
|
161
|
+
[
|
162
|
+
current_session&.persisted?,
|
163
|
+
].all?
|
164
|
+
rescue SessionNonceMismatch
|
165
|
+
false
|
198
166
|
end
|
199
167
|
|
200
168
|
private
|
201
169
|
|
202
|
-
def
|
203
|
-
|
170
|
+
def find_or_create_session(key:)
|
171
|
+
if key == :create
|
172
|
+
CoalescingPanda::PersistentSession.create_from_launch(params, current_lti_account.id) if current_lti_account.present?
|
173
|
+
else
|
174
|
+
CoalescingPanda::PersistentSession.find_by(session_key: key)
|
175
|
+
end
|
204
176
|
end
|
205
177
|
|
206
178
|
def organization_key
|
@@ -211,51 +183,16 @@ module CoalescingPanda
|
|
211
183
|
params[:organization_id] || (current_session_data[:launch_params][:organization_id] if @current_session)
|
212
184
|
end
|
213
185
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
186
|
+
# This is necessitated by a bug in Rails Engines where it isn't resolving the URL correctly
|
187
|
+
# when using coalescing_panda.xyz_url (The Engine Prefix is not included)
|
188
|
+
# I believe https://github.com/rails/rails/issues/34452 is the same issue
|
189
|
+
def resolve_coalescing_panda_url(key)
|
190
|
+
key = key.to_s[0...-4] if key.to_s.ends_with?('_url')
|
191
|
+
resolved_path = coalescing_panda.send(:"#{key}_path")
|
192
|
+
cpurl = coalescing_panda_url
|
193
|
+
cppath = URI.parse(cpurl).path
|
194
|
+
resolved_path = cppath + resolved_path unless resolved_path.starts_with?(cppath)
|
195
|
+
URI.join(cpurl, resolved_path)
|
219
196
|
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
197
|
end
|
261
198
|
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
|
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)
|
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.0
|
4
|
+
version: 5.1.0
|
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-14 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
|