mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
2e079154a8
In 90e710d767
the FeaturePolicy middleware
was renamed to PermissionsPolicy as this will be new name of the header
as used by browsers.
The Permissions-Policy header requires a different implementation and
isn't yet supported by all browsers. To avoid having to rename the
middleware in the future, we keep the new name for the Middleware, but
use the old implementation and header name.
173 lines
4.6 KiB
Ruby
173 lines
4.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "active_support/core_ext/object/deep_dup"
|
|
|
|
module ActionDispatch #:nodoc:
|
|
class PermissionsPolicy
|
|
class Middleware
|
|
CONTENT_TYPE = "Content-Type"
|
|
# 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.
|
|
POLICY = "Feature-Policy"
|
|
|
|
def initialize(app)
|
|
@app = app
|
|
end
|
|
|
|
def call(env)
|
|
request = ActionDispatch::Request.new(env)
|
|
_, headers, _ = response = @app.call(env)
|
|
|
|
return response unless html_response?(headers)
|
|
return response if policy_present?(headers)
|
|
|
|
if policy = request.permissions_policy
|
|
headers[POLICY] = policy.build(request.controller_instance)
|
|
end
|
|
|
|
if policy_empty?(policy)
|
|
headers.delete(POLICY)
|
|
end
|
|
|
|
response
|
|
end
|
|
|
|
private
|
|
def html_response?(headers)
|
|
if content_type = headers[CONTENT_TYPE]
|
|
/html/.match?(content_type)
|
|
end
|
|
end
|
|
|
|
def policy_present?(headers)
|
|
headers[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/master/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",
|
|
magnetometer: "magnetometer",
|
|
microphone: "microphone",
|
|
midi: "midi",
|
|
payment: "payment",
|
|
picture_in_picture: "picture-in-picture",
|
|
speaker: "speaker",
|
|
usb: "usb",
|
|
vibrate: "vibrate",
|
|
vr: "vr",
|
|
}.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
|