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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e77e7eba739ca8bf0afe44555c8af8c9188f4ce8668310aad110a2afc638701
4
- data.tar.gz: 19db55450dfe15e7aa4f678d7556ec427e52bcad421a096791d47a5a3fe128b4
3
+ metadata.gz: 0cebe935d536e1f903b212075108ba9cbcade4aa2e8d2abf1c5e0d6e6f539ce8
4
+ data.tar.gz: 9b0c576aaa36c5a05596c1bc1bec23d3ab0f37c56dc72342bfa962b10442928f
5
5
  SHA512:
6
- metadata.gz: f504398b08e35ca42b765afca4357d750836ce5d7e3e7ec9ba73b061dde57fd2bbd3fa5d7752b9e6995739fac8a8718b0a8a71d70432ec14b65c95e13dadeb29
7
- data.tar.gz: ceb20219c23d4e44d006c5fe177fff3ef2cacb77ec75e109b92c03d12f0e9cf434459759d007df96becfa0e426c89f4f8f9517c00c579135cfbb0e613852a0a1
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
- # You can use +:root+, +:method+, and +:params+ at the same time:
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', root: '/widget', method: :get, params: {foo: 'bar'})
118
- # # => "/widget/9169af1b8f40c62a1c2bb15b1b377c65bda681b8efded0e613a4176387468c15/mp/1?foo=bar"
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. If the
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
- Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, secret: old_secret), hmac)
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
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 79
7
+ RodaMinorVersion = 80
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
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.79.0
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-04-12 00:00:00.000000000 Z
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.3
510
+ rubygems_version: 3.5.9
509
511
  signing_key:
510
512
  specification_version: 4
511
513
  summary: Routing tree web toolkit