panda_pal 5.14.0.beta7 → 5.15.0
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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dcf0b49dce1c842893ddd32353c984df374c0355fcc3508f15fe430bd04d93df
|
4
|
+
data.tar.gz: af35bc87c1179a41aaf836282863806e58d7af534c6a6cc8523e7fe8e47b088a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: caaf1b79881bb2e3fc0670bba8c9148e808243ba569a143a5c940bbace0e5458732e4236a2b1c0c92b61f7820040118243a32547d35c74fcf2f6155e308a668a
|
7
|
+
data.tar.gz: d4c2901a2ec51d3c40b06cc3d727a3af346e93f8a7239f7d8b1d0e906fd5d7ac1c3e41b3b256d45091aad19898eeea0efe89a9491336125169ed5dd8e591119a
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module PandaPal
|
2
2
|
class Session < PandaPalRecord
|
3
|
+
class SessionNonceMismatch < StandardError; end
|
4
|
+
|
3
5
|
belongs_to :panda_pal_organization, class_name: 'PandaPal::Organization', optional: true
|
4
6
|
|
5
7
|
after_initialize do
|
@@ -9,6 +11,129 @@ module PandaPal
|
|
9
11
|
|
10
12
|
delegate :dig, to: :data
|
11
13
|
|
14
|
+
def self.for_request(request, params = request.params, enforce_tenant: true)
|
15
|
+
return request.instance_variable_get(:@panda_pal_session) if request.instance_variable_defined?(:@panda_pal_session)
|
16
|
+
|
17
|
+
panda_token = extract_panda_token(request, params)
|
18
|
+
request.instance_variable_set(:@panda_pal_session, for_panda_token(panda_token, request: request, enforce_tenant: enforce_tenant))
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.for_panda_token(panda_token, request: nil, validate: true, enforce_tenant: true)
|
22
|
+
return nil unless panda_token.present?
|
23
|
+
|
24
|
+
# <B64({ v: TOKEN FORMAT, o: ORG ID, s: SESSION ID, typ: TYPE, exp?: EXPIRE, usig?: URL SIGNATURE, nnc?: NONCE })>.<HMAC(<B64HEADER>) | KEY>
|
25
|
+
# TYPE = KEY | T-URI | T-IP | T-NONCE | T-EXP
|
26
|
+
|
27
|
+
found_session = nil
|
28
|
+
|
29
|
+
header, sig = panda_token.split('.')
|
30
|
+
decoded_header = JSON.parse(Base64.urlsafe_decode64(header))
|
31
|
+
|
32
|
+
org_id = decoded_header['o'].to_i
|
33
|
+
session_id = decoded_header['s']
|
34
|
+
type = decoded_header['typ']
|
35
|
+
|
36
|
+
tenant = "public"
|
37
|
+
|
38
|
+
if org_id > 0
|
39
|
+
org = PandaPal::Organization.find(org_id)
|
40
|
+
tenant = org.tenant_name
|
41
|
+
end
|
42
|
+
|
43
|
+
Apartment::Tenant.switch!(tenant) if enforce_tenant == :switch
|
44
|
+
|
45
|
+
if enforce_tenant && Apartment::Tenant.current != tenant
|
46
|
+
raise SessionNonceMismatch, "Session Not Found"
|
47
|
+
end
|
48
|
+
|
49
|
+
Apartment::Tenant.switch(tenant) do
|
50
|
+
if type == 'KEY'
|
51
|
+
found_session = PandaPal::Session.find_by(session_secret: sig)
|
52
|
+
return found_session unless validate
|
53
|
+
|
54
|
+
raise SessionNonceMismatch, "Session Not Found" unless found_session.present?
|
55
|
+
raise SessionNonceMismatch, "Session Not Found" if session_id && found_session.id != session_id
|
56
|
+
else
|
57
|
+
session_record = PandaPal::Session.find_by(id: session_id)
|
58
|
+
return session_record unless validate
|
59
|
+
|
60
|
+
raise SessionNonceMismatch, "Session Not Found" unless session_record.present?
|
61
|
+
|
62
|
+
# Validate the header against the signature
|
63
|
+
raise SessionNonceMismatch, "Invalid Signature" unless session_record.validate_signature(header, sig)
|
64
|
+
|
65
|
+
# Check expiration
|
66
|
+
if (expr = decoded_header['exp']).present?
|
67
|
+
raise SessionNonceMismatch, "Expired" unless expr > DateTime.now.iso8601
|
68
|
+
end
|
69
|
+
|
70
|
+
if type == "T-URI" || type == "T-URL"
|
71
|
+
# Signed URLs only support GET requests
|
72
|
+
raise SessionNonceMismatch, "Invalid Method" unless request.method.to_s.upcase == "GET"
|
73
|
+
|
74
|
+
# Verify the signature against the request URL
|
75
|
+
# The usig doesn't _need_ to be signed (since it's part of the payload that is already signed),
|
76
|
+
# but this was an easy way to make the value a constant length. Any basic hashing function could solve this,
|
77
|
+
# but the HMAC implementation was already available.
|
78
|
+
to_sign = format_url_for_signing(request.url)
|
79
|
+
raise SessionNonceMismatch, "Invalid Signature" unless session_record.validate_signature(to_sign, decoded_header['usig'])
|
80
|
+
|
81
|
+
# TODO Support single-use tokens via Redis?
|
82
|
+
|
83
|
+
found_session = session_record
|
84
|
+
elsif type == "T-NONCE"
|
85
|
+
raise SessionNonceMismatch, "Invalid Nonce" unless session_record.data[:link_nonce] == decoded_header['nnc']
|
86
|
+
found_session = session_record
|
87
|
+
found_session.data[:link_nonce] = nil
|
88
|
+
elsif type == "T-IP"
|
89
|
+
raise SessionNonceMismatch, "Invalid IP" unless decoded_header["ip"] == request.remote_ip
|
90
|
+
found_session = session_record
|
91
|
+
elsif type == "T-EXP"
|
92
|
+
# Expiration itself is checked above, but enforce that an expiration is set
|
93
|
+
raise SessionNonceMismatch, "Invalid Expiration" unless decoded_header["exp"].present?
|
94
|
+
found_session = session_record
|
95
|
+
else
|
96
|
+
raise SessionNonceMismatch, "Invalid Panda Token Type"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
raise SessionNonceMismatch, "Session Not Found" unless found_session.present?
|
102
|
+
|
103
|
+
found_session
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.extract_panda_token(request, params = request.params)
|
107
|
+
headers = request.respond_to?(:headers) ? request.headers : request.env
|
108
|
+
token = headers['HTTP_X_PANDA_TOKEN'] || headers['X-Panda-Token']
|
109
|
+
|
110
|
+
token ||= begin
|
111
|
+
if (auth_header = headers['HTTP_AUTHORIZATION'] || headers['Authorization']).present?
|
112
|
+
match = auth_header.match(/Bearer panda:(.+)/)
|
113
|
+
match ||= auth_header.match(/token=(.+)/) # Legacy Support
|
114
|
+
token = match[1] if match && match[1].include?('.')
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
token ||= params["panda_token"]
|
119
|
+
token ||= params["session_token"] if params["session_token"]&.include?('.') # Legacy Support
|
120
|
+
token ||= params["session_key"] if params["session_key"]&.include?('.') # Legacy Support
|
121
|
+
|
122
|
+
token.presence
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.format_url_for_signing(uri)
|
126
|
+
uri = URI.parse(uri) if uri.is_a?(String)
|
127
|
+
|
128
|
+
query = Rack::Utils.parse_query(uri.query)
|
129
|
+
query.delete("panda_token")
|
130
|
+
query.delete("session_key")
|
131
|
+
query.delete("session_token")
|
132
|
+
uri.query = Rack::Utils.build_query(query)
|
133
|
+
|
134
|
+
"#{uri.path}?#{uri.query}"
|
135
|
+
end
|
136
|
+
|
12
137
|
def session_key
|
13
138
|
# <B64({ v: TOKEN FORMAT, o: ORG ID, s: SESSION ID, typ: "KEY" })>.<SESSION SECRET>
|
14
139
|
payload = {
|
@@ -26,10 +151,11 @@ module PandaPal
|
|
26
151
|
end
|
27
152
|
|
28
153
|
def sign_value(data)
|
29
|
-
OpenSSL::HMAC.
|
154
|
+
OpenSSL::HMAC.base64digest(OpenSSL::Digest.new('sha256'), signing_key, data)
|
30
155
|
end
|
31
156
|
|
32
157
|
def validate_signature(data, signature)
|
158
|
+
# TODO Constant time
|
33
159
|
signature == sign_value(data)
|
34
160
|
end
|
35
161
|
|
@@ -106,11 +232,13 @@ module PandaPal
|
|
106
232
|
val
|
107
233
|
end
|
108
234
|
|
235
|
+
# Retrieve the User's Role Labels in the Launch Context
|
109
236
|
def canvas_role_labels
|
110
237
|
labels = get_lti_cust_param('custom_canvas_role')
|
111
238
|
labels.is_a?(String) ? labels.split(',') : []
|
112
239
|
end
|
113
240
|
|
241
|
+
# Retrieve the User's Role Labels in the specified Account, defaulting to the Root Account
|
114
242
|
def canvas_account_role_labels(account = 'self')
|
115
243
|
account = 'self' if account.to_s == "root"
|
116
244
|
account = account.canvas_id if account.respond_to?(:canvas_id)
|
@@ -181,7 +181,7 @@ module Apartment
|
|
181
181
|
# to effectively disable this patch for that model.
|
182
182
|
if (adapter = Thread.current[:apartment_adapter]) && adapter.is_a?(Apartment::Adapters::PostgresMultiDBSchemaAdapter) && !adapter.is_excluded_model?(self)
|
183
183
|
shard, schema = Apartment::Tenant.split_tenant(adapter.current)
|
184
|
-
return "
|
184
|
+
return "#{shard}" unless shard == "default"
|
185
185
|
end
|
186
186
|
|
187
187
|
pre_apartment_current_shard
|
@@ -220,18 +220,19 @@ Apartment.configure do |config|
|
|
220
220
|
end
|
221
221
|
|
222
222
|
Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request|
|
223
|
-
if match = request.path.match(/\/(?:orgs?|organizations?)\/(\d+)/)
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
223
|
+
tenant = if match = request.path.match(/\/(?:orgs?|organizations?)\/(\d+)/)
|
224
|
+
PandaPal::Organization.find_by(id: match[1]).try(:tenant_name)
|
225
|
+
elsif match = request.path.match(/\/(?:orgs?|organizations?|o)\/(\w+)/)
|
226
|
+
PandaPal::Organization.find_by(name: match[1]).try(:tenant_name)
|
227
|
+
elsif (panda_token = PandaPal::Session.extract_panda_token(request)).present?
|
228
|
+
header, sig = panda_token.split('.')
|
229
|
+
# This is just switching the tenant. Auth should be performed elsewhere (eg via `forbid_access_if_lacking_session`/`valid_session?`).
|
230
|
+
decoded_header = JSON.parse(Base64.urlsafe_decode64(header))
|
231
|
+
PandaPal::Organization.find_by(id: decoded_header['o']).try(:tenant_name)
|
232
|
+
elsif request.path.start_with?('/rails/active_storage/blobs/')
|
233
|
+
PandaPal::Organization.find_by(id: request.params['organization_id']).try(:tenant_name)
|
234
|
+
end
|
235
|
+
tenant || 'public'
|
235
236
|
}
|
236
237
|
|
237
238
|
module PandaPal::Plugins::ApartmentCache
|
@@ -142,7 +142,7 @@ module PandaPal::Helpers
|
|
142
142
|
return false unless current_panda_session.panda_pal_organization_id == current_organization.id
|
143
143
|
return false unless Apartment::Tenant.current == current_organization.tenant_name
|
144
144
|
true
|
145
|
-
rescue SessionNonceMismatch
|
145
|
+
rescue PandaPal::Session::SessionNonceMismatch
|
146
146
|
false
|
147
147
|
end
|
148
148
|
|
@@ -1,6 +1,4 @@
|
|
1
1
|
module PandaPal::Helpers
|
2
|
-
class SessionNonceMismatch < StandardError; end
|
3
|
-
|
4
2
|
module SessionReplacement
|
5
3
|
extend ActiveSupport::Concern
|
6
4
|
|
@@ -43,64 +41,8 @@ module PandaPal::Helpers
|
|
43
41
|
def current_panda_session
|
44
42
|
return @current_session if defined?(@current_session)
|
45
43
|
|
46
|
-
if (panda_token =
|
47
|
-
|
48
|
-
# TYPE = KEY | T-URI | T-IP | T-NONCE | T-EXP
|
49
|
-
|
50
|
-
header, sig = panda_token.split('.')
|
51
|
-
decoded_header = JSON.parse(Base64.urlsafe_decode64(header))
|
52
|
-
|
53
|
-
org_id = decoded_header['o'].to_i
|
54
|
-
session_id = decoded_header['s']
|
55
|
-
type = decoded_header['typ']
|
56
|
-
|
57
|
-
# raise SessionNonceMismatch, "Session Not Found" unless org_id < 1 || org_id == current_organization.id
|
58
|
-
|
59
|
-
if type == 'KEY'
|
60
|
-
@current_session = PandaPal::Session.find_by(session_secret: sig)
|
61
|
-
raise SessionNonceMismatch, "Session Not Found" unless @current_session.present?
|
62
|
-
raise SessionNonceMismatch, "Session Not Found" if session_id && @current_session.id != session_id
|
63
|
-
else
|
64
|
-
session_record = PandaPal::Session.find_by(id: session_id)
|
65
|
-
raise SessionNonceMismatch, "Session Not Found" unless session_record.present?
|
66
|
-
|
67
|
-
# Validate the header against the signature
|
68
|
-
raise SessionNonceMismatch, "Invalid Signature" unless session_record.validate_signature(header, sig)
|
69
|
-
|
70
|
-
# Check expiration
|
71
|
-
if (expr = decoded_header['exp']).present?
|
72
|
-
raise SessionNonceMismatch, "Expired" unless expr > DateTime.now.iso8601
|
73
|
-
end
|
74
|
-
|
75
|
-
if type == "T-URI"
|
76
|
-
# Signed URLs only support GET requests
|
77
|
-
raise SessionNonceMismatch, "Invalid Method" unless request.method == :get
|
78
|
-
|
79
|
-
# Verify the signature against the request URL
|
80
|
-
# The usig doesn't _need_ to be signed (since it's part of the payload that is already signed),
|
81
|
-
# but this was an easy way to make the value a constant length. Any basic hashing function could solve this,
|
82
|
-
# but the HMAC implementation was already available.
|
83
|
-
resigned = generate_signed_url(session_record.signing_key, request.url)
|
84
|
-
raise SessionNonceMismatch, "Invalid Signature" unless resigned == decoded_header['usig']
|
85
|
-
|
86
|
-
# TODO Support single-use tokens via Redis?
|
87
|
-
|
88
|
-
@current_session = session_record
|
89
|
-
elsif type == "T-NONCE"
|
90
|
-
raise SessionNonceMismatch, "Invalid Nonce" unless session_record.data[:link_nonce] == decoded_header['nnc']
|
91
|
-
@current_session = session_record
|
92
|
-
@current_session.data[:link_nonce] = nil
|
93
|
-
elsif type == "T-IP"
|
94
|
-
raise SessionNonceMismatch, "Invalid IP" unless decoded_header["ip"] == request.remote_ip
|
95
|
-
@current_session = session_record
|
96
|
-
elsif type == "T-EXP"
|
97
|
-
# Expiration itself is checked above, but enforce that an expiration is set
|
98
|
-
raise SessionNonceMismatch, "Invalid Expiration" unless decoded_header["exp"].present?
|
99
|
-
@current_session = session_record
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
raise SessionNonceMismatch, "Session Not Found" unless @current_session.present?
|
44
|
+
if (panda_token = PandaPal::Session.extract_panda_token(request, params)).present?
|
45
|
+
@current_session = PandaPal::Session.for_panda_token(panda_token, request: request)
|
104
46
|
|
105
47
|
elsif (session_key = params[:session_key] || session_key_header || flash[:session_key] || session[:session_key]).present?
|
106
48
|
# Legacy-format session_keys
|
@@ -189,9 +131,10 @@ module PandaPal::Helpers
|
|
189
131
|
|
190
132
|
if type == 'url'
|
191
133
|
raise StandardError, "URL is required" unless url.present?
|
134
|
+
to_sign = PandaPal::Session.format_url_for_signing(url)
|
192
135
|
payload.merge!(
|
193
136
|
typ: 'T-URI',
|
194
|
-
usig:
|
137
|
+
usig: current_session.sign_value(to_sign),
|
195
138
|
)
|
196
139
|
else
|
197
140
|
@cached_link_nonces ||= {}
|
@@ -237,22 +180,9 @@ module PandaPal::Helpers
|
|
237
180
|
15
|
238
181
|
end
|
239
182
|
|
183
|
+
# @deprecated
|
240
184
|
def self.extract_panda_token(request, params = request.params)
|
241
|
-
|
242
|
-
token = headers['HTTP_X_PANDA_TOKEN'] || headers['X-Panda-Token']
|
243
|
-
|
244
|
-
token ||= begin
|
245
|
-
if (auth_header = headers['HTTP_AUTHORIZATION'] || headers['Authorization']).present?
|
246
|
-
match = auth_header.match(/Bearer panda:(.+)/)
|
247
|
-
match ||= auth_header.match(/token=(.+)/) # Legacy Support
|
248
|
-
token = match[1] if match && match[1].include?('.')
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
token ||= params["panda_token"]
|
253
|
-
token ||= params["session_token"] if params["session_token"]&.include?('.') # Legacy Support
|
254
|
-
token ||= params["session_key"] if params["session_key"]&.include?('.') # Legacy Support
|
255
|
-
token.presence
|
185
|
+
PandaPal::Session.extract_panda_token(request, params)
|
256
186
|
end
|
257
187
|
|
258
188
|
private
|
@@ -262,6 +192,7 @@ module PandaPal::Helpers
|
|
262
192
|
@session_cryptor ||= ActiveSupport::MessageEncryptor.new(secret_key_base[0..31])
|
263
193
|
end
|
264
194
|
|
195
|
+
# @deprecated
|
265
196
|
def session_key_header
|
266
197
|
if match = request.headers['Authorization'].try(:match, /token=(.+)/)
|
267
198
|
match[1]
|
@@ -273,19 +204,6 @@ module PandaPal::Helpers
|
|
273
204
|
save_session if @current_session && session_changed?
|
274
205
|
end
|
275
206
|
|
276
|
-
def generate_signed_url(key, uri)
|
277
|
-
uri = URI.parse(uri) if uri.is_a?(String)
|
278
|
-
signable = "#{uri.path}?#{uri.query}"
|
279
|
-
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, signable)
|
280
|
-
end
|
281
|
-
|
282
|
-
def validate_url_signature(key, uri, signature)
|
283
|
-
uri = URI.parse(uri) if uri.is_a?(String)
|
284
|
-
signable = uri.path + '?' + uri.query
|
285
|
-
signable.gsub!(/[&?](session_key|session_token|panda_token)=.*?(&.*)?$/, "")
|
286
|
-
signature == OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, signable)
|
287
|
-
end
|
288
|
-
|
289
207
|
def monkeypatch_flash
|
290
208
|
if valid_session? && (value = current_session['flashes']).present?
|
291
209
|
flashes = value["flashes"]
|
data/lib/panda_pal/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: panda_pal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Instructure CustomDev
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rails
|
@@ -282,7 +281,6 @@ dependencies:
|
|
282
281
|
- - '='
|
283
282
|
- !ruby/object:Gem::Version
|
284
283
|
version: 2.7.1
|
285
|
-
description:
|
286
284
|
email:
|
287
285
|
- pseng@instructure.com
|
288
286
|
executables: []
|
@@ -400,7 +398,6 @@ homepage: http://instructure.com
|
|
400
398
|
licenses:
|
401
399
|
- MIT
|
402
400
|
metadata: {}
|
403
|
-
post_install_message:
|
404
401
|
rdoc_options: []
|
405
402
|
require_paths:
|
406
403
|
- lib
|
@@ -415,8 +412,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
415
412
|
- !ruby/object:Gem::Version
|
416
413
|
version: '0'
|
417
414
|
requirements: []
|
418
|
-
rubygems_version: 3.
|
419
|
-
signing_key:
|
415
|
+
rubygems_version: 3.6.9
|
420
416
|
specification_version: 4
|
421
417
|
summary: LTI mountable engine
|
422
418
|
test_files:
|
@@ -433,6 +429,7 @@ test_files:
|
|
433
429
|
- spec/dummy/app/controllers/application_controller.rb
|
434
430
|
- spec/dummy/app/helpers/application_helper.rb
|
435
431
|
- spec/dummy/app/views/layouts/application.html.erb
|
432
|
+
- spec/dummy/config.ru
|
436
433
|
- spec/dummy/config/application.rb
|
437
434
|
- spec/dummy/config/boot.rb
|
438
435
|
- spec/dummy/config/database.yml
|
@@ -450,7 +447,6 @@ test_files:
|
|
450
447
|
- spec/dummy/config/locales/en.yml
|
451
448
|
- spec/dummy/config/routes.rb
|
452
449
|
- spec/dummy/config/secrets.yml
|
453
|
-
- spec/dummy/config.ru
|
454
450
|
- spec/dummy/db/schema.rb
|
455
451
|
- spec/dummy/db/test2_schema.rb
|
456
452
|
- spec/dummy/public/404.html
|