roda 3.76.0 → 3.78.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: 1f4eee948a9994645560f635fd228d2f46f3f466da782a476d7046b2b0b9f026
4
- data.tar.gz: 4abb6bed043b264c59e17b1120771822a26292f24037a283c08003168c72e602
3
+ metadata.gz: 44eb48fa9dd507c495e32f90e5ef3b9b197d47f1c148f5d91714272afdca74f8
4
+ data.tar.gz: 276ceaf7256b816b4a5c59e7c2508cb9066e1d1693d398e7be43ac17be7a9115
5
5
  SHA512:
6
- metadata.gz: 112d90a74ed25ae0bb608a2e89d2f6fa757912287feb3a68d312ef4b8fd317bdb8f04eb76f529090d14a92c322d869ededa831f8db5d20bce8a66cd973a71584
7
- data.tar.gz: 22ddb0a055b5849c6dcd3ec93426e495198434cf1e9cf9adbae1c22ece9b788c0ea7332122b9ad525954e2b946b0eef873de3b0466fe5b844407dec6c9844c84
6
+ metadata.gz: b99672a570f6c119375435546104627d27437cb66372f61df0b7e08a9eb4ae66709b33162d2cf0354b2a22c3ed7c456b60423218cd734a9fe644c9b278c5c44f
7
+ data.tar.gz: 6bd6f328f173bfe2dd28b1b4ea9452158f127ac2c93dcf4de6783af5513d38803e9642c3c24b899e16d05dbdb35569464dc9ca2f00111a8814193e48870afe45
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ = 3.78.0 (2024-03-13)
2
+
3
+ * Add permissions_policy plugin for setting Permissions-Policy header (jeremyevans)
4
+
5
+ = 3.77.0 (2024-02-12)
6
+
7
+ * Support formaction/formmethod attributes in forms in route_csrf plugin (jeremyevans)
8
+
1
9
  = 3.76.0 (2024-01-12)
2
10
 
