roda 3.77.0 → 3.78.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: 8afa46b8055c19e63e1efeebf444b422cd810701c759936d476797515630245d
4
- data.tar.gz: 4e857447435707de1d586857126795e2a1191f685ca8893e99cd1c78aeb50c02
3
+ metadata.gz: 44eb48fa9dd507c495e32f90e5ef3b9b197d47f1c148f5d91714272afdca74f8
4
+ data.tar.gz: 276ceaf7256b816b4a5c59e7c2508cb9066e1d1693d398e7be43ac17be7a9115
5
5
  SHA512:
6
- metadata.gz: 2ecfeb211d574d46bdc31995c3551afb97af50acf8567cce83b46808e87ff797a5ef7c769e42c0477ed6a6d9271040b818da72e77c91e4f91f9760201d2cd5ca
7
- data.tar.gz: 45e67fbd1da64839c6dc5a8dc0975a0dca5f67a77c83ae79a62ab22e7d9cf0002cb8b8f6a22661bdaf97ad973b6bc6ff928af2e416a96c35b12150e9fe0eaf4d
6
+ metadata.gz: b99672a570f6c119375435546104627d27437cb66372f61df0b7e08a9eb4ae66709b33162d2cf0354b2a22c3ed7c456b60423218cd734a9fe644c9b278c5c44f
7
+ data.tar.gz: 6bd6f328f173bfe2dd28b1b4ea9452158f127ac2c93dcf4de6783af5513d38803e9642c3c24b899e16d05dbdb35569464dc9ca2f00111a8814193e48870afe45
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ = 3.78.0 (2024-03-13)
2
+
3
+ * Add permissions_policy plugin for setting Permissions-Policy header (jeremyevans)
4
+
1
5
  = 3.77.0 (2024-02-12)
2
6
 
3
7
  * Support formaction/formmethod attributes in forms in route_csrf plugin (jeremyevans)
@@ -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
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 = 77
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.77.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-02-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
@@ -251,6 +251,7 @@ extra_rdoc_files:
251
251
  - doc/release_notes/3.75.0.txt
252
252
  - doc/release_notes/3.76.0.txt
253
253
  - doc/release_notes/3.77.0.txt
254
+ - doc/release_notes/3.78.0.txt
254
255
  - doc/release_notes/3.8.0.txt
255
256
  - doc/release_notes/3.9.0.txt
256
257
  files:
@@ -335,6 +336,7 @@ files:
335
336
  - doc/release_notes/3.75.0.txt
336
337
  - doc/release_notes/3.76.0.txt
337
338
  - doc/release_notes/3.77.0.txt
339
+ - doc/release_notes/3.78.0.txt
338
340
  - doc/release_notes/3.8.0.txt
339
341
  - doc/release_notes/3.9.0.txt
340
342
  - lib/roda.rb
@@ -432,6 +434,7 @@ files:
432
434
  - lib/roda/plugins/path.rb
433
435
  - lib/roda/plugins/path_matchers.rb
434
436
  - lib/roda/plugins/path_rewriter.rb
437
+ - lib/roda/plugins/permissions_policy.rb
435
438
  - lib/roda/plugins/placeholder_string_matchers.rb
436
439
  - lib/roda/plugins/plain_hash_response_headers.rb
437
440
  - lib/roda/plugins/precompile_templates.rb