roda 3.79.0 → 3.80.0

Sign up to get free protection for your applications and to get access to all the features.
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