3
11
  * Support :filter plugin option in error_mail and error_email for filtering parameters, environment variables, and session values (jeremyevans) (#346)
@@ -0,0 +1,8 @@
1
+ = New Features
2
+
3
+ * The route_csrf plugin now supports formaction/formmethod attributes
4
+ in forms. A csrf_formaction_tag method has been added for creating
5
+ a hidden input for a particular path and method. When a form is
6
+ submitted, the check_csrf! method will fix check for a path-specific
7
+ csrf token (set by the hidden tag added by the csrf_formaction_tag
8
+ method), before checking for the default csrf token.
@@ -0,0 +1,99 @@
1
+ = New Features
2
+
3
+ * A permissions_policy plugin has been added that allows you to easily set a
4
+ Permissions-Policy header for the application, which browsers can use to
5
+ determine whether to allow specific functionality on the returned page
6
+ (mainly related to which JavaScript APIs the page is allowed to use).
7
+
8
+ You would generally call the plugin with a block to set the default policy:
9
+
10
+ plugin :permissions_policy do |pp|
11
+ pp.camera :none
12
+ pp.fullscreen :self
13
+ pp.clipboard_read :self, 'https://example.com'
14
+ end
15
+
16
+ Then, anywhere in the routing tree, you can customize the policy for just that
17
+ branch or action using the same block syntax:
18
+
19
+ r.get 'foo' do
20
+ permissions_policy do |pp|
21
+ pp.camera :self
22
+ end
23
+ # ...
24
+ end
25
+
26
+ In addition to using a block, you can also call methods on the object returned
27
+ by the method:
28
+
29
+ r.get 'foo' do
30
+ permissions_policy.camera :self
31
+ # ...
32
+ end
33
+
34
+ You can use the :default plugin option to set the default for all settings.
35
+ For example, to disallow all access for each setting by default:
36
+
37
+ plugin :permissions_policy, default: :none
38
+
39
+ The following methods are available for configuring the permissions policy,
40
+ which specify the setting (substituting _ with -):
41
+
42
+ * accelerometer
43
+ * ambient_light_sensor
44
+ * autoplay
45
+ * bluetooth
46
+ * camera
47
+ * clipboard_read
48
+ * clipboard_write
49
+ * display_capture
50
+ * encrypted_media
51
+ * fullscreen
52
+ * geolocation
53
+ * gyroscope
54
+ * hid
55
+ * idle_detection
56
+ * keyboard_map
57
+ * magnetometer
58
+ * microphone
59
+ * midi
60
+ * payment
61
+ * picture_in_picture
62
+ * publickey_credentials_get
63
+ * screen_wake_lock
64
+ * serial
65
+ * sync_xhr
66
+ * usb
67
+ * web_share
68
+ * window_management
69
+
70
+ All of these methods support any number of arguments, and each argument should
71
+ be one of the following values:
72
+
73
+ :all :: Grants permission to all domains (must be only argument)
74
+ :none :: Does not allow permission at all (must be only argument)
75
+ :self :: Allows feature in current document and any nested browsing contexts
76
+ that use the same domain as the current document.
77
+ :src :: Allows feature in current document and any nested browsing contexts
78
+ that use the same domain as the src of the iframe.
79
+ String :: Specifies origin domain where access is allowed
80
+
81
+ When calling a method with no arguments, the setting is removed from the policy instead
82
+ of being left empty, since all of these setting require at least one value. Likewise,
83
+ if the policy does not have any settings, the header will not be added.
84
+
85
+ Calling the method overrides any previous setting. Each of the methods has +add_*+ and
86
+ +get_*+ methods defined. The +add_*+ method appends to any existing setting, and the +get_*+ method
87
+ returns the current value for the setting (this will be +:all+ if all domains are allowed, or
88
+ any array of strings/:self/:src).
89
+
90
+ permissions_policy.fullscreen :self, 'https://example.com'
91
+ # fullscreen (self "https://example.com")
92
+
93
+ permissions_policy.add_fullscreen 'https://*.example.com'
94
+ # fullscreen (self "https://example.com" "https://*.example.com")
95
+
96
+ permissions_policy.get_fullscreen
97
+ # => [:self, "https://example.com", "https://*.example.com"]
98
+
99
+ The clear method can be used to remove all settings from the policy.
@@ -89,7 +89,7 @@ class Roda
89
89
  # content_security_policy.add_script_src 'example.com', [:nonce, 'foobarbaz']
90
90
  # # script-src 'self' 'unsafe-eval' example.com 'nonce-foobarbaz';
91
91
  #
92
- # content_security_policy.get_script_src 'example.com', [:nonce, 'foobarbaz']
92
+ # content_security_policy.get_script_src
93
93
  # # => [:self, :unsafe_eval, 'example.com', [:nonce, 'foobarbaz']]
94
94
  #
95
95
  # The clear method can be used to remove all settings from the policy.
@@ -0,0 +1,326 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # A permissions_policy plugin has been added that allows you to easily set a
7
+ # Permissions-Policy header for the application, which browsers can use to
8
+ # determine whether to allow specific functionality on the returned page
9
+ # (mainly related to which JavaScript APIs the page is allowed to use).
10
+ #
11
+ # You would generally call the plugin with a block to set the default policy:
12
+ #
13
+ # plugin :permissions_policy do |pp|
14
+ # pp.camera :none
15
+ # pp.fullscreen :self
16
+ # pp.clipboard_read :self, 'https://example.com'
17
+ # end
18
+ #
19
+ # Then, anywhere in the routing tree, you can customize the policy for just that
20
+ # branch or action using the same block syntax:
21
+ #
22
+ # r.get 'foo' do
23
+ # permissions_policy do |pp|
24
+ # pp.camera :self
25
+ # end
26
+ # # ...
27
+ # end
28
+ #
29
+ # In addition to using a block, you can also call methods on the object returned
30
+ # by the method:
31
+ #
32
+ # r.get 'foo' do
33
+ # permissions_policy.camera :self
34
+ # # ...
35
+ # end
36
+ #
37
+ # You can use the :default plugin option to set the default for all settings.
38
+ # For example, to disallow all access for each setting by default:
39
+ #
40
+ # plugin :permissions_policy, default: :none
41
+ #
42
+ # The following methods are available for configuring the permissions policy,
43
+ # which specify the setting (substituting _ with -):
44
+ #
45
+ # * accelerometer
46
+ # * ambient_light_sensor
47
+ # * autoplay
48
+ # * bluetooth
49
+ # * camera
50
+ # * clipboard_read
51
+ # * clipboard_write
52
+ # * display_capture
53
+ # * encrypted_media
54
+ # * fullscreen
55
+ # * geolocation
56
+ # * gyroscope
57
+ # * hid
58
+ # * idle_detection
59
+ # * keyboard_map
60
+ # * magnetometer
61
+ # * microphone
62
+ # * midi
63
+ # * payment
64
+ # * picture_in_picture
65
+ # * publickey_credentials_get
66
+ # * screen_wake_lock
67
+ # * serial
68
+ # * sync_xhr
69
+ # * usb
70
+ # * web_share
71
+ # * window_management
72
+ #
73
+ # All of these methods support any number of arguments, and each argument should
74
+ # be one of the following values:
75
+ #
76
+ # :all :: Grants permission to all domains (must be only argument)
77
+ # :none :: Does not allow permission at all (must be only argument)
78
+ # :self :: Allows feature in current document and any nested browsing contexts
79
+ # that use the same domain as the current document.
80
+ # :src :: Allows feature in current document and any nested browsing contexts
81
+ # that use the same domain as the src of the iframe.
82
+ # String :: Specifies origin domain where access is allowed
83
+ #
84
+ # When calling a method with no arguments, the setting is removed from the policy instead
85
+ # of being left empty, since all of these setting require at least one value. Likewise,
86
+ # if the policy does not have any settings, the header will not be added.
87
+ #
88
+ # Calling the method overrides any previous setting. Each of the methods has +add_*+ and
89
+ # +get_*+ methods defined. The +add_*+ method appends to any existing setting, and the +get_*+ method
90
+ # returns the current value for the setting (this will be +:all+ if all domains are allowed, or
91
+ # any array of strings/:self/:src).
92
+ #
93
+ # permissions_policy.fullscreen :self, 'https://example.com'
94
+ # # fullscreen (self "https://example.com")
95
+ #
96
+ # permissions_policy.add_fullscreen 'https://*.example.com'
97
+ # # fullscreen (self "https://example.com" "https://*.example.com")
98
+ #
99
+ # permissions_policy.get_fullscreen
100
+ # # => [:self, "https://example.com", "https://*.example.com"]
101
+ #
102
+ # The clear method can be used to remove all settings from the policy.
103
+ module PermissionsPolicy
104
+ SUPPORTED_SETTINGS = %w'
105
+ accelerometer
106
+ ambient-light-sensor
107
+ autoplay
108
+ bluetooth
109
+ camera
110
+ clipboard-read
111
+ clipboard-write
112
+ display-capture
113
+ encrypted-media
114
+ fullscreen
115
+ geolocation
116
+ gyroscope
117
+ hid
118
+ idle-detection
119
+ keyboard-map
120
+ magnetometer
121
+ microphone
122
+ midi
123
+ payment
124
+ picture-in-picture
125
+ publickey-credentials-get
126
+ screen-wake-lock
127
+ serial
128
+ sync-xhr
129
+ usb
130
+ web-share
131
+ window-management
132
+ '.each(&:freeze).freeze
133
+ private_constant :SUPPORTED_SETTINGS
134
+
135
+ # Represents a permissions policy.
136
+ class Policy
137
+ SUPPORTED_SETTINGS.each do |setting|
138
+ meth = setting.gsub('-', '_').freeze
139
+
140
+ # Setting method name sets the setting value, or removes it if no args are given.
141
+ define_method(meth) do |*args|
142
+ if args.empty?
143
+ @opts.delete(setting)
144
+ else
145
+ @opts[setting] = option_value(args)
146
+ end
147
+ nil
148
+ end
149
+
150
+ # add_* method name adds to the setting value, or clears setting if no values
151
+ # are given.
152
+ define_method(:"add_#{meth}") do |*args|
153
+ unless args.empty?
154
+ case v = @opts[setting]
155
+ when :all
156
+ # If all domains are already allowed, there is no reason to add more.
157
+ return
158
+ when Array
159
+ @opts[setting] = option_value(v + args)
160
+ else
161
+ @opts[setting] = option_value(args)
162
+ end
163
+ end
164
+ nil
165
+ end
166
+
167
+ # get_* method always returns current setting value.
168
+ define_method(:"get_#{meth}") do
169
+ @opts[setting]
170
+ end
171
+ end
172
+
173
+ def initialize
174
+ clear
175
+ end
176
+
177
+ # Clear all settings, useful to remove any inherited settings.
178
+ def clear
179
+ @opts = {}
180
+ end
181
+
182
+ # Do not allow future modifications to any settings.
183
+ def freeze
184
+ @opts.freeze
185
+ header_value.freeze
186
+ super
187
+ end
188
+
189
+ # The header name to use, depends on whether report only mode has been enabled.
190
+ def header_key
191
+ @report_only ? RodaResponseHeaders::PERMISSIONS_POLICY_REPORT_ONLY : RodaResponseHeaders::PERMISSIONS_POLICY
192
+ end
193
+
194
+ # The header value to use.
195
+ def header_value
196
+ return @header_value if @header_value
197
+
198
+ s = String.new
199
+ @opts.each do |k, vs|
200
+ s << k << "="
201
+
202
+ if vs == :all
203
+ s << '*, '
204
+ else
205
+ s << '('
206
+ vs.each{|v| append_formatted_value(s, v)}
207
+ s.chop! unless vs.empty?
208
+ s << '), '
209
+ end
210
+ end
211
+ s.chop!
212
+ s.chop!
213
+ @header_value = s
214
+ end
215
+
216
+ # Set whether the Permissions-Policy-Report-Only header instead of the
217
+ # default Permissions-Policy header.
218
+ def report_only(report=true)
219
+ @report_only = report
220
+ end
221
+
222
+ # Whether this policy uses report only mode.
223
+ def report_only?
224
+ !!@report_only
225
+ end
226
+
227
+ # Set the current policy in the headers hash. If no settings have been made
228
+ # in the policy, does not set a header.
229
+ def set_header(headers)
230
+ return if @opts.empty?
231
+ headers[header_key] ||= header_value
232
+ end
233
+
234
+ private
235
+
236
+ # Formats nested values, quoting strings and using :self and :src verbatim.
237
+ def append_formatted_value(s, v)
238
+ case v
239
+ when String
240
+ s << v.inspect << ' '
241
+ when :self
242
+ s << 'self '
243
+ when :src
244
+ s << 'src '
245
+ else
246
+ raise RodaError, "unsupported Permissions-Policy item value used: #{v.inspect}"
247
+ end
248
+ end
249
+
250
+ # Make object copy use copy of settings, and remove cached header value.
251
+ def initialize_copy(_)
252
+ super
253
+ @opts = @opts.dup
254
+ @header_value = nil
255
+ end
256
+
257
+ # The option value to store for the given args.
258
+ def option_value(args)
259
+ if args.length == 1
260
+ case args[0]
261
+ when :all
262
+ :all
263
+ when :none
264
+ EMPTY_ARRAY
265
+ else
266
+ args.freeze
267
+ end
268
+ else
269
+ args.freeze
270
+ end
271
+ end
272
+ end
273
+
274
+ # Yield the current Permissions Policy to the block.
275
+ def self.configure(app, opts=OPTS)
276
+ policy = app.opts[:permissions_policy] = if policy = app.opts[:permissions_policy]
277
+ policy.dup
278
+ else
279
+ Policy.new
280
+ end
281
+
282
+ if default = opts[:default]
283
+ SUPPORTED_SETTINGS.each do |setting|
284
+ policy.send(setting.gsub('-', '_'), *default)
285
+ end
286
+ end
287
+
288
+ yield policy if defined?(yield)
289
+ policy.freeze
290
+ end
291
+
292
+ module InstanceMethods
293
+ # If a block is given, yield the current permission policy. Returns the
294
+ # current permissions policy.
295
+ def permissions_policy
296
+ policy = @_response.permissions_policy
297
+ yield policy if defined?(yield)
298
+ policy
299
+ end
300
+ end
301
+
302
+ module ResponseMethods
303
+ # Unset any permissions policy when reinitializing
304
+ def initialize
305
+ super
306
+ @permissions_policy &&= nil
307
+ end
308
+
309
+ # The current permissions policy to be used for this response.
310
+ def permissions_policy
311
+ @permissions_policy ||= roda_class.opts[:permissions_policy].dup
312
+ end
313
+
314
+ private
315
+
316
+ # Set the appropriate permissions policy header.
317
+ def set_default_headers
318
+ super
319
+ (@permissions_policy || roda_class.opts[:permissions_policy]).set_header(headers)
320
+ end
321
+ end
322
+ end
323
+
324
+ register_plugin(:permissions_policy, PermissionsPolicy)
325
+ end
326
+ end
@@ -42,6 +42,9 @@ class Roda
42
42
  # This plugin supports the following options:
43
43
  #
44
44
  # :field :: Form input parameter name for CSRF token (default: '_csrf')
45
+ # :formaction_field :: Form input parameter name for path-specific CSRF tokens (used by the
46
+ # +csrf_formaction_tag+ method). If present, this parameter should be
47
+ # submitted as a hash, keyed by path, with CSRF token values.
45
48
  # :header :: HTTP header name for CSRF token (default: 'X-CSRF-Token')
46
49
  # :key :: Session key for CSRF secret (default: '_roda_csrf_secret')
47
50
  # :require_request_specific_tokens :: Whether request-specific tokens are required (default: true).
@@ -86,6 +89,10 @@ class Roda
86
89
  # override any of the plugin options for this specific call.
87
90
  # The :token option can be used to specify the provided CSRF token
88
91
  # (instead of looking for the token in the submitted parameters).
92
+ # csrf_formaction_tag(path, method='POST') :: An HTML hidden input tag string containing the CSRF token, suitable
93
+ # for placing in an HTML form that has inputs that use formaction
94
+ # attributes to change the endpoint to which the form is submitted.
95
+ # Takes the same arguments as csrf_token.
89
96
  # csrf_field :: The field name to use for the hidden tag containing the CSRF token.
90
97
  # csrf_path(action) :: This takes an argument that would be the value of the HTML form's
91
98
  # action attribute, and returns a path you can pass to csrf_token
@@ -152,6 +159,7 @@ class Roda
152
159
  # Default CSRF option values
153
160
  DEFAULTS = {
154
161
  :field => '_csrf'.freeze,
162
+ :formaction_field => '_csrfs'.freeze,
155
163
  :header => 'X-CSRF-Token'.freeze,
156
164
  :key => '_roda_csrf_secret'.freeze,
157
165
  :require_request_specific_tokens => true,
@@ -252,6 +260,14 @@ class Roda
252
260
  end
253
261
  end
254
262
 
263
+ # An HTML hidden input tag string containing the CSRF token, used for inputs
264
+ # with formaction, so the same form can be used to submit to multiple endpoints
265
+ # depending on which button was clicked. See csrf_token for arguments, but the
266
+ # path argument is required.
267
+ def csrf_formaction_tag(path, *args)
268
+ "<input type=\"hidden\" name=\"#{csrf_options[:formaction_field]}[#{Rack::Utils.escape_html(path)}]\" value=\"#{csrf_token(path, *args)}\" \/>"
269
+ end
270
+
255
271
  # An HTML hidden input tag string containing the CSRF token. See csrf_token for
256
272
  # arguments.
257
273
  def csrf_tag(*args)
@@ -291,6 +307,8 @@ class Roda
291
307
  return
292
308
  end
293
309
 
310
+ path = @_request.path
311
+
294
312
  unless encoded_token = opts[:token]
295
313
  encoded_token = case opts[:check_header]
296
314
  when :only
@@ -298,7 +316,8 @@ class Roda
298
316
  when true
299
317
  return (csrf_invalid_message(opts.merge(:check_header=>false)) && csrf_invalid_message(opts.merge(:check_header=>:only)))
300
318
  else
301
- @_request.params[opts[:field]]
319
+ params = @_request.params
320
+ ((formactions = params[opts[:formaction_field]]).is_a?(Hash) && (formactions[path])) || params[opts[:field]]
302
321
  end
303
322
  end
304
323
 
@@ -326,7 +345,7 @@ class Roda
326
345
 
327
346
  random_data = submitted_hmac.slice!(0...31)
328
347
 
329
- if csrf_compare(csrf_hmac(random_data, method, @_request.path), submitted_hmac)
348
+ if csrf_compare(csrf_hmac(random_data, method, path), submitted_hmac)
330
349
  return
331
350
  end
332
351
 
data/lib/roda/response.rb CHANGED
@@ -14,7 +14,8 @@ class Roda
14
14
 
15
15
  %w'Allow Cache-Control Content-Disposition Content-Encoding Content-Length
16
16
  Content-Security-Policy Content-Security-Policy-Report-Only Content-Type
17
- ETag Expires Last-Modified Link Location Set-Cookie Transfer-Encoding Vary'.
17
+ ETag Expires Last-Modified Link Location Set-Cookie Transfer-Encoding Vary
18
+ Permissions-Policy Permissions-Policy-Report-Only'.
18
19
  each do |value|
19
20
  value = value.downcase if downcase
20
21
  const_set(value.gsub('-', '_').upcase!.to_sym, value.freeze)
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 = 76
7
+ RodaMinorVersion = 78
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.76.0
4
+ version: 3.78.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-01-12 00:00:00.000000000 Z
11
+ date: 2024-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -250,6 +250,8 @@ extra_rdoc_files:
250
250
  - doc/release_notes/3.74.0.txt
251
251
  - doc/release_notes/3.75.0.txt
252
252
  - doc/release_notes/3.76.0.txt
253
+ - doc/release_notes/3.77.0.txt
254
+ - doc/release_notes/3.78.0.txt
253
255
  - doc/release_notes/3.8.0.txt
254
256
  - doc/release_notes/3.9.0.txt
255
257
  files:
@@ -333,6 +335,8 @@ files:
333
335
  - doc/release_notes/3.74.0.txt
334
336
  - doc/release_notes/3.75.0.txt
335
337
  - doc/release_notes/3.76.0.txt
338
+ - doc/release_notes/3.77.0.txt
339
+ - doc/release_notes/3.78.0.txt
336
340
  - doc/release_notes/3.8.0.txt
337
341
  - doc/release_notes/3.9.0.txt
338
342
  - lib/roda.rb
@@ -430,6 +434,7 @@ files:
430
434
  - lib/roda/plugins/path.rb
431
435
  - lib/roda/plugins/path_matchers.rb
432
436
  - lib/roda/plugins/path_rewriter.rb
437
+ - lib/roda/plugins/permissions_policy.rb
433
438
  - lib/roda/plugins/placeholder_string_matchers.rb
434
439
  - lib/roda/plugins/plain_hash_response_headers.rb
435
440
  - lib/roda/plugins/precompile_templates.rb