/
permissions_policy.rb
194 lines (166 loc) · 5.37 KB
/
permissions_policy.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# frozen_string_literal: true
# :markup: markdown
require "active_support/core_ext/object/deep_dup"
module ActionDispatch # :nodoc:
# # Action Dispatch PermissionsPolicy
#
# Configures the HTTP
# [Feature-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy)
# response header to specify which browser features the current
# document and its iframes can use.
#
# Example global policy:
#
# Rails.application.config.permissions_policy do |policy|
# policy.camera :none
# policy.gyroscope :none
# policy.microphone :none
# policy.usb :none
# policy.fullscreen :self
# policy.payment :self, "https://secure.example.com"
# end
#
# The Feature-Policy header has been renamed to Permissions-Policy. The
# Permissions-Policy requires a different implementation and isn't yet supported
# by all browsers. To avoid having to rename this middleware in the future we
# use the new name for the middleware but keep the old header name and
# implementation for now.
class PermissionsPolicy
class Middleware
def initialize(app)
@app = app
end
def call(env)
_, headers, _ = response = @app.call(env)
return response unless html_response?(headers)
return response if policy_present?(headers)
request = ActionDispatch::Request.new(env)
if policy = request.permissions_policy
headers[ActionDispatch::Constants::FEATURE_POLICY] = policy.build(request.controller_instance)
end
if policy_empty?(policy)
headers.delete(ActionDispatch::Constants::FEATURE_POLICY)
end
response
end
private
def html_response?(headers)
if content_type = headers[Rack::CONTENT_TYPE]
content_type.include?("html")
end
end
def policy_present?(headers)
headers[ActionDispatch::Constants::FEATURE_POLICY]
end
def policy_empty?(policy)
policy&.directives&.empty?
end
end
module Request
POLICY = "action_dispatch.permissions_policy"
def permissions_policy
get_header(POLICY)
end
def permissions_policy=(policy)
set_header(POLICY, policy)
end
end
MAPPINGS = {
self: "'self'",
none: "'none'",
}.freeze
# List of available permissions can be found at
# https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md#policy-controlled-features
DIRECTIVES = {
accelerometer: "accelerometer",
ambient_light_sensor: "ambient-light-sensor",
autoplay: "autoplay",
camera: "camera",
encrypted_media: "encrypted-media",
fullscreen: "fullscreen",
geolocation: "geolocation",
gyroscope: "gyroscope",
hid: "hid",
idle_detection: "idle-detection",
magnetometer: "magnetometer",
microphone: "microphone",
midi: "midi",
payment: "payment",
picture_in_picture: "picture-in-picture",
screen_wake_lock: "screen-wake-lock",
serial: "serial",
sync_xhr: "sync-xhr",
usb: "usb",
web_share: "web-share",
}.freeze
private_constant :MAPPINGS, :DIRECTIVES
attr_reader :directives
def initialize
@directives = {}
yield self if block_given?
end
def initialize_copy(other)
@directives = other.directives.deep_dup
end
DIRECTIVES.each do |name, directive|
define_method(name) do |*sources|
if sources.first
@directives[directive] = apply_mappings(sources)
else
@directives.delete(directive)
end
end
end
def build(context = nil)
build_directives(context).compact.join("; ")
end
private
def apply_mappings(sources)
sources.map do |source|
case source
when Symbol
apply_mapping(source)
when String, Proc
source
else
raise ArgumentError, "Invalid HTTP permissions policy source: #{source.inspect}"
end
end
end
def apply_mapping(source)
MAPPINGS.fetch(source) do
raise ArgumentError, "Unknown HTTP permissions policy source mapping: #{source.inspect}"
end
end
def build_directives(context)
@directives.map do |directive, sources|
if sources.is_a?(Array)
"#{directive} #{build_directive(sources, context).join(' ')}"
elsif sources
directive
else
nil
end
end
end
def build_directive(sources, context)
sources.map { |source| resolve_source(source, context) }
end
def resolve_source(source, context)
case source
when String
source
when Symbol
source.to_s
when Proc
if context.nil?
raise RuntimeError, "Missing context for the dynamic permissions policy source: #{source.inspect}"
else
context.instance_exec(&source)
end
else
raise RuntimeError, "Unexpected permissions policy source: #{source.inspect}"
end
end
end
end