roda 3.79.0 → 3.80.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 +4 -4
- data/CHANGELOG +4 -0
- data/doc/release_notes/3.80.0.txt +31 -0
- data/lib/roda/plugins/hmac_paths.rb +117 -10
- data/lib/roda/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0cebe935d536e1f903b212075108ba9cbcade4aa2e8d2abf1c5e0d6e6f539ce8
|
4
|
+
data.tar.gz: 9b0c576aaa36c5a05596c1bc1bec23d3ab0f37c56dc72342bfa962b10442928f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e83d0fe8e1f70bb196ea5f285f7a00590ab1669e4b1d686f859d1e5c65a8e760d1320b345306740d00c7d063f0f6bada1af88433b3b04c6b429f439bb7e2521
|
7
|
+
data.tar.gz: e9b0ffd5b5fb7e976a971f11fbed4f0beb35868dcb7aa70c41c005046a9053cb6c62caf29f669c02cd53e67cd5db51a28076f058824c90a1c1b917a9b7f0f4fb
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
= 3.80.0 (2024-05-10)
|
2
|
+
|
3
|
+
* Support :namespace option in hmac_paths plugin, allowing for easy per-user/per-group HMAC paths (jeremyevans)
|
4
|
+
|
1
5
|
= 3.79.0 (2024-04-12)
|
2
6
|
|
3
7
|
* Do not update template mtime when there is an error reloading templates in the render plugin (jeremyevans)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* The hmac_paths plugin now supports a :namespace option for both hmac_path and
|
4
|
+
r.hmac_path. The :namespace option makes the generated HMAC values unique
|
5
|
+
per namespace, allowing easy use of per user/group HMAC paths. This can
|
6
|
+
be useful if the same path will show different information to different
|
7
|
+
users/groups, and you want to prevent path enumeration for each user/group
|
8
|
+
(not allow paths enumerated by one user/group to be valid for a different
|
9
|
+
user/group). Example:
|
10
|
+
|
11
|
+
hmac_path('/widget/1', namespace: '1')
|
12
|
+
# => "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1"
|
13
|
+
|
14
|
+
hmac_path('/widget/1', namespace: '2')
|
15
|
+
# => "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1"
|
16
|
+
|
17
|
+
The HMAC path created with namespace: '1' will only be valid when calling
|
18
|
+
r.hmac_path with namespace: '1' (similar for namespace: '2').
|
19
|
+
|
20
|
+
It is expected that the most common use of the :namespace option is to
|
21
|
+
reference session values, so the value of each path depends on the logged in
|
22
|
+
user. You can use the :namespace_session_key plugin option to set the
|
23
|
+
default namespace for both hmac_path and r.hmac_path:
|
24
|
+
|
25
|
+
plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes',
|
26
|
+
namespace_session_key: 'account_id'
|
27
|
+
|
28
|
+
This will use <tt>session['account_id']</tt> (converted to a string) as the namespace
|
29
|
+
for both hmac_path and r.hmac_path, unless a specific :namespace option is
|
30
|
+
given, making it simple to implement per user/group HMAC paths across an
|
31
|
+
application.
|
@@ -112,13 +112,43 @@ class Roda
|
|
112
112
|
# this for POST requests (or other HTTP verbs that can have request bodies), use +r.GET+
|
113
113
|
# instead of +r.params+ to specifically check query string parameters.
|
114
114
|
#
|
115
|
-
#
|
115
|
+
# The :namespace option, if provided, should be a string, and it modifies the generated HMACs
|
116
|
+
# to only match those in the same namespace. This can be used to provide different paths to
|
117
|
+
# different users or groups of users.
|
116
118
|
#
|
117
|
-
# hmac_path('/1',
|
118
|
-
# # => "/
|
119
|
+
# hmac_path('/widget/1', namespace: '1')
|
120
|
+
# # => "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1"
|
121
|
+
#
|
122
|
+
# hmac_path('/widget/1', namespace: '2')
|
123
|
+
# # => "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1"
|
124
|
+
#
|
125
|
+
# The +r.hmac_path+ method accepts a :namespace option, and if a :namespace option is
|
126
|
+
# provided, it will only match an hmac path if the namespace given matches the one used
|
127
|
+
# when the hmac path was created.
|
128
|
+
#
|
129
|
+
# r.hmac_path(namespace: '1'){}
|
130
|
+
# # will match "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1"
|
131
|
+
# # will not match "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1"
|
132
|
+
#
|
133
|
+
# The most common use of the :namespace option is to reference session values, so the value of
|
134
|
+
# each path depends on the logged in user. You can use the +:namespace_session_key+ plugin
|
135
|
+
# option to set the default namespace for both +hmac_path+ and +r.hmac_path+:
|
136
|
+
#
|
137
|
+
# plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes',
|
138
|
+
# namespace_session_key: 'account_id'
|
139
|
+
#
|
140
|
+
# This will use <tt>session['account_id']</tt> as the default namespace for both +hmac_path+
|
141
|
+
# and +r.hmac_path+ (if the session value is not nil, it is converted to a string using +to_s+).
|
142
|
+
# You can override the default namespace by passing a +:namespace+ option when calling +hmac_path+
|
143
|
+
# and +r.hmac_path+.
|
144
|
+
#
|
145
|
+
# You can use +:root+, +:method+, +:params+, and +:namespace+ at the same time:
|
146
|
+
#
|
147
|
+
# hmac_path('/1', root: '/widget', method: :get, params: {foo: 'bar'}, namespace: '1')
|
148
|
+
# # => "/widget/c14c78a81d34d766cf334a3ddbb7a6b231bc2092ef50a77ded0028586027b14e/mpn/1?foo=bar"
|
119
149
|
#
|
120
150
|
# This gives you a path only valid for a GET request with a root of <tt>/widget</tt> and
|
121
|
-
# a query string of <tt>foo=bar</tt
|
151
|
+
# a query string of <tt>foo=bar</tt>, using namespace +1+.
|
122
152
|
#
|
123
153
|
# To handle secret rotation, you can provide an +:old_secret+ option when loading the
|
124
154
|
# plugin.
|
@@ -128,6 +158,43 @@ class Roda
|
|
128
158
|
#
|
129
159
|
# This will use +:secret+ for constructing new paths, but will respect paths generated by
|
130
160
|
# +:old_secret+.
|
161
|
+
#
|
162
|
+
# = HMAC Construction
|
163
|
+
#
|
164
|
+
# This describes the internals for how HMACs are constructed based on the options provided
|
165
|
+
# to +hmac_path+. In the examples below:
|
166
|
+
#
|
167
|
+
# * +HMAC+ is the raw HMAC-SHA256 output (first argument is secret, second is data)
|
168
|
+
# * +HMAC_hex+ is the hexidecimal version of +HMAC+
|
169
|
+
# * +secret+ is the plugin :secret option
|
170
|
+
#
|
171
|
+
# The +:secret+ plugin option is never used directly as the HMAC secret. All HMACs are
|
172
|
+
# generated with a root-specific secret. The root will be the empty if no +:root+ option
|
173
|
+
# is given. The hmac path flags are always included in the hmac calculation, prepended to the
|
174
|
+
# path:
|
175
|
+
#
|
176
|
+
# r.hmac_path('/1')
|
177
|
+
# HMAC_hex(HMAC_hex(secret, ''), '/0/1')
|
178
|
+
#
|
179
|
+
# r.hmac_path('/1', root: '/2')
|
180
|
+
# HMAC_hex(HMAC_hex(secret, '/2'), '/0/1')
|
181
|
+
#
|
182
|
+
# The +:method+ option uses an uppercase version of the method prepended to the path. This
|
183
|
+
# cannot conflict with the path itself, since paths must start with a slash.
|
184
|
+
#
|
185
|
+
# r.hmac_path('/1', method: :get)
|
186
|
+
# HMAC_hex(HMAC_hex(secret, ''), 'GET:/m/1')
|
187
|
+
#
|
188
|
+
# The +:params+ option includes the query string for the params in the HMAC:
|
189
|
+
#
|
190
|
+
# r.hmac_path('/1', params: {k: 2})
|
191
|
+
# HMAC_hex(HMAC_hex(secret, ''), '/p/1?k=2')
|
192
|
+
#
|
193
|
+
# If a +:namespace+ option is provided, the original secret used before the +:root+ option is
|
194
|
+
# an HMAC of the +:secret+ plugin option and the given namespace.
|
195
|
+
#
|
196
|
+
# r.hmac_path('/1', namespace: '2')
|
197
|
+
# HMAC_hex(HMAC_hex(HMAC(secret, '2'), ''), '/n/1')
|
131
198
|
module HmacPaths
|
132
199
|
def self.configure(app, opts=OPTS)
|
133
200
|
hmac_secret = opts[:secret]
|
@@ -143,6 +210,10 @@ class Roda
|
|
143
210
|
|
144
211
|
app.opts[:hmac_paths_secret] = hmac_secret
|
145
212
|
app.opts[:hmac_paths_old_secret] = hmac_old_secret
|
213
|
+
|
214
|
+
if opts[:namespace_session_key]
|
215
|
+
app.opts[:hmac_paths_namespace_session_key] = opts[:namespace_session_key]
|
216
|
+
end
|
146
217
|
end
|
147
218
|
|
148
219
|
module InstanceMethods
|
@@ -152,6 +223,9 @@ class Roda
|
|
152
223
|
# valid paths. The given path should be a string starting with +/+. Options:
|
153
224
|
#
|
154
225
|
# :method :: Limits the returned path to only be valid for the given request method.
|
226
|
+
# :namespace :: Make the HMAC value depend on the given namespace. If this is not
|
227
|
+
# provided, the default namespace is used. To explicitly not use a
|
228
|
+
# namespace when there is a default namespace, pass a nil value.
|
155
229
|
# :params :: Includes parameters in the query string of the returned path, and
|
156
230
|
# limits the returned path to only be valid for that exact query string.
|
157
231
|
# :root :: Should be an empty string or string starting with +/+. This will be
|
@@ -180,6 +254,10 @@ class Roda
|
|
180
254
|
path << '?' << Rack::Utils.build_query(params)
|
181
255
|
end
|
182
256
|
|
257
|
+
if hmac_path_namespace(opts)
|
258
|
+
flags << 'n'
|
259
|
+
end
|
260
|
+
|
183
261
|
flags << '0' if flags.empty?
|
184
262
|
|
185
263
|
hmac_path = if method
|
@@ -188,7 +266,7 @@ class Roda
|
|
188
266
|
"/#{flags}#{path}"
|
189
267
|
end
|
190
268
|
|
191
|
-
"#{root}/#{hmac_path_hmac(root, hmac_path)}/#{flags}#{path}"
|
269
|
+
"#{root}/#{hmac_path_hmac(root, hmac_path, opts)}/#{flags}#{path}"
|
192
270
|
end
|
193
271
|
|
194
272
|
# The HMAC to use in hmac_path, for the given root, path, and options.
|
@@ -196,14 +274,34 @@ class Roda
|
|
196
274
|
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, hmac_path_hmac_secret(root, opts), path)
|
197
275
|
end
|
198
276
|
|
277
|
+
# The namespace to use for the hmac path. If a :namespace option is not
|
278
|
+
# provided, and a :namespace_session_key option was provided, this will
|
279
|
+
# use the value of the related session key, if present.
|
280
|
+
def hmac_path_namespace(opts=OPTS)
|
281
|
+
opts.fetch(:namespace){hmac_path_default_namespace}
|
282
|
+
end
|
283
|
+
|
199
284
|
private
|
200
285
|
|
201
286
|
# The secret used to calculate the HMAC in hmac_path. This is itself an HMAC, created
|
202
|
-
# using the secret given in the plugin, for the given root and options.
|
287
|
+
# using the secret given in the plugin, for the given root and options.
|
288
|
+
# This always returns a hexidecimal string.
|
203
289
|
def hmac_path_hmac_secret(root, opts=OPTS)
|
204
290
|
secret = opts[:secret] || self.opts[:hmac_paths_secret]
|
291
|
+
|
292
|
+
if namespace = hmac_path_namespace(opts)
|
293
|
+
secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, namespace)
|
294
|
+
end
|
295
|
+
|
205
296
|
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, root)
|
206
297
|
end
|
298
|
+
|
299
|
+
# The default namespace to use for hmac_path, if a :namespace option is not provided.
|
300
|
+
def hmac_path_default_namespace
|
301
|
+
if (key = opts[:hmac_paths_namespace_session_key]) && (value = session[key])
|
302
|
+
value.to_s
|
303
|
+
end
|
304
|
+
end
|
207
305
|
end
|
208
306
|
|
209
307
|
module RequestMethods
|
@@ -221,6 +319,13 @@ class Roda
|
|
221
319
|
if submitted_hmac.bytesize == 64
|
222
320
|
on String do |flags|
|
223
321
|
if flags.bytesize >= 1
|
322
|
+
if flags.include?('n') ^ !scope.hmac_path_namespace(opts).nil?
|
323
|
+
# Namespace required and not provided, or provided and not required.
|
324
|
+
# Bail early to avoid unnecessary HMAC calculation.
|
325
|
+
@remaining_path = orig_path
|
326
|
+
return
|
327
|
+
end
|
328
|
+
|
224
329
|
if flags.include?('m')
|
225
330
|
rpath = "#{env['REQUEST_METHOD'].to_s.upcase}:#{rpath}"
|
226
331
|
end
|
@@ -229,7 +334,7 @@ class Roda
|
|
229
334
|
rpath = "#{rpath}?#{env["QUERY_STRING"]}"
|
230
335
|
end
|
231
336
|
|
232
|
-
if hmac_path_valid?(mpath, rpath, submitted_hmac)
|
337
|
+
if hmac_path_valid?(mpath, rpath, submitted_hmac, opts)
|
233
338
|
always(&block)
|
234
339
|
end
|
235
340
|
end
|
@@ -249,11 +354,13 @@ class Roda
|
|
249
354
|
private
|
250
355
|
|
251
356
|
# Determine whether the provided hmac matches.
|
252
|
-
def hmac_path_valid?(root, path, hmac)
|
253
|
-
if Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path), hmac)
|
357
|
+
def hmac_path_valid?(root, path, hmac, opts=OPTS)
|
358
|
+
if Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac)
|
254
359
|
true
|
255
360
|
elsif old_secret = roda_class.opts[:hmac_paths_old_secret]
|
256
|
-
|
361
|
+
opts = opts.dup
|
362
|
+
opts[:secret] = old_secret
|
363
|
+
Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac)
|
257
364
|
else
|
258
365
|
false
|
259
366
|
end
|
data/lib/roda/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: roda
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.80.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-05-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -254,6 +254,7 @@ extra_rdoc_files:
|
|
254
254
|
- doc/release_notes/3.78.0.txt
|
255
255
|
- doc/release_notes/3.79.0.txt
|
256
256
|
- doc/release_notes/3.8.0.txt
|
257
|
+
- doc/release_notes/3.80.0.txt
|
257
258
|
- doc/release_notes/3.9.0.txt
|
258
259
|
files:
|
259
260
|
- CHANGELOG
|
@@ -340,6 +341,7 @@ files:
|
|
340
341
|
- doc/release_notes/3.78.0.txt
|
341
342
|
- doc/release_notes/3.79.0.txt
|
342
343
|
- doc/release_notes/3.8.0.txt
|
344
|
+
- doc/release_notes/3.80.0.txt
|
343
345
|
- doc/release_notes/3.9.0.txt
|
344
346
|
- lib/roda.rb
|
345
347
|
- lib/roda/cache.rb
|
@@ -505,7 +507,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
505
507
|
- !ruby/object:Gem::Version
|
506
508
|
version: '0'
|
507
509
|
requirements: []
|
508
|
-
rubygems_version: 3.5.
|
510
|
+
rubygems_version: 3.5.9
|
509
511
|
signing_key:
|
510
512
|
specification_version: 4
|
511
513
|
summary: Routing tree web toolkit
|