roda 3.77.0 → 3.78.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +4 -0
- data/doc/release_notes/3.78.0.txt +99 -0
- data/lib/roda/plugins/content_security_policy.rb +1 -1
- data/lib/roda/plugins/permissions_policy.rb +326 -0
- data/lib/roda/response.rb +2 -1
- data/lib/roda/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 44eb48fa9dd507c495e32f90e5ef3b9b197d47f1c148f5d91714272afdca74f8
|
4
|
+
data.tar.gz: 276ceaf7256b816b4a5c59e7c2508cb9066e1d1693d398e7be43ac17be7a9115
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b99672a570f6c119375435546104627d27437cb66372f61df0b7e08a9eb4ae66709b33162d2cf0354b2a22c3ed7c456b60423218cd734a9fe644c9b278c5c44f
|
7
|
+
data.tar.gz: 6bd6f328f173bfe2dd28b1b4ea9452158f127ac2c93dcf4de6783af5513d38803e9642c3c24b899e16d05dbdb35569464dc9ca2f00111a8814193e48870afe45
|
data/CHANGELOG
CHANGED
@@ -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
|
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
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.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-
|
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
|