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
|
# 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 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 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
|