panda_pal 5.17.0 → 5.17.1
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 +4 -4
- data/app/models/panda_pal/session.rb +11 -1
- data/config/initializers/apartment.rb +42 -23
- data/lib/panda_pal/helpers/secure_headers.rb +4 -0
- data/lib/panda_pal/helpers/session_replacement.rb +9 -7
- data/lib/panda_pal/version.rb +1 -1
- data/spec/lib/panda_pal/helpers/session_replacement_spec.rb +72 -0
- data/spec/models/panda_pal/session_spec.rb +47 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f32b5b78d11428c9f4be64fc963ae2fac80cae44abfd66dcda0f39be8b17006
|
|
4
|
+
data.tar.gz: cb494809a3aa72956b16bad0b7b17ba095156a1c4a7fa7261a51738d92442630
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7819962aa331b91ae3b6ad0d95fdb8e5bf314d691cdaa4f120a1f12b199d26c916a811b9f071b9fd73802c8893ae70b6a6158a7ea9457b602f1ea2aa496d4092
|
|
7
|
+
data.tar.gz: 5cfd2d2f902e658968b3ce2ce2495c5a58e605071db2c97436186b2893f178d2c198856ac42acd9371f6462d108a2a959834958b84ad1fa9d4f9c0740686cc93
|
|
@@ -9,6 +9,16 @@ module PandaPal
|
|
|
9
9
|
self.data ||= {}.with_indifferent_access
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
# ActiveModel 8.0 skips its own "mark every duped attribute as changed" step
|
|
13
|
+
# when the source record isn't persisted, and validate_v1p3_launch deletes the
|
|
14
|
+
# login session before duping it. Under partial_inserts the resulting INSERT
|
|
15
|
+
# then omits session_secret, which lands NULL and breaks every later
|
|
16
|
+
# panda_token signature check. Force a full row on every dup.
|
|
17
|
+
def initialize_dup(other)
|
|
18
|
+
super
|
|
19
|
+
attribute_names.each { |name| attribute_will_change!(name) }
|
|
20
|
+
end
|
|
21
|
+
|
|
12
22
|
delegate :dig, to: :data
|
|
13
23
|
|
|
14
24
|
def self.for_request(request, params = request.params, enforce_tenant: true)
|
|
@@ -383,7 +393,7 @@ module PandaPal
|
|
|
383
393
|
ROLE_TYPES.each do |rt|
|
|
384
394
|
# LIS V1
|
|
385
395
|
if role.downcase.start_with?(rt[:urn])
|
|
386
|
-
return clean_role(
|
|
396
|
+
return clean_role(rt[:urn], role), rt
|
|
387
397
|
end
|
|
388
398
|
|
|
389
399
|
# LIS V2
|
|
@@ -50,6 +50,34 @@ module Apartment
|
|
|
50
50
|
bits[0] = "default" if bits[0].nil? || bits[0].empty?
|
|
51
51
|
bits
|
|
52
52
|
end
|
|
53
|
+
|
|
54
|
+
# Lazily switch a long-lived worker thread to `tenant_name` for the duration of the block,
|
|
55
|
+
# without touching the DB up front. Actively switching checks out a connection and runs
|
|
56
|
+
# SET search_path even for work that never queries anything, so this only *stages* the
|
|
57
|
+
# tenant; the ActiveRecord `checkout` hook registered below applies it the moment a
|
|
58
|
+
# connection is actually acquired, and only then.
|
|
59
|
+
#
|
|
60
|
+
# For anything that runs app code on a worker thread outside a normal request/job cycle --
|
|
61
|
+
# ActionCable's `:work` callback below, CableRoom's `Room#around_work` below, or a similar
|
|
62
|
+
# hook of your own -- this is the one place that logic lives; call it instead of
|
|
63
|
+
# reimplementing the stage/release/restore dance yourself.
|
|
64
|
+
def self.with_lazy_tenant(tenant_name)
|
|
65
|
+
Thread.current[:cable_tenant] = {
|
|
66
|
+
adapter: Apartment::Tenant.adapter,
|
|
67
|
+
tenant: tenant_name,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# If this thread already holds a connection from earlier work, release it back to the pool
|
|
71
|
+
# so the checkout hook below gets a fresh checkout to apply the schema to.
|
|
72
|
+
pool = Apartment.connection_class.connection_pool
|
|
73
|
+
pool.release_connection if pool.active_connection?
|
|
74
|
+
|
|
75
|
+
Apartment::Tenant.adapter.instance_variable_set(:@current, tenant_name)
|
|
76
|
+
|
|
77
|
+
yield
|
|
78
|
+
ensure
|
|
79
|
+
Thread.current[:cable_tenant] = nil
|
|
80
|
+
end
|
|
53
81
|
end
|
|
54
82
|
|
|
55
83
|
module Adapters
|
|
@@ -319,30 +347,10 @@ ActiveSupport.on_load(:action_cable) do
|
|
|
319
347
|
end
|
|
320
348
|
end
|
|
321
349
|
|
|
322
|
-
# Lazily switch any worker threads to the correct tenant when they are working
|
|
323
|
-
#
|
|
324
|
-
# The message processing may not interface with the DB, so this would be a huge waste.
|
|
325
|
-
# Instead, we ensure that the thread will trigger a :checkout if it needs a connection,
|
|
326
|
-
# at which time we hack-in the correct tenant/schema.
|
|
327
|
-
|
|
350
|
+
# Lazily switch any worker threads to the correct tenant when they are working (see
|
|
351
|
+
# Apartment::Tenant.with_lazy_tenant above for why this is lazy, not an active switch).
|
|
328
352
|
ActionCable::Server::Worker.set_callback :work, :around do |_, blk|
|
|
329
|
-
|
|
330
|
-
# which could then cause `checkout` to recurse)
|
|
331
|
-
Thread.current[:cable_tenant] = {
|
|
332
|
-
adapter: Apartment::Tenant.adapter,
|
|
333
|
-
tenant: connection.tenant,
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
# If the current thread already has a connection checked out, release it back to the pool so that the later after_checkout
|
|
337
|
-
# callback can set the schema properly.
|
|
338
|
-
pool = Apartment.connection_class.connection_pool
|
|
339
|
-
pool.release_connection if pool.active_connection?
|
|
340
|
-
|
|
341
|
-
Apartment::Tenant.adapter.instance_variable_set(:@current, connection.tenant)
|
|
342
|
-
|
|
343
|
-
blk.call
|
|
344
|
-
ensure
|
|
345
|
-
Thread.current[:cable_tenant] = nil
|
|
353
|
+
Apartment::Tenant.with_lazy_tenant(connection.tenant, &blk)
|
|
346
354
|
end
|
|
347
355
|
|
|
348
356
|
ActiveSupport.on_load(:active_record) do
|
|
@@ -358,6 +366,17 @@ ActiveSupport.on_load(:action_cable) do
|
|
|
358
366
|
end
|
|
359
367
|
end
|
|
360
368
|
|
|
369
|
+
# cable_room runs a Room's own code -- message handlers, startup, shutdown, snapshotting -- on a
|
|
370
|
+
# Host worker thread, never inside ActionCable's `:work` callback (its worker pool deliberately
|
|
371
|
+
# isn't an ActionCable::Server::Worker subclass, so it doesn't share that callback chain).
|
|
372
|
+
# `Room#around_work` is its equivalent seam; hook it the same lazy way, on every Room, via
|
|
373
|
+
# CableRoom::Room::Base so every app Room picks it up.
|
|
374
|
+
if defined?(CableRoom)
|
|
375
|
+
CableRoom::Room::Base.around_work do |room, blk|
|
|
376
|
+
Apartment::Tenant.with_lazy_tenant(room.tenant, &blk)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
361
380
|
if defined?(Delayed)
|
|
362
381
|
module PandaPal::Plugins
|
|
363
382
|
class ApartmentDelayedJobsPlugin < ::Delayed::Plugin
|
|
@@ -27,6 +27,10 @@ module PandaPal
|
|
|
27
27
|
csp_entry(:connect_src, "http://localhost:3035")
|
|
28
28
|
csp_entry(:connect_src, "ws://localhost:3035")
|
|
29
29
|
|
|
30
|
+
# Vite dev server
|
|
31
|
+
csp_entry(:connect_src, "http://localhost:3036")
|
|
32
|
+
csp_entry(:connect_src, "ws://localhost:3036")
|
|
33
|
+
|
|
30
34
|
# Allow stuff like rack-mini-profiler to work in development:
|
|
31
35
|
# https://github.com/MiniProfiler/rack-mini-profiler/issues/327
|
|
32
36
|
csp_entry(:script_src, "'unsafe-eval'")
|
|
@@ -129,21 +129,23 @@ module PandaPal::Helpers
|
|
|
129
129
|
type = type.to_s.downcase
|
|
130
130
|
type = type[2..] if type.start_with?('t-')
|
|
131
131
|
|
|
132
|
+
base_payload = {
|
|
133
|
+
v: 1,
|
|
134
|
+
o: current_organization.id,
|
|
135
|
+
s: current_session.id,
|
|
136
|
+
}
|
|
137
|
+
|
|
132
138
|
if type == 'url'
|
|
133
139
|
raise StandardError, "URL is required" unless url.present?
|
|
134
140
|
to_sign = PandaPal::Session.format_url_for_signing(url)
|
|
135
|
-
payload.merge
|
|
141
|
+
payload = base_payload.merge(
|
|
136
142
|
typ: 'T-URI',
|
|
137
143
|
usig: current_session.sign_value(to_sign),
|
|
138
144
|
)
|
|
139
145
|
else
|
|
140
146
|
@cached_link_nonces ||= {}
|
|
141
|
-
@cached_link_nonces[type] ||= begin
|
|
142
|
-
payload =
|
|
143
|
-
v: 1,
|
|
144
|
-
o: current_organization.id,
|
|
145
|
-
s: current_session.id,
|
|
146
|
-
}
|
|
147
|
+
payload = @cached_link_nonces[type] ||= begin
|
|
148
|
+
payload = base_payload.dup
|
|
147
149
|
|
|
148
150
|
if type == 'nonce'
|
|
149
151
|
current_session[:link_nonce] = SecureRandom.hex
|
data/lib/panda_pal/version.rb
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe PandaPal::Helpers::SessionReplacement do
|
|
4
|
+
let(:organization) { create(:panda_pal_organization) }
|
|
5
|
+
let(:session) { PandaPal::Session.create!(panda_pal_organization: organization, data: {}) }
|
|
6
|
+
|
|
7
|
+
let(:host_class) do
|
|
8
|
+
Class.new do
|
|
9
|
+
# Stub the concern's controller-only class macros.
|
|
10
|
+
def self.helper_method(*args); end
|
|
11
|
+
def self.prepend_around_action(*args); end
|
|
12
|
+
|
|
13
|
+
include PandaPal::Helpers::SessionReplacement
|
|
14
|
+
|
|
15
|
+
attr_reader :current_organization, :current_session
|
|
16
|
+
|
|
17
|
+
def initialize(organization, session)
|
|
18
|
+
@current_organization = organization
|
|
19
|
+
@current_session = session
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def request
|
|
23
|
+
@request ||= Struct.new(:remote_ip).new('127.0.0.1')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
subject(:controller) { host_class.new(organization, session) }
|
|
29
|
+
|
|
30
|
+
def decoded_payload(token)
|
|
31
|
+
JSON.parse(Base64.urlsafe_decode64(token.split('.').first))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#link_nonce' do
|
|
35
|
+
it 'returns the same real payload on repeated calls within one request' do
|
|
36
|
+
first = controller.link_nonce(type: :nonce)
|
|
37
|
+
second = controller.link_nonce(type: :nonce)
|
|
38
|
+
third = controller.link_nonce(type: :nonce)
|
|
39
|
+
|
|
40
|
+
[first, second, third].each do |token|
|
|
41
|
+
expect(token.split('.').first).not_to eq(Base64.urlsafe_encode64('null'))
|
|
42
|
+
payload = decoded_payload(token)
|
|
43
|
+
expect(payload['typ']).to eq('T-NONCE')
|
|
44
|
+
expect(payload['o']).to eq(organization.id)
|
|
45
|
+
expect(payload['s']).to eq(session.id)
|
|
46
|
+
expect(payload['nnc']).to be_present
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
expect(second).to eq(first)
|
|
50
|
+
expect(third).to eq(first)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'builds a T-URI payload with the org and session the verifier requires' do
|
|
54
|
+
token = controller.link_nonce('https://example.test/a/b', type: :url)
|
|
55
|
+
|
|
56
|
+
payload = decoded_payload(token)
|
|
57
|
+
expect(payload['typ']).to eq('T-URI')
|
|
58
|
+
expect(payload['o']).to eq(organization.id)
|
|
59
|
+
expect(payload['s']).to eq(session.id)
|
|
60
|
+
expect(payload['usig']).to be_present
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'still caches one nonce per type per request rather than rotating it' do
|
|
64
|
+
controller.link_nonce(type: :nonce)
|
|
65
|
+
nonce_after_first = session[:link_nonce]
|
|
66
|
+
|
|
67
|
+
controller.link_nonce(type: :nonce)
|
|
68
|
+
|
|
69
|
+
expect(session[:link_nonce]).to eq(nonce_after_first)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -6,6 +6,53 @@ module PandaPal
|
|
|
6
6
|
expect(PandaPal::Session.new.session_secret).to_not be_nil
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
+
describe '#dup' do
|
|
10
|
+
# Mirrors validate_v1p3_launch: load the login session, delete it, dup it into
|
|
11
|
+
# the org's tenant. Only fails on Rails 8.0 with partial_inserts left on, so it
|
|
12
|
+
# is the appraisal run that gates this, not the default (Rails 8.1) lockfile.
|
|
13
|
+
it 'persists every attribute, including session_secret, on the duped record' do
|
|
14
|
+
original = PandaPal::Session.find(create(:panda_pal_session).id)
|
|
15
|
+
original_secret = original.session_secret
|
|
16
|
+
original.delete
|
|
17
|
+
|
|
18
|
+
with_partial_inserts(true) do
|
|
19
|
+
duped = original.dup
|
|
20
|
+
duped.save!
|
|
21
|
+
|
|
22
|
+
reloaded = PandaPal::Session.find(duped.id)
|
|
23
|
+
expect(reloaded.session_secret).to eq(original_secret)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Rails 7.0 split partial_writes into partial_inserts/partial_updates; on 6.1
|
|
28
|
+
# the single partial_writes flag is what controls the INSERT.
|
|
29
|
+
def with_partial_inserts(value)
|
|
30
|
+
setting = PandaPal::Session.respond_to?(:partial_inserts) ? 'partial_inserts' : 'partial_writes'
|
|
31
|
+
previous = PandaPal::Session.public_send(setting)
|
|
32
|
+
PandaPal::Session.public_send("#{setting}=", value)
|
|
33
|
+
yield
|
|
34
|
+
ensure
|
|
35
|
+
PandaPal::Session.public_send("#{setting}=", previous) unless previous.nil?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe Session::RoleStore do
|
|
40
|
+
it 'parses an LTI 1.1 (URN-style) institution role' do
|
|
41
|
+
role_store = Session::RoleStore.new('urn:lti:instrole:ims/lis/Administrator')
|
|
42
|
+
expect(role_store.institution_roles).to eq(['administrator'])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'parses an LTI 1.1 (URN-style) system role' do
|
|
46
|
+
role_store = Session::RoleStore.new('urn:lti:sysrole:ims/lis/SysAdmin')
|
|
47
|
+
expect(role_store.system_roles).to eq(['sys_admin'])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'parses an LTI 1.3 (URL-style) institution role' do
|
|
51
|
+
role_store = Session::RoleStore.new(['http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator'])
|
|
52
|
+
expect(role_store.institution_roles).to eq(['administrator'])
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
9
56
|
describe '.for_panda_token' do
|
|
10
57
|
it 'returns nil for a nil token' do
|
|
11
58
|
expect(Session.for_panda_token(nil)).to be_nil
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: panda_pal
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.17.
|
|
4
|
+
version: 5.17.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Instructure CustomDev
|
|
@@ -210,6 +210,7 @@ files:
|
|
|
210
210
|
- spec/internal/db/schema.rb
|
|
211
211
|
- spec/jobs/panda_pal/jobs/grade_passback_job_spec.rb
|
|
212
212
|
- spec/lib/panda_pal/helpers/secure_headers_spec.rb
|
|
213
|
+
- spec/lib/panda_pal/helpers/session_replacement_spec.rb
|
|
213
214
|
- spec/models/panda_pal/api_call_spec.rb
|
|
214
215
|
- spec/models/panda_pal/organization/settings_validation_spec.rb
|
|
215
216
|
- spec/models/panda_pal/organization/task_scheduling_spec.rb
|
|
@@ -254,6 +255,7 @@ test_files:
|
|
|
254
255
|
- spec/internal/db/schema.rb
|
|
255
256
|
- spec/jobs/panda_pal/jobs/grade_passback_job_spec.rb
|
|
256
257
|
- spec/lib/panda_pal/helpers/secure_headers_spec.rb
|
|
258
|
+
- spec/lib/panda_pal/helpers/session_replacement_spec.rb
|
|
257
259
|
- spec/models/panda_pal/api_call_spec.rb
|
|
258
260
|
- spec/models/panda_pal/organization/settings_validation_spec.rb
|
|
259
261
|
- spec/models/panda_pal/organization/task_scheduling_spec.rb
